Encrypting Secrets at Rest
Overview
Sukarix\Security\SecretBox is a thin wrapper around libsodium’s secretbox (XSalsa20-Poly1305) for encrypting
credentials before they are stored — a third-party API key or OAuth token sitting in a database column, for example.
It is authenticated encryption: a tampered or foreign-key blob fails to decrypt rather than silently returning
garbage.
A fresh nonce is generated for every call and prefixed to the ciphertext, so encrypting the same plaintext twice never produces the same blob — a database dump reveals nothing by comparison.
Provisioning a Key
Generate a key once per deployment and keep it out of version control, the same way as any other secret (see Environment Variables):
use Sukarix\Security\SecretBox;
echo SecretBox::generateKey(); // 64 hex characters — store this as an env var
Encrypting and Decrypting
$box = SecretBox::fromKeyMaterial(getenv('SECRETS_KEY'));
$blob = $box->encrypt('sk_live_...'); // store $blob
$plaintext = $box->decrypt($blob); // 'sk_live_...'
fromKeyMaterial() accepts the key as 64 hex characters, base64, or 32 raw bytes, and throws \RuntimeException for
anything else. Structured data — several credentials belonging to one integration, say — can be encrypted as a whole
with encryptArray()/decryptArray(), which JSON-encode the value before sealing it:
$blob = $box->encryptArray(['client_id' => '...', 'client_secret' => '...']);
$credentials = $box->decryptArray($blob); // ['client_id' => '...', 'client_secret' => '...']
Warning
Losing the key means losing every secret sealed with it
There is no recovery path built in. Back the key up the same way you would a database encryption key, and rotate it by decrypting with the old key and re-encrypting with the new one —
SecretBoxdoes not do this for you.
Failure Modes
decrypt() and decryptArray() throw \RuntimeException in two cases: the stored value is not a valid sealed blob
(truncated, not base64, or shorter than a nonce), or it was sealed with a different key. Both cases look identical
from the caller’s side — decryption either succeeds with the original plaintext or throws, never a partial or
corrupted result.