From 8dda00bb6b15ca797d125c6f1422986bdf6ba1c0 Mon Sep 17 00:00:00 2001 From: Hannes Papenberg Date: Thu, 20 Aug 2026 19:19:33 +0200 Subject: [PATCH] Add documentation --- docs/index.md | 8 +- docs/overview.md | 175 +++++++++++++++++++++++++++++++++++++++- docs/v2-to-v3-update.md | 38 ++++++++- docs/v3-to-v4-update.md | 34 +++++++- 4 files changed, 242 insertions(+), 13 deletions(-) diff --git a/docs/index.md b/docs/index.md index 8ca42a4a..805bd724 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,5 @@ -* [Overview](overview.md) -* [Updating from v2 to v3](v2-to-v3-update.md) -* [Updating from v3 to v4](v3-to-v4-update.md) +* Guide + * [Overview](overview.md) +* Upgrading + * [Updating from v2 to v3](v2-to-v3-update.md) + * [Updating from v3 to v4](v3-to-v4-update.md) diff --git a/docs/overview.md b/docs/overview.md index 1a3c9325..7a7f3ce5 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,3 +1,174 @@ -# Crypt package +# Overview -TODO +The Crypt package provides symmetric encryption behind a common cipher interface, plus a key object +and a set of typed exceptions. + +```bash +composer require joomla/crypt +``` + +## What is in the package + +| Class | Purpose | +|---|---| +| `Joomla\Crypt\Crypt` | Pairs a cipher with a key and exposes `encrypt()`, `decrypt()`, `generateKey()` | +| `Joomla\Crypt\CipherInterface` | The cipher contract | +| `Joomla\Crypt\Cipher\Crypto` | Wrapper around `defuse/php-encryption` — **the one to use** | +| `Joomla\Crypt\Cipher\Sodium` | libsodium `crypto_box` | +| `Joomla\Crypt\Cipher\OpenSSL` | ext-openssl — see the warning below | +| `Joomla\Crypt\Key` | Holds a key type plus the private and public key material | +| `Joomla\Crypt\Exception\*` | `CryptExceptionInterface` and five concrete exceptions | + +## Which cipher to use + +Use **`Cipher\Crypto`** unless you have a specific reason not to. It delegates key generation, +encryption and authentication to `defuse/php-encryption`, a library built and reviewed for exactly +this purpose. + +```bash +composer require defuse/php-encryption +``` + +```php +use Joomla\Crypt\Cipher\Crypto; +use Joomla\Crypt\Crypt; + +$cipher = new Crypto(); + +// Generate a key once and store the ASCII form somewhere safe. +$key = $cipher->generateKey(); +$secret = $key->getPrivate(); // save this + +$crypt = new Crypt($cipher, $key); + +$ciphertext = $crypt->encrypt('some secret value'); +$plaintext = $crypt->decrypt($ciphertext); +``` + +To use a stored key later, rebuild the `Key` object with the same type: + +```php +use Joomla\Crypt\Key; + +$key = new Key('crypto', $secret, ''); +$crypt = new Crypt(new Crypto(), $key); +``` + +`Crypto::generateKey()` currently puts the raw key bytes into the key object's *public* field. That +field is meaningless for a symmetric algorithm, so treat `getPublic()` on a `crypto` key as secret +material and never log, transmit or store it. + +## Handling failures + +Everything throws a typed exception implementing `CryptExceptionInterface`: + +```php +use Joomla\Crypt\Exception\DecryptionException; +use Joomla\Crypt\Exception\InvalidKeyTypeException; + +try { + $plaintext = $crypt->decrypt($ciphertext); +} catch (DecryptionException $e) { + // Wrong key, or the ciphertext was tampered with. +} catch (InvalidKeyTypeException $e) { + // The Key does not belong to this cipher. +} +``` + +| Exception | Raised when | +|---|---| +| `EncryptionException` | Encryption failed | +| `DecryptionException` | Decryption failed or the ciphertext was modified | +| `InvalidKeyException` | A key could not be generated | +| `InvalidKeyTypeException` | The key type does not match the cipher | +| `UnsupportedCipherException` | Declared for unsupported environments, but never actually thrown | + +Check support before choosing a cipher: + +```php +if (!Crypto::isSupported()) { + // fall back, or fail loudly +} +``` + +`Sodium::isSupported()` returns `true` unconditionally rather than checking that the extension is +present, so verify with `extension_loaded('sodium')` yourself if that matters. + +## Random bytes + +```php +Crypt::genRandomBytes(32); // binary string, from random_bytes() +``` + +The result is **binary**. Run it through `bin2hex()` or `base64_encode()` before putting it in a +URL, a database column or a header. + +## The Sodium cipher + +`Cipher\Sodium` uses `sodium_crypto_box` and needs a nonce set before use: + +```php +use Joomla\Crypt\Cipher\Sodium; + +$cipher = new Sodium(); +$key = $cipher->generateKey(); + +$nonce = random_bytes(SODIUM_CRYPTO_BOX_NONCEBYTES); // 24 bytes +$cipher->setNonce($nonce); + +$ciphertext = $cipher->encrypt('message', $key); +``` + +Two things the package does not do for you, and both are essential: + +* **Generate the nonce.** There is no helper; you must produce 24 random bytes yourself. +* **Vary it per message.** The nonce is stored on the cipher instance and reused for every + `encrypt()` call on it. Reusing a nonce with the same key breaks XSalsa20-Poly1305 completely — + it exposes the XOR of the plaintexts and undermines the authenticator. Create a fresh nonce for + each message and store it alongside the ciphertext: + +```php +$nonce = random_bytes(SODIUM_CRYPTO_BOX_NONCEBYTES); +$cipher->setNonce($nonce); +$stored = base64_encode($nonce) . ':' . base64_encode($cipher->encrypt($message, $key)); +``` + +## The OpenSSL cipher + +> **Do not use `Cipher\OpenSSL` for new work.** Its initialisation vector is fixed at construction +> and reused for every message, it applies no authentication to the ciphertext, and +> `generateKey()` uses the supplied passphrase directly as the raw key with no derivation. A fixed +> IV means identical plaintexts produce identical ciphertexts, and in a stream mode it means key +> stream reuse; the missing authentication means a modified ciphertext decrypts without complaint. +> Use `Cipher\Crypto` instead. + +If you are stuck with it, one more detail matters. The option is called `passphrase` and the docs +describe it as a passphrase file, but the value is never read as a file — it is handed to +`openssl_encrypt()` as the key material verbatim: + +```php +$key = $cipher->generateKey(['passphrase' => '/path/to/secret.dat']); +// the key is the string '/path/to/secret.dat', not the contents of that file +``` + +Combined with the cipher method's key length — `aes-128-cbc` consumes the first **16 bytes** and +ignores the rest — two "different" passphrase files in the same directory produce the *same* +encryption key, because their paths share that prefix: + +```php +$a = $cipher->generateKey(['passphrase' => '/var/www/keys/tenant-a.dat']); +$b = $cipher->generateKey(['passphrase' => '/var/www/keys/tenant-b.dat']); +// aes-128-cbc sees '/var/www/keys/t' for both - the same key +``` + +If you must keep this cipher, pass the secret itself rather than a path, make sure it is at least +as long as the method's key size, and generate it with `random_bytes()`. + +## Not part of this package + +* No password hashing — that lives in `joomla/authentication` (`Password\BCryptHandler` and the + Argon2 handlers). +* No key derivation (`sodium_crypto_pwhash`, PBKDF2, HKDF). +* No constant-time comparison helper — use `hash_equals()` directly. +* No signatures (`sodium_crypto_sign_*`), no AEAD cipher, no ciphertext envelope carrying the + cipher identifier, IV/nonce and key id, and therefore no key rotation support. diff --git a/docs/v2-to-v3-update.md b/docs/v2-to-v3-update.md index 0b54dc80..e100d15b 100644 --- a/docs/v2-to-v3-update.md +++ b/docs/v2-to-v3-update.md @@ -1,7 +1,37 @@ -## Updating from v2 to v3 +# Updating from v2 to v3 -The following changes were made to the Crypt package between v2 and v3. +Release 3.0.0 raises the PHP requirement and reformats the codebase. **No public or protected +method signature changed**, so code written against 2.x keeps working on PHP 8.1. -### Minimum supported PHP version raised +## At a glance -All Framework packages now require PHP 8.1 or newer. +| | v2 (2.0.1) | v3 (3.0.0) | +|---|---|---| +| PHP | `^7.2.5 \| ^8.0` | `^8.1.0` | +| Public API | — | unchanged | +| Coding style | Joomla Coding Standard | PSR-12 | + +## Minimum supported PHP version raised + +All Framework packages now require **PHP 8.1** or newer. + +## No API changes + +`Crypt`, `CipherInterface`, `Key`, the three ciphers and all five exception classes have the same +signatures in 3.0.0 as in 2.0.0. + +## Codebase converted to PSR-12 + +The package was reformatted from the Joomla Coding Standard to PSR-12. This touches nearly every +line and changes no behaviour, so a `git diff` between 2.x and 3.x is almost entirely noise. Use +`git diff -w` when looking for real changes. + +## Dependency changes + +| Package | v2 (2.0.1) | v3 (3.0.0) | +|---|---|---| +| `php` | `^7.2.5 \| ^8.0` | `^8.1.0` | + +The optional packages are unchanged: `ext-openssl`, `ext-sodium`, `defuse/php-encryption` and +`paragonie/sodium_compat` all remain in `suggest`. Install `defuse/php-encryption` if you use +`Cipher\Crypto`, which is the recommended cipher. diff --git a/docs/v3-to-v4-update.md b/docs/v3-to-v4-update.md index f6469747..7a417b50 100644 --- a/docs/v3-to-v4-update.md +++ b/docs/v3-to-v4-update.md @@ -1,7 +1,33 @@ -## Updating from v3 to v4 +# Updating from v3 to v4 -The following changes were made to the Crypt package between v3 and v4. +Release 4.0.0 raises the PHP requirement. Nothing else changed in `src/`. -### Minimum supported PHP version raised +## At a glance -All Framework packages now require PHP 8.3 or newer. +| | v3 (3.0.2) | v4 (4.0.0) | +|---|---|---| +| PHP | `^8.1.0` | `^8.3.0` | +| Public API | — | unchanged | + +## Minimum supported PHP version raised + +All Framework packages now require **PHP 8.3** or newer. + +## No API changes + +`git diff 3.0.2 HEAD -- src/` is empty. Every class is byte-for-byte identical to 3.0.2, so +upgrading is a matter of satisfying the PHP requirement. + +## Dependency changes + +| Package | v3 (3.0.2) | v4 (4.0.0) | +|---|---|---| +| `php` | `^8.1.0` | `^8.3.0` | + +The optional packages in `suggest` are unchanged: `ext-openssl`, `ext-sodium`, +`defuse/php-encryption`, `paragonie/sodium_compat`. + +## Worth doing while you are here + +The upgrade itself is trivial, but if your code uses `Cipher\OpenSSL`, this is a good moment to +move to `Cipher\Crypto`. See [the overview](overview.md#the-openssl-cipher) for why.