You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Split off the libsodium impl into phpnomad/sodium-integration
phpnomad/encryption is now the contract only — interfaces, EncryptedValue,
FieldEncrypter, key providers — with no ext-sodium dependency. The
SodiumEncryptionStrategy (XChaCha20-Poly1305 AEAD) + legacy secretbox
strategy move to phpnomad/sodium-integration, matching the ecosystem's
contract + *-integration convention (fetch / guzzle-fetch). Contract tests
run against an in-package reversible fake, so the package needs no cipher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: README.md
+65-31Lines changed: 65 additions & 31 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,34 +1,49 @@
1
1
# phpnomad/encryption
2
2
3
-
Interface-driven [libsodium](https://www.php.net/manual/en/book.sodium.php) encryption primitives for PHP: authenticated encryption (AEAD), versioned keys with rotation, and framework-agnostic field-level encryption. No framework, ORM, or datastore dependency — just PHP and `ext-sodium`.
4
-
5
-
-**Authenticated by default.** Every ciphertext is sealed with XChaCha20-Poly1305 AEAD, so tampering is detected on decrypt.
6
-
-**Context binding.** Pin a ciphertext to the row, column, or tenant it belongs to. A value copied elsewhere won't decrypt.
7
-
-**Key rotation built in.** Keys are versioned; encrypt with the current version, decrypt against whatever version sealed the data.
8
-
-**Field-level helper.** Encrypt-on-write / decrypt-on-read for marked fields of a plain array — wire it into any storage layer.
9
-
-**Migration-friendly.** Can read legacy `sodium_crypto_secretbox` data while you move to AEAD.
3
+
Encryption **contracts** and framework-agnostic **field-level encryption** for
4
+
PHPNomad — bring your own cipher via an integration package. This package holds
5
+
only interfaces, value objects, key providers, and the field/attribute helpers.
6
+
It has no cipher dependency (no `ext-sodium`, no framework, no ORM) — just PHP.
7
+
8
+
The default cipher lives in a separate integration:
use PHPNomad\Encryption\Providers\Base64EnvKeyProvider;
71
+
use PHPNomad\Sodium\EncryptionIntegration\Strategies\SodiumEncryptionStrategy;
56
72
57
73
// Reads a base64-encoded 32-byte key from APP_MASTER_KEY (file fallback optional).
58
74
$keys = new Base64EnvKeyProvider('APP_MASTER_KEY', __DIR__ . '/.master_key');
@@ -61,7 +77,9 @@ $encryption = new SodiumEncryptionStrategy($keys);
61
77
62
78
## Associated data (AEAD context)
63
79
64
-
The second argument to `encrypt()`/`decrypt()` is **associated data**: authenticated but not encrypted. Use it to bind a ciphertext to where it lives. The *same* context must be supplied to decrypt.
80
+
The second argument to `encrypt()`/`decrypt()` is **associated data**:
81
+
authenticated but not encrypted. Use it to bind a ciphertext to where it lives.
This turns an encrypted-value swap between rows or columns from a silent success into a hard failure.
91
+
This turns an encrypted-value swap between rows or columns from a silent success
92
+
into a hard failure.
74
93
75
94
## Field-level encryption
76
95
77
-
`FieldEncrypter` transparently encrypts a fixed set of fields on an associative array and decrypts them on the way back — no storage coupling. Each field is bound to its own AEAD context (`"{context}:{field}"`), so values can't be swapped between columns.
96
+
`FieldEncrypter` transparently encrypts a fixed set of fields on an associative
97
+
array and decrypts them on the way back — no storage coupling, and cipher-agnostic
98
+
(pass any `EncryptionStrategy`). Each field is bound to its own AEAD context
99
+
(`"{context}:{field}"`), so values can't be swapped between columns.
`encryptRow()` is idempotent (already-encrypted and `null` values are left alone), so it's safe on partial updates.
118
+
`encryptRow()` is idempotent (already-encrypted and `null` values are left
119
+
alone), so it's safe on partial updates.
97
120
98
-
Prefer a trait? `EncryptsFields` wires the same behavior into a datastore adapter or repository:
121
+
Prefer a trait? `EncryptsFields` wires the same behavior into a datastore adapter
122
+
or repository:
99
123
100
124
```php
101
125
use PHPNomad\Encryption\Interfaces\EncryptionStrategy;
@@ -120,7 +144,8 @@ final class TokenRepository
120
144
121
145
## Key rotation
122
146
123
-
Keys are addressed by version. Keep every version still referenced by stored ciphertext; point the ring's current version at the newest key.
147
+
Keys are addressed by version. Keep every version still referenced by stored
148
+
ciphertext; point the ring's current version at the newest key.
124
149
125
150
```php
126
151
// v1 was current when old values were sealed. Now add v2 and make it current.
@@ -135,25 +160,30 @@ $encryption->encrypt('x'); // sealed under v2
135
160
$encryption->decrypt($oldValue); // still decrypts against v1
136
161
```
137
162
138
-
To fully migrate, decrypt each stored value and re-encrypt it (the new `EncryptedValue` carries `keyVersion = 2`), then retire the old key once nothing references it. Multiple keys can also live behind separate providers via `KeyRing` (e.g. one `Base64EnvKeyProvider` per env var).
163
+
To fully migrate, decrypt each stored value and re-encrypt it (the new
164
+
`EncryptedValue` carries `keyVersion = 2`), then retire the old key once nothing
165
+
references it. Multiple keys can also live behind separate providers via
166
+
`KeyRing` (e.g. one `Base64EnvKeyProvider` per env var).
139
167
140
-
## Reading legacy `secretbox` data
168
+
## Writing a cipher integration
141
169
142
-
If you're adopting this library over data previously encrypted with `sodium_crypto_secretbox`, enable the fallback so unmarked values are tried as AEAD first and then as secretbox:
170
+
Implement `Interfaces\EncryptionStrategy`and return an `EncryptedValue`:
143
171
144
172
```php
145
-
$encryption = new SodiumEncryptionStrategy($keys, allowLegacySecretboxFallback: true);
146
-
```
147
-
148
-
New writes are always AEAD; old secretbox values keep decrypting until you re-encrypt them. A standalone `LegacySecretboxEncryptionStrategy` is also provided for read-only or explicit secretbox handling.
173
+
use PHPNomad\Encryption\Interfaces\EncryptionStrategy;
174
+
use PHPNomad\Encryption\ValueObjects\EncryptedValue;
149
175
150
-
## Security notes
176
+
final class MyCipherStrategy implements EncryptionStrategy
177
+
{
178
+
public function encrypt(string $plaintext, string $context = ''): EncryptedValue { /* ... */ }
179
+
public function decrypt(EncryptedValue $value, string $context = ''): string { /* ... */ }
180
+
}
181
+
```
151
182
152
-
-**XChaCha20-Poly1305** is used because its 24-byte nonce is large enough to pick at random per message without collision worries — no nonce counter to persist. Keys are 32 bytes.
153
-
- Keys are wiped from memory with `sodium_memzero` after each operation.
154
-
- Decryption failure (wrong key, wrong context, or tampering) always raises `DecryptionFailedException` — never a partial or forged plaintext.
155
-
- Associated data is authenticated, **not** encrypted. Don't put secrets in the context.
156
-
- This library encrypts values; it does **not** manage where your keys come from or how they're stored. Keep keys out of source control (environment variables, a secrets manager, or a KMS).
183
+
The contract requires that decryption fail (throw `DecryptionFailedException`) on
184
+
a wrong key, a mismatched `$context`, or tampered bytes, and that it decrypt
185
+
against the key version recorded on the value. See `phpnomad/sodium-integration`
186
+
for the reference implementation.
157
187
158
188
## API at a glance
159
189
@@ -162,13 +192,13 @@ New writes are always AEAD; old secretbox values keep decrypting until you re-en
0 commit comments