ez-php / encryption
Symmetric authenticated encryption (ext-sodium XChaCha20-Poly1305) with key rotation, encrypted cookies and an ORM cast
Requires
- php: ^8.5
- ext-sodium: *
- ez-php/contracts: ^2.7.0
- ez-php/http: ^2.7.0
Requires (Dev)
- ez-php/console: ^2.7.0
- ez-php/docker: ^2.7.0
- ez-php/orm: ^2.7.0
- ez-php/testing-application: ^2.7.0
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^13.0
Suggests
- ez-php/console: Needed for the encryption:key command
- ez-php/orm: Needed for the Orm\EncryptedString cast
Provides
None
Conflicts
None
Replaces
None
README
Symmetric authenticated encryption for ez-php: an Encrypter over ext-sodium's
XChaCha20-Poly1305 AEAD with key rotation, a Crypt static façade, transparent cookie
encryption and an ORM cast for encrypted columns.
Installation
composer require ez-php/encryption
Requires ext-sodium (bundled with standard PHP builds).
Setup
ez encryption:key # prints base64:...
ENCRYPTION_KEY=base64:... ENCRYPTION_PREVIOUS_KEYS= # comma-separated, decrypt-only
// provider/modules.php EzPhp\Encryption\EncryptionServiceProvider::class,
config/encryption.php:
return [ 'key' => getenv('ENCRYPTION_KEY') ?: '', 'previous_keys' => getenv('ENCRYPTION_PREVIOUS_KEYS') ?: '', 'cookie_except' => ['XSRF-TOKEN'], ];
Usage
use EzPhp\Encryption\Crypt; use EzPhp\Encryption\Encrypter; $payload = Crypt::encrypt('secret'); // "ez1.…", URL- and cookie-safe $plain = Crypt::decrypt($payload); // throws DecryptException on tampering // Bind a payload to a purpose — it only decrypts with the same context. $token = $encrypter->encrypt($userId, 'password-reset'); $encrypter->decrypt($token, 'password-reset');
Inject Encrypter where you can; the façade exists for static call sites.
Key rotation
ez encryption:key→ new key.- Move the old key to
ENCRYPTION_PREVIOUS_KEYS, set the new one asENCRYPTION_KEY. - New payloads use the new key; old ones still decrypt. Drop the old key once nothing encrypted with it remains.
Encrypted cookies
$app->middleware(EzPhp\Encryption\Middleware\EncryptedCookieMiddleware::class);
Register it early in the global stack, before middleware that reads cookies through the
Request (e.g. MaintenanceModeMiddleware's bypass cookie). It only sees cookies set on the
Response and read from the Request: PHP's native session cookie (session_start(),
$_COOKIE) never passes through it and stays an opaque, unencrypted id.
Handlers read and queue plaintext ($request->cookie(), $response->withCookie()); the
browser only sees ciphertext. A cookie that does not decrypt — tampered, copied from another
cookie name, or made with a retired key — is dropped. Cookies listed in
encryption.cookie_except (default XSRF-TOKEN) stay plaintext.
Encrypted ORM columns (ez-php/orm)
use EzPhp\Encryption\Orm\EncryptedString; final class Account extends Entity { protected static array $casts = ['iban' => EncryptedString::class]; } $account->iban = EncryptedString::of('DE89 3704 0044 0532 0130 00'); $account->iban->value(); // plaintext
The column stores the payload (use TEXT/VARCHAR(255)+). An unchanged value keeps its
stored ciphertext, so loading and saving an entity does not mark it dirty.
License
MIT