PHPackages                             phpnomad/encryption - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [PSR &amp; Standards](/categories/psr-standards)
4. /
5. phpnomad/encryption

ActiveLibrary[PSR &amp; Standards](/categories/psr-standards)

phpnomad/encryption
===================

encryption contracts + field-level encryption for PHPNomad (bring your own cipher via an integration)

v1.0.1(yesterday)012↑2650%1MITPHP &gt;=8.2

Since Jul 19Compare

[ Source](https://github.com/phpnomad/encryption)[ Packagist](https://packagist.org/packages/phpnomad/encryption)[ Docs](https://github.com/phpnomad/encryption)[ RSS](/packages/phpnomad-encryption/feed)WikiDiscussions Synced today

READMEChangelogDependencies (1)Versions (3)Used By (1)

phpnomad/encryption
===================

[](#phpnomadencryption)

Encryption **contracts** for PHPNomad — bring your own cipher via an integration package. This package holds only the strategy and key-provider interfaces, the immutable encrypted-value model, the key providers, and the exception types. It makes **no assumption about how you store or transport an encrypted value** — no serialization format, no column shape. It has no cipher dependency (no `ext-sodium`, no framework, no ORM) — just PHP.

The default cipher lives in a separate integration: **[`phpnomad/sodium-integration`](https://github.com/phpnomad/sodium-integration)**(libsodium XChaCha20-Poly1305 AEAD).

- **Contract-first.** `EncryptionStrategy` and `KeyProvider` are the seams; swap ciphers or key sources without touching call sites.
- **Context binding.** The contract requires ciphertext to be bound to a caller context (associated data), so a value copied elsewhere won't decrypt.
- **Key rotation built in.** Keys are versioned; encrypt with the current version, decrypt against whatever version sealed the data.
- **Storage-agnostic.** `EncryptedValue` is pure data — ciphertext, nonce, key version, and an opaque cipher tag. How you persist it is entirely yours.

Requirements
------------

[](#requirements)

- PHP &gt;= 8.2
- A cipher implementation — e.g. `phpnomad/sodium-integration`.

Install
-------

[](#install)

```
composer require phpnomad/encryption phpnomad/sodium-integration
```

`phpnomad/encryption` gives you the contracts; `phpnomad/sodium-integration`gives you the `SodiumEncryptionStrategy` to wire in.

Quickstart
----------

[](#quickstart)

```
use PHPNomad\Encryption\Providers\ArrayKeyProvider;
use PHPNomad\Encryption\Models\EncryptedValue;
use PHPNomad\Sodium\EncryptionIntegration\Strategies\SodiumEncryptionStrategy;

// A key ring holding one 32-byte key at version 1.
$keys = new ArrayKeyProvider([1 => sodium_crypto_aead_xchacha20poly1305_ietf_keygen()]);

// The cipher comes from the integration package; everything else is this one.
$encryption = new SodiumEncryptionStrategy($keys);

$sealed = $encryption->encrypt('sk-live-super-secret');

// Persist it however your storage layer prefers — the value is pure data, so the
// shape is yours. For example, spread across columns (base64 for text columns):
$row = [
    'ciphertext'  => base64_encode($sealed->getCiphertext()),
    'nonce'       => base64_encode($sealed->getNonce()),
    'key_version' => $sealed->getKeyVersion(),
    'cipher'      => $sealed->getCipher(),
];

// Later — rebuild the value from your stored fields and decrypt:
$restored = new EncryptedValue(
    base64_decode($row['ciphertext']),
    base64_decode($row['nonce']),
    (int) $row['key_version'],
    $row['cipher'],
);

$plaintext = $encryption->decrypt($restored);
// => "sk-live-super-secret"
```

Generate a base64 key for your environment:

```
php -r 'echo base64_encode(sodium_crypto_aead_xchacha20poly1305_ietf_keygen()), "\n";'
```

Where keys come from — an env var, a file, a KMS — is your application's concern: implement `Interfaces\KeyProvider` (two methods) however you load them. `ArrayKeyProvider` is the in-memory primitive when you already hold the raw keys.

Associated data (AEAD context)
------------------------------

[](#associated-data-aead-context)

The second argument to `encrypt()`/`decrypt()` is **associated data**: authenticated but not encrypted. Use it to bind a ciphertext to where it lives. The *same* context must be supplied to decrypt.

```
$sealed = $encryption->encrypt($token, "tenant:42:column:access_token");

$encryption->decrypt($sealed, "tenant:42:column:access_token"); // ok
$encryption->decrypt($sealed, "tenant:99:column:access_token"); // throws DecryptionFailedException
```

This turns an encrypted-value swap between rows or columns from a silent success into a hard failure. If you encrypt several fields on one record, give each its own context (e.g. `"record:{id}:{field}"`) so ciphertexts can't be swapped between columns.

Key rotation
------------

[](#key-rotation)

Keys are addressed by version. Keep every version still referenced by stored ciphertext; point the ring's current version at the newest key.

```
// v1 was current when old values were sealed. Now add v2 and make it current.
$keys = new ArrayKeyProvider([
    1 => $oldKey,   // retained so old ciphertext still decrypts
    2 => $newKey,
], currentVersion: 2);

$encryption = new SodiumEncryptionStrategy($keys);

$encryption->encrypt('x');        // sealed under v2
$encryption->decrypt($oldValue);  // still decrypts against v1
```

To fully migrate, decrypt each stored value and re-encrypt it (the new `EncryptedValue` carries `keyVersion = 2`), then retire the old key once nothing references it. Multiple keys can also live behind separate providers via `KeyRing` (one `KeyProvider` per version).

Writing a cipher integration
----------------------------

[](#writing-a-cipher-integration)

Implement `Interfaces\EncryptionStrategy` and return an `EncryptedValue`, stamping your own opaque cipher discriminator so decrypt-time can recognize it:

```
use PHPNomad\Encryption\Interfaces\EncryptionStrategy;
use PHPNomad\Encryption\Models\EncryptedValue;

final class MyCipherStrategy implements EncryptionStrategy
{
    public const CIPHER = 'my-cipher-v1';

    public function encrypt(string $plaintext, string $context = ''): EncryptedValue { /* ...return new EncryptedValue($ct, $nonce, $version, self::CIPHER) */ }
    public function decrypt(EncryptedValue $value, string $context = ''): string { /* ... */ }
}
```

The contract requires that decryption fail (throw `DecryptionFailedException`) on a wrong key, a mismatched `$context`, or tampered bytes, and that it decrypt against the key version recorded on the value. The `cipher` discriminator is owned by the strategy, not this package — the contract names no ciphers. See `phpnomad/sodium-integration` for the reference implementation.

API at a glance
---------------

[](#api-at-a-glance)

TypeRole`Interfaces\EncryptionStrategy``encrypt(string, context): EncryptedValue` / `decrypt(EncryptedValue, context): string``Interfaces\KeyProvider``getKey(version): string` / `currentVersion(): int``Models\EncryptedValue`immutable data: ciphertext + nonce + keyVersion + opaque cipher tag; getters only`Providers\ArrayKeyProvider`in-memory versioned key ring`Providers\KeyRing`compose per-version providers`Exceptions\*``EncryptionException`, `DecryptionFailedException`, `KeyNotFoundException`*cipher strategy*provided by an integration, e.g. `phpnomad/sodium-integration`Testing
-------

[](#testing)

```
composer install
composer test
```

The contract suite has no cipher dependency — it exercises the strategy contract against a small in-package reversible fake. The real libsodium cipher is tested in `phpnomad/sodium-integration`.

License
-------

[](#license)

MIT © Novatorius / Alex Standiford

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance100

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community5

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/9e6206223bd6f2a57b8ac80605b1b5c3521faaec18ad3f20f25fb728a9a13784?d=identicon)[tstandiford](/maintainers/tstandiford)

---

Tags

contractscryptographyencryptionkey-rotationfield encryptionphpnomad

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/phpnomad-encryption/health.svg)

```
[![Health](https://phpackages.com/badges/phpnomad-encryption/health.svg)](https://phpackages.com/packages/phpnomad-encryption)
```

###  Alternatives

[symfony/translation-contracts

Generic abstractions related to translation

2.6k747.7M687](/packages/symfony-translation-contracts)[symfony/cache-contracts

Generic abstractions related to caching

2.4k332.6M320](/packages/symfony-cache-contracts)[symfony/http-client-contracts

Generic abstractions related to HTTP clients

2.0k428.1M439](/packages/symfony-http-client-contracts)[symfony/contracts

A set of abstractions extracted out of the Symfony components

3.9k65.9M138](/packages/symfony-contracts)[facade/ignition-contracts

Solution contracts for Ignition

636149.2M164](/packages/facade-ignition-contracts)[dragon-code/contracts

A set of contracts for any project

1010.2M39](/packages/dragon-code-contracts)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
