Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -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)
175 changes: 173 additions & 2 deletions docs/overview.md
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 34 additions & 4 deletions docs/v2-to-v3-update.md
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 30 additions & 4 deletions docs/v3-to-v4-update.md
Original file line number Diff line number Diff line change
@@ -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.
Loading