|
5 | 5 | namespace SonsOfPHP\Component\Vault; |
6 | 6 |
|
7 | 7 | use RuntimeException; |
| 8 | +use SensitiveParameter; |
8 | 9 | use SonsOfPHP\Component\Vault\Cipher\CipherInterface; |
| 10 | +use SonsOfPHP\Component\Vault\KeyRing\KeyRingInterface; |
9 | 11 | use SonsOfPHP\Component\Vault\Storage\StorageInterface; |
10 | 12 |
|
11 | 13 | /** |
|
15 | 17 | class Vault |
16 | 18 | { |
17 | 19 | /** |
18 | | - * @param StorageInterface $storage The storage backend. |
19 | | - * @param CipherInterface $cipher The cipher used for encryption. |
20 | | - * @param array<string,string> $keys Map of key IDs to encryption keys. |
21 | | - * @param string $currentKeyId Identifier of the active key. |
| 20 | + * @param StorageInterface $storage The storage backend. |
| 21 | + * @param CipherInterface $cipher The cipher used for encryption. |
| 22 | + * @param KeyRingInterface $keyRing Provides access to encryption keys. |
22 | 23 | */ |
23 | | - public function __construct(private readonly StorageInterface $storage, private readonly CipherInterface $cipher, private array $keys, private string $currentKeyId) |
24 | | - { |
25 | | - } |
| 24 | + public function __construct(private readonly StorageInterface $storage, private readonly CipherInterface $cipher, private readonly KeyRingInterface $keyRing) {} |
26 | 25 |
|
27 | 26 | /** |
28 | 27 | * Stores a secret in the vault. |
29 | 28 | * |
30 | | - * @param string $name Identifier of the secret. |
31 | | - * @param mixed $secret The secret to store. |
32 | | - * @param string $aad Additional authenticated data. |
| 29 | + * @param string $name Identifier of the secret. |
| 30 | + * @param mixed $secret The secret to store. |
| 31 | + * @param array<array-key, mixed> $aad Additional authenticated data. |
33 | 32 | */ |
34 | | - public function set(string $name, mixed $secret, string $aad = ''): void |
| 33 | + public function set(string $name, #[SensitiveParameter] mixed $secret, array $aad = []): void |
35 | 34 | { |
36 | 35 | $serialized = serialize($secret); |
37 | | - $key = $this->keys[$this->currentKeyId]; |
| 36 | + $key = $this->keyRing->getCurrentKey(); |
38 | 37 | $encrypted = $this->cipher->encrypt($serialized, $key, $aad); |
39 | | - $this->storage->set($name, $this->currentKeyId . ':' . $encrypted); |
| 38 | + |
| 39 | + $record = $this->storage->get($name); |
| 40 | + if (null === $record) { |
| 41 | + $versions = []; |
| 42 | + } else { |
| 43 | + $versions = @unserialize($record, ['allowed_classes' => false]); |
| 44 | + if (!is_array($versions)) { |
| 45 | + throw new RuntimeException('Invalid secret storage format.'); |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + $version = $versions === [] ? 1 : max(array_map('intval', array_keys($versions))) + 1; |
| 50 | + $versions[(string) $version] = $this->keyRing->getCurrentKeyId() . ':' . $encrypted; |
| 51 | + $this->storage->set($name, serialize($versions)); |
40 | 52 | } |
41 | 53 |
|
42 | 54 | /** |
43 | 55 | * Retrieves a secret from the vault or null if it does not exist. |
44 | 56 | * |
45 | | - * @param string $name Identifier of the secret. |
46 | | - * @param string $aad Additional authenticated data. |
| 57 | + * @param string $name Identifier of the secret. |
| 58 | + * @param array<array-key, mixed> $aad Additional authenticated data. |
| 59 | + * @param int|null $version Specific version to retrieve or null for latest. |
47 | 60 | * |
48 | 61 | * @return mixed|null |
49 | 62 | */ |
50 | | - public function get(string $name, string $aad = ''): mixed |
| 63 | + public function get(string $name, array $aad = [], ?int $version = null): mixed |
51 | 64 | { |
52 | 65 | $record = $this->storage->get($name); |
53 | 66 | if (null === $record) { |
54 | 67 | return null; |
55 | 68 | } |
56 | 69 |
|
57 | | - [$keyId, $ciphertext] = explode(':', $record, 2); |
58 | | - $key = $this->keys[$keyId] ?? null; |
| 70 | + $versions = @unserialize($record, ['allowed_classes' => false]); |
| 71 | + if (!is_array($versions)) { |
| 72 | + throw new RuntimeException('Invalid secret storage format.'); |
| 73 | + } |
| 74 | + |
| 75 | + if (null === $version) { |
| 76 | + $version = (int) max(array_map('intval', array_keys($versions))); |
| 77 | + } |
| 78 | + |
| 79 | + $entry = $versions[(string) $version] ?? null; |
| 80 | + if (null === $entry) { |
| 81 | + return null; |
| 82 | + } |
| 83 | + |
| 84 | + [$keyId, $ciphertext] = explode(':', (string) $entry, 2); |
| 85 | + $key = $this->keyRing->getKey($keyId); |
59 | 86 | if (null === $key) { |
60 | 87 | throw new RuntimeException('Unknown key identifier.'); |
61 | 88 | } |
@@ -84,9 +111,8 @@ public function delete(string $name): void |
84 | 111 | * @param string $keyId Identifier for the new key. |
85 | 112 | * @param string $key The new encryption key. |
86 | 113 | */ |
87 | | - public function rotateKey(string $keyId, string $key): void |
| 114 | + public function rotateKey(string $keyId, #[SensitiveParameter] string $key): void |
88 | 115 | { |
89 | | - $this->keys[$keyId] = $key; |
90 | | - $this->currentKeyId = $keyId; |
| 116 | + $this->keyRing->rotate($keyId, $key); |
91 | 117 | } |
92 | 118 | } |
0 commit comments