Skip to content

Commit 016938f

Browse files
committed
feat(vault): add marshaller and domain exceptions
1 parent afef94c commit 016938f

18 files changed

+287
-53
lines changed

docs/components/vault.md

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,62 @@
11
# Vault
22

3-
The Vault component securely stores secrets using pluggable storage backends and encryption with key rotation and support for additional authenticated data (AAD).
3+
The Vault component securely stores secrets using pluggable storage backends and encryption with key rotation, versioning, and additional authenticated data (AAD).
44

5-
## Basic Usage
5+
## Setup
66

77
```php
88
use SonsOfPHP\Component\Vault\Cipher\OpenSSLCipher;
99
use SonsOfPHP\Component\Vault\KeyRing\InMemoryKeyRing;
10+
use SonsOfPHP\Component\Vault\Marshaller\JsonMarshaller;
1011
use SonsOfPHP\Component\Vault\Storage\InMemoryStorage;
1112
use SonsOfPHP\Component\Vault\Vault;
1213

13-
$keyRing = new InMemoryKeyRing(['v1' => '32_byte_master_key_example!!'], 'v1');
14-
$vault = new Vault(new InMemoryStorage(), new OpenSSLCipher(), $keyRing);
15-
$vault->set('db_password', 'secret', ['app']);
16-
$secret = $vault->get('db_password', ['app']);
14+
$storage = new InMemoryStorage();
15+
$cipher = new OpenSSLCipher();
16+
$keyRing = new InMemoryKeyRing(['v1' => '32_byte_master_key_example!!'], 'v1');
17+
$marshaller = new JsonMarshaller();
18+
$vault = new Vault($storage, $cipher, $keyRing, $marshaller);
19+
```
20+
21+
## Storing and Retrieving Secrets
22+
23+
```php
24+
$vault->set('db_password', 'secret');
25+
$secret = $vault->get('db_password');
26+
```
27+
28+
## Using Additional Authenticated Data
1729

18-
// Store a new version of the secret
19-
$vault->set('db_password', 'new-secret', ['app']);
20-
$oldSecret = $vault->get('db_password', ['app'], 1); // retrieve version 1
30+
```php
31+
$vault->set('token', 'secret', ['app']);
32+
$secret = $vault->get('token', ['app']);
33+
```
34+
35+
## Versioned Secrets
36+
37+
```php
38+
$vault->set('db_password', 'old-secret');
39+
$vault->set('db_password', 'new-secret');
2140

22-
// Rotate the master key
41+
// Retrieve specific version
42+
$old = $vault->get('db_password', [], 1);
43+
```
44+
45+
## Rotating Keys
46+
47+
```php
2348
$vault->rotateKey('v2', 'another_32_byte_master_key!!');
49+
```
2450

25-
// Store non-string data
51+
Secrets encrypted with previous keys remain accessible because the key ring keeps old keys.
52+
53+
## Storing Non-String Data
54+
55+
```php
2656
$vault->set('config', ['user' => 'root']);
2757
$config = $vault->get('config');
2858
```
59+
60+
## Custom Marshallers
61+
62+
Vault uses a marshaller to convert secrets to strings before encryption. The default `JsonMarshaller` handles arrays and scalars, but you can provide your own implementation of `MarshallerInterface` for advanced use cases.

src/SonsOfPHP/Component/Vault/AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,13 @@
1212
- Test this package only: `PHPUNIT_OPTIONS='src/SonsOfPHP/Component/Vault/Tests' make test`
1313
- Style and static analysis: `make php-cs-fixer` and `make psalm`
1414
- Upgrade code (may modify files): `make upgrade-code`
15+
16+
## Testing Guidelines
17+
18+
- Use PHPUnit attributes such as `#[CoversClass]` for coverage.
19+
- Each test method should verify a single behavior.
20+
21+
## Component Notes
22+
23+
- Secrets are marshalled using implementations of `MarshallerInterface`.
24+
- Prefer domain-specific exceptions under `Exception/` when reporting errors.

src/SonsOfPHP/Component/Vault/Cipher/OpenSSLCipher.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
namespace SonsOfPHP\Component\Vault\Cipher;
66

7-
use RuntimeException;
87
use SensitiveParameter;
8+
use SonsOfPHP\Component\Vault\Exception\DecryptionFailedException;
9+
use SonsOfPHP\Component\Vault\Exception\EncryptionFailedException;
10+
use SonsOfPHP\Component\Vault\Exception\InvalidCiphertextException;
911

1012
/**
1113
* Encrypts and decrypts secrets using OpenSSL.
@@ -17,7 +19,11 @@ class OpenSSLCipher implements CipherInterface
1719
*/
1820
public function __construct(private readonly string $cipherMethod = 'aes-256-gcm') {}
1921

20-
/** @param array<array-key, mixed> $aad */
22+
/**
23+
* @param array<array-key, mixed> $aad Additional authenticated data.
24+
*
25+
* @throws EncryptionFailedException
26+
*/
2127
public function encrypt(#[SensitiveParameter] string $plaintext, #[SensitiveParameter] string $key, array $aad = []): string
2228
{
2329
$ivLength = openssl_cipher_iv_length($this->cipherMethod);
@@ -26,18 +32,23 @@ public function encrypt(#[SensitiveParameter] string $plaintext, #[SensitivePara
2632
$encodedAad = json_encode($aad);
2733
$encrypted = openssl_encrypt($plaintext, $this->cipherMethod, $key, OPENSSL_RAW_DATA, $iv, $tag, $encodedAad, 16);
2834
if (false === $encrypted) {
29-
throw new RuntimeException('Unable to encrypt secret.');
35+
throw new EncryptionFailedException('Unable to encrypt secret.');
3036
}
3137

3238
return base64_encode($iv . $tag . $encrypted);
3339
}
3440

35-
/** @param array<array-key, mixed> $aad */
41+
/**
42+
* @param array<array-key, mixed> $aad Additional authenticated data.
43+
*
44+
* @throws InvalidCiphertextException
45+
* @throws DecryptionFailedException
46+
*/
3647
public function decrypt(#[SensitiveParameter] string $ciphertext, #[SensitiveParameter] string $key, array $aad = []): string
3748
{
3849
$data = base64_decode($ciphertext, true);
3950
if (false === $data) {
40-
throw new RuntimeException('Invalid ciphertext.');
51+
throw new InvalidCiphertextException('Invalid ciphertext.');
4152
}
4253

4354
$ivLength = openssl_cipher_iv_length($this->cipherMethod);
@@ -48,7 +59,7 @@ public function decrypt(#[SensitiveParameter] string $ciphertext, #[SensitivePar
4859
$encodedAad = json_encode($aad);
4960
$decrypted = openssl_decrypt($payload, $this->cipherMethod, $key, OPENSSL_RAW_DATA, $iv, $tag, $encodedAad);
5061
if (false === $decrypted) {
51-
throw new RuntimeException('Unable to decrypt secret.');
62+
throw new DecryptionFailedException('Unable to decrypt secret.');
5263
}
5364

5465
return $decrypted;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SonsOfPHP\Component\Vault\Exception;
6+
7+
/**
8+
* Base exception for cipher related failures.
9+
*/
10+
class CipherException extends VaultException {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SonsOfPHP\Component\Vault\Exception;
6+
7+
/**
8+
* Thrown when a value cannot be decrypted.
9+
*/
10+
class DecryptionFailedException extends CipherException {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SonsOfPHP\Component\Vault\Exception;
6+
7+
/**
8+
* Thrown when a value cannot be encrypted.
9+
*/
10+
class EncryptionFailedException extends CipherException {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SonsOfPHP\Component\Vault\Exception;
6+
7+
/**
8+
* Thrown when ciphertext cannot be decoded before decryption.
9+
*/
10+
class InvalidCiphertextException extends CipherException {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SonsOfPHP\Component\Vault\Exception;
6+
7+
/**
8+
* Thrown when marshalling or unmarshalling secret data fails.
9+
*/
10+
class MarshallingException extends VaultException {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SonsOfPHP\Component\Vault\Exception;
6+
7+
/**
8+
* Thrown when the stored secret data is malformed or corrupted.
9+
*/
10+
class SecretStorageCorruptedException extends VaultException {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SonsOfPHP\Component\Vault\Exception;
6+
7+
/**
8+
* Thrown when a key cannot be found in the key ring.
9+
*/
10+
class UnknownKeyException extends VaultException {}

0 commit comments

Comments
 (0)