-
Notifications
You must be signed in to change notification settings - Fork 2
Sodium Handler
InitPHP\Encryption\Sodium wraps libsodium's crypto_secretbox AEAD
construction (XSalsa20-Poly1305 under the hood). It is the recommended
default: the underlying primitive has no tunables, the API is hard to
misuse, and authentication is part of the construction rather than
something you bolt on afterwards.
use InitPHP\Encryption\Encrypt;
use InitPHP\Encryption\Sodium;
$handler = Encrypt::use(Sodium::class, [
'key' => getenv('APP_ENCRYPTION_KEY'),
// optional, default is 16:
// 'blocksize' => 16,
]);
$ct = $handler->encrypt(['user_id' => 42]);
$pt = $handler->decrypt($ct);That's the whole API. There is no cipher to choose, no algo to choose;
the construction is fixed by libsodium.
encrypt($data, $options)
1. Resolve options (per-call merged on top of persistent options)
2. Require non-empty 'key'
3. Resolve 'blocksize' (default 16; must be positive integer)
4. derivedKey = sodium_crypto_generichash(
$userKey, '',
SODIUM_CRYPTO_SECRETBOX_KEYBYTES // 32
)
5. Serialize $data via the configured serializer (default: JSON)
6. padded = sodium_pad($serialized, $blocksize)
7. nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES) // 24
8. box = sodium_crypto_secretbox($padded, $nonce, $derivedKey)
9. sodium_memzero($derivedKey) ← always, even on exception
10. return bin2hex(VERSION || SERIALIZER || NONCE || BOX)
decrypt($data, $options)
1. Resolve options, require key, resolve blocksize
2. binary = hex2bin($data)
3. Reject if shorter than 2 + nonce + MAC bytes
4. Read 2-byte header; reject if version byte ≠ 0x02
5. Read 24-byte nonce
6. derivedKey = same BLAKE2b derivation as encrypt()
7. padded = sodium_crypto_secretbox_open(...) ← fails if MAC wrong
8. payload = sodium_unpad($padded, $blocksize)
9. sodium_memzero($derivedKey)
10. Deserialize via the serializer flag from the header → return value
Every step that can fail raises EncryptionException — see
Error Handling.
+---------+-----------+----------+----------------------+
| 1 byte | 1 byte | 24 bytes | variable |
+---------+-----------+----------+----------------------+
| VERSION | SERIALIZER| NONCE | secretbox(MAC || CT) |
+---------+-----------+----------+----------------------+
-
VERSIONis0x02for every ciphertext this handler produces in 2.x. -
SERIALIZERis0x00for JSON (default),0x01forphp_serialize. - The 24-byte nonce is
SODIUM_CRYPTO_SECRETBOX_NONCEBYTES. A fresh nonce is generated for every call viarandom_bytes()(the OS CSPRNG). -
secretbox(MAC || CT)is whateversodium_crypto_secretbox()returns: the Poly1305 MAC prepended to the encrypted bytes, with total length = plaintext +SODIUM_CRYPTO_SECRETBOX_MACBYTES(16).
Unlike the OpenSSL handler, the secretbox MAC authenticates nonce + box
implicitly — there is no separate HMAC field on the wire.
See Ciphertext Format for the cross-handler comparison.
You can pass any non-empty string as the user key. The handler runs it
through BLAKE2b (sodium_crypto_generichash) to obtain the 32-byte key
crypto_secretbox requires:
$derivedKey = sodium_crypto_generichash(
$userKey,
'', // no key for the hash itself
SODIUM_CRYPTO_SECRETBOX_KEYBYTES // 32
);Consequences:
- The same user key always derives the same 32-byte key, so two processes with the same user key interoperate with no key-sharing ceremony beyond agreeing on that string.
- The derived key is held in a local buffer and zeroed via
sodium_memzero()in afinallyblock — even if encryption or decryption throws. - The user key you passed in is not zeroed. Managing that buffer is your responsibility.
- BLAKE2b cannot turn a weak user key into a strong one. If your
keyis"password123", that is what the security of the system is worth. See Security Best Practices.
Plaintext length leaks through ciphertext length. The Sodium handler pads
the serialised payload with sodium_pad($payload, $blocksize) to
mitigate that.
-
blocksizedefaults to16. Any positive integer is accepted; digit strings like'32'are coerced. - Larger block size → bigger ciphertext → less length leakage. Use
1only if you genuinely do not care about hiding plaintext length. - An explicit
nullfalls back to the default of 16.0, negatives, floats, or non-numeric strings raiseEncryptionException: The "blocksize" option must be a positive integer.
use InitPHP\Encryption\Sodium;
$handler = new Sodium(['key' => 'short-key-1234']); // 14 bytes, < 32
$ct = $handler->encrypt(['session_id' => 'abc']);
$pt = $handler->decrypt($ct);
// $pt === ['session_id' => 'abc']In 1.x, this silently broke with a SodiumException deep in
crypto_secretbox. 2.x derives, so any non-empty string works.
use InitPHP\Encryption\Sodium;
use InitPHP\Encryption\Exceptions\EncryptionException;
$handler = new Sodium(['key' => 'secret']);
$ct = $handler->encrypt('hello');
$tampered = $ct;
$tampered[-1] = $ct[-1] === '0' ? '1' : '0';
try {
$handler->decrypt($tampered);
} catch (EncryptionException $e) {
echo $e->getMessage(), PHP_EOL;
// → Sodium decryption failed; ciphertext is corrupted or has been tampered with.
}use InitPHP\Encryption\Sodium;
$small = new Sodium(['key' => 'k', 'blocksize' => 1]); // no length hiding
$padded = new Sodium(['key' => 'k', 'blocksize' => 256]); // hide up to 256 bytes
$ctSmall = $small->encrypt('hi');
$ctPadded = $padded->encrypt('hi');
echo strlen($ctSmall), ' vs ', strlen($ctPadded), PHP_EOL;
// Approx: 80 vs 592 hex charactersuse InitPHP\Encryption\Sodium;
$writer = new Sodium(['key' => 'same-user-key']);
$reader = new Sodium(['key' => 'same-user-key']);
echo $reader->decrypt($writer->encrypt('hello')); // → hello- BLAKE2b key derivation runs on every call. It costs microseconds; you do not need to cache the derived key.
- Increasing
blocksizeincreases ciphertext size linearly. The CPU cost of padding is negligible compared to the secretbox operation. - XSalsa20-Poly1305 is software-fast on every modern platform — there is no AES-NI hardware-acceleration story to worry about.
- libsodium is not available in your environment (rare on PHP 7.2+).
- Your compliance regime mandates AES (FIPS 140 contexts).
- You need to interoperate with an existing OpenSSL-encrypt-then-MAC consumer outside PHP.
Otherwise, Sodium is the handler you want.
- OpenSSL Handler — the other built-in handler.
- Configuration Options — every option in detail.
- Ciphertext Format — wire format reference.
-
Error Handling — every
EncryptionException. - Security Best Practices — threat model and key management.
initphp/encryption · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Handlers
Reference
Practical Guides
Other