Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 — SecretBox does 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.