PHPackages                             sneakyx/laravel-dynamic-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. [Security](/categories/security)
4. /
5. sneakyx/laravel-dynamic-encryption

ActiveLibrary[Security](/categories/security)

sneakyx/laravel-dynamic-encryption
==================================

Laravel package that replaces the default encrypter with a dynamic key loaded from cache or database.

0.6.0(2mo ago)1467↓100%MITPHPPHP &gt;=8.1

Since Nov 14Pushed 2mo agoCompare

[ Source](https://github.com/sneakyx/laravel-dynamic-encryption)[ Packagist](https://packagist.org/packages/sneakyx/laravel-dynamic-encryption)[ Docs](https://github.com/sneakyx/laravel-dynamic-encryption)[ RSS](/packages/sneakyx-laravel-dynamic-encryption/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (10)Dependencies (5)Versions (12)Used By (0)

sneakyx/laravel-dynamic-encryption
==================================

[](#sneakyxlaravel-dynamic-encryption)

A minimalist Laravel package that replaces the default `Encrypter` with a dynamic key system. The application encryption key is not taken from `APP_KEY`, but resolved at runtime from a volatile store (cache) and/or derived from a password using a KDF. This enables ephemeral key management and straightforward rotation. Read more in: [Why does this exist?](docs/why-does-this-exist.md)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

[](#a-minimalist-laravel-package-that-replaces-the-default-encrypter-with-a-dynamic-key-systemthe-application-encryption-key-is-not-taken-from-app_key-but-resolved-at-runtime-from-a-volatile-store-cache-andor-derived-from-a-password-using-a-kdf-this-enables-ephemeral-key-management-and-straightforward-rotationread-more-in-why-does-this-exist)

Installation
------------

[](#installation)

### Local Development (Monorepo)

[](#local-development-monorepo)

1. The package is already located at: `packages/only-local/laravel-dynamic-encryption`
2. Update autoloading: ```
    composer dump-autoload
    ```

### Standalone Project (Packagist/GitHub)

[](#standalone-project-packagistgithub)

```
composer require sneakyx/laravel-dynamic-encryption
```

The package uses Laravel’s auto-discovery and binds the service provider automatically.

Configuration
-------------

[](#configuration)

Add environment variables and publish/adjust the config if needed.

Relevant `.env` variables (examples):

```
# Which cache store (by name) is expected to carry the key bundle (validated);
# the bundle is read via your DEFAULT cache store. Ensure they match in practice.
DYNAMIC_ENCRYPTION_CACHE_STORE=memcached

# Cache key that holds the bundle (an array) with the field "password"
DYNAMIC_ENCRYPTION_CACHE_KEY=dynamic_encryption_key
DYNAMIC_ENCRYPTION_ARRAY_KEY=password

# KDF settings (password -> key). Salt/params come from .env
DYNAMIC_ENCRYPTION_KDF=pbkdf2    # or: argon2id
DYNAMIC_ENCRYPTION_KDF_ALGO=sha256
DYNAMIC_ENCRYPTION_KDF_ITERS=210000

# Salt can be raw text or base64:...
DYNAMIC_ENCRYPTION_SALT=base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

# Behavior when the dynamic key bundle is missing
#   block     -> prevent saving changed encryptable fields (default)
#   plaintext -> skip encryption for changed fields (store as plaintext)
#   fail      -> throw and fail boot
DYNAMIC_ENCRYPTION_ON_MISSING_BUNDLE=block

```

See `config/dynamic-encryption.php` for all options.

Key size is taken from `config('app.cipher')`:

- aes-256-cbc / aes-256-gcm → 32 bytes
- aes-128-cbc / aes-128-gcm → 16 bytes

Key material: two supported formats
-----------------------------------

[](#key-material-two-supported-formats)

- Base64-derived key: `base64:` that decodes to the required key length.
- Plain password: any string; the package derives the correct-length key using the configured KDF and salt from `.env`.

Behavior when the key bundle is missing
---------------------------------------

[](#behavior-when-the-key-bundle-is-missing)

- The package keeps the framework operational (sessions/cookies) by providing a core encrypter from `APP_KEY`.
- A runtime flag is set (`dynamic-encryption.missing_bundle` by default). Application code should NOT try decrypting data with a fallback key. Instead, use the cast below to surface a "locked" value in the UI until the user unlocks their data (Variante A).
- Policies when saving while the bundle is missing:
    - `block` (default): Saving models with changed encryptable fields throws a validation error. Unchanged fields are untouched. Reading remains tolerant.
    - `plaintext`: Encryption is skipped for changed encryptable fields (they are stored as plaintext). Use only in exceptional cases.
    - `fail`: Provider throws during boot; the application will error.

Usage
-----

[](#usage)

The package replaces Laravel’s default Encrypter. Use the Crypt facade as usual:

```
use Illuminate\Support\Facades\Crypt;

$payload = Crypt::encryptString('secret');
$plain   = Crypt::decryptString($payload);
```

### Usage

[](#usage-1)

Use the `EncryptedNullableCast` or `EncryptedNullableJsonCast` in your model's `$casts` array:

```
use Sneakyx\LaravelDynamicEncryption\Casts\EncryptedNullableCast;
use Sneakyx\LaravelDynamicEncryption\Casts\EncryptedNullableJsonCast;

class User extends Model
{
    protected function casts(): array
    {
        return [
            'secret_field' => EncryptedNullableCast::class,
            'secret_array' => EncryptedNullableJsonCast::class,
        ];
    }
}
```

Migration from v0.3.x
---------------------

[](#migration-from-v03x)

Replace the `DynamicEncryptable` trait and `$encryptable` property with casts:

```
// OLD (deprecated)
use Sneakyx\LaravelDynamicEncryption\Traits\DynamicEncryptable;
class User extends Model {
    use DynamicEncryptable;
    protected array $encryptable = ['secret_field'];
}

// NEW
use Sneakyx\LaravelDynamicEncryption\Casts\EncryptedNullableCast;
class User extends Model {
    protected function casts(): array {
        return ['secret_field' => EncryptedNullableCast::class];
    }
}
```

Please migrate to the Cast-based approach above. The trait will log deprecation warnings.

### Cast-based "Locked" Flow

[](#cast-based-locked-flow)

Instead of using a cryptographic fallback (e.g., `APP_KEY`), the following cast returns a `LockedEncryptedValue` placeholder object when the key is missing—no exception is thrown. This allows the UI to display an "unlock" dialog without breaking the rendering flow. If you have also (legacy) unencrypted fields for some entities, you also can use the `.env` value:

```
DYNAMIC_ENCRYPTION_ON_DECRYPTION_ERROR_RETURN="raw"
```

(There are also the values `null` and `fail` for the policy.)

1. Use Cast:

```
use Illuminate\Database\Eloquent\Model;
use Sneakyx\LaravelDynamicEncryption\Casts\EncryptedNullableCast;

class UserSecret extends Model
{
    protected $casts = [
        'iban' => EncryptedNullableCast::class,
    ];
}
```

2. Check in the UI:

```
use Sneakyx\LaravelDynamicEncryption\Values\LockedEncryptedValue;

if ($userSecret->iban instanceof LockedEncryptedValue) {
    // Show banner/modal: "Please enter password to view"
} else {
    echo $userSecret->iban; // Decrypted plaintext
}
```

3. When saving:

- If the bundle key is available, encryption proceeds normally.
- If the key is missing, the policy dynamic-encryption.on\_missing\_bundle applies:
    - block (default): Throws a ValidationException.
    - plaintext: Stores the value in plaintext (use only temporarily or in exceptional cases!).

Note: `LockedEncryptedValue` is string-/JSON-serializable (returns an empty string or null) and contains no plaintext data.

### Where is the key/password stored?

[](#where-is-the-keypassword-stored)

The package expects a key bundle (array) in your cache under `DYNAMIC_ENCRYPTION_CACHE_KEY`. Read more: [Where is the key?](docs/where-is-the-key.md)

Data Prefixing (Versioned Encryption)
-------------------------------------

[](#data-prefixing-versioned-encryption)

Starting with version 0.2.0, encrypted values are stored with a versioned prefix (default: `dynenc:v1:`). This allows the system to reliably distinguish between:

1. **Encrypted ciphertext:** Values starting with the prefix.
2. **Legacy plaintext:** Values without the prefix (e.g., from before the encryption was enabled).

### Why use a prefix?

[](#why-use-a-prefix)

Without a prefix, it's difficult to know if a string in the database is already encrypted or if it's still plaintext. Attempting to decrypt plaintext usually results in a decryption error. With the prefix, the `EncryptedNullableCast` can safely return the raw value if no prefix is found, preventing errors during migration or partial rollouts.

### Migration Command

[](#migration-command)

If you have data that was encrypted with an older version of this package (without a prefix) or using different encryption parameters, you can use the `migrate-legacy` command to re-encrypt your data with the current settings:

```
php artisan dynamic-encrypter:migrate-legacy --all --dry-run
php artisan dynamic-encrypter:migrate-legacy --all
```

The command decrypts legacy values (without prefix, encrypted with an old key/salt) and re-encrypts them using the current encrypter and adds the versioned prefix.

By default, it automatically attempts multiple decryption strategies:

- Current KDF password + current salt
- Current KDF password + empty salt (for environments that previously had no `DYNAMIC_ENCRYPTION_SALT`)
- Laravel `APP_KEY`
- Old password from cache (if available)

Options:

- `--model=FQCN`: Process specific models (can be repeated).
- `--all`: Process all models using encryption.
- `--from="2025-01-01 00:00:00"`: Only process records updated after this date.
- `--to="2025-12-31 23:59:59"`: Only process records updated before this date.
- `--old-password=PASSWORD`: Explicitly provide the legacy password.
- `--old-salt=SALT`: Explicitly provide the legacy salt.
- `--old-kdf-iters=ITERS`: Explicitly provide legacy KDF iterations.
- `--skip-app-key`: Do not attempt decryption with the Laravel `APP_KEY`.
- `--skip-no-salt`: Do not attempt decryption with an empty salt.
- `--dry-run`: Show what would be updated without changing the database.
- `--debug-first`: Useful if records cannot be decrypted. It stops at the first failed record and prints detailed diagnostic information (payload structure, tried encrypters, and error messages).

The command only processes values that:

1. Do not already have the current prefix.
2. Appear to be encrypted (e.g., valid JSON structure with `iv`, `value`, and `mac`).

But be careful, there is a theoretical possibility that unencrypted Data is misinterpreted.

### Decrypt encrypted fields and store plaintext

[](#decrypt-encrypted-fields-and-store-plaintext)

Use this if you need to permanently decrypt encrypted values in your database for specific models/fields:

```
php artisan dynamic-encrypter:decrypt --model=App\\Models\\Secret --field=iban --field=note
# or everything
php artisan dynamic-encrypter:decrypt --all
```

- Decrypts values that are recognized as encrypted (with the configured prefix) and writes back the plaintext.
- Supports `--model` (repeatable), `--field` (repeatable), `--all`, and `--dry-run`.

### Encrypt any still-plaintext values in bulk

[](#encrypt-any-still-plaintext-values-in-bulk)

Use this when you have legacy plaintext in encryptable fields and want to encrypt them in one go:

```
php artisan dynamic-encrypter:encrypt --model=App\\Models\\Secret --field=iban
# or everything
php artisan dynamic-encrypter:encrypt --all
```

- Encrypts values that do not start with the configured prefix and are not already legacy-structured ciphertexts.
- Writes back with the current prefix and current dynamic encrypter.
- Supports `--model` (repeatable), `--field` (repeatable), `--all`, and `--dry-run`.

Key Rotation
------------

[](#key-rotation)

Re-encrypt existing data from an old password to a new one (interactive):

```
php artisan dynamic-encrypter:rotate --model=App\\Models\\Secret
```

Options:

- `--model=FQCN` Can be repeated.
- `--all` Rotate for all models using encryption (be careful on large datasets).
- `--dry-run` Do not write changes.

How it works (since v0.5.1):

- The command reads the current password from the configured cache bundle (field `password`). If none is present, it aborts with a clear error (server must be unlocked first).
- It prompts for the old password (default = cached password; press Enter to use it) and for the new password (secret input).
- Before writing, it shows a short summary (affected models, dry-run status) and asks for explicit confirmation.
- Then it decrypts each field with the old encrypter and re-encrypts with the new one, in chunks (`dynamic-encryption.chunk`).
- In dry-run mode, no writes are performed.
- After success, if the entered new password differs from the cached password, the cache is updated and a reminder is printed to also update the external source used for unlocking.

Testing
-------

[](#testing)

If you are writing Feature tests for your application, you need to ensure that a valid encryption key is present in the cache. Otherwise, models using `DynamicEncryptable` might throw exceptions or fail to save.

This package ships with a helper trait `DynamicEncryptionTestLoader`. Use it in your base `TestCase` to automatically inject a temporary key into the cache for the duration of the tests.

```
namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Sneakyx\LaravelDynamicEncryption\Testing\DynamicEncryptionTestLoader;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;
    use DynamicEncryptionTestLoader;

    protected function setUp(): void
    {
        parent::setUp();

        // Initializes a random key and writes it to the configured cache store
        $this->initializeDynamicEncryptionKeyForTests();
    }
}
```

Security notes
--------------

[](#security-notes)

- No key/password is written to logs.
- KDF parameters and salt live in `.env`/secrets; the password/key material lives in cache (bundle).
- Use a non-persistent cache store shared by your PHP workers (memcached/redis). Avoid the `array` driver.

Compatibility
-------------

[](#compatibility)

Tested with Laravel 10.

License
-------

[](#license)

MIT

###  Health Score

40

—

FairBetter than 88% of packages

Maintenance86

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

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 ~11 days

Recently: every ~3 days

Total

11

Last Release

67d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/ac68a0bac058fca08e856ad2182540a32cd2536b2347ebaece583d86a4640854?d=identicon)[sneaky](/maintainers/sneaky)

---

Top Contributors

[![sneakyx](https://avatars.githubusercontent.com/u/20614860?v=4)](https://github.com/sneakyx "sneakyx (2 commits)")

---

Tags

laravelsecurityencryptionpbkdf2Argon2idkdf

### Embed Badge

![Health badge](/badges/sneakyx-laravel-dynamic-encryption/health.svg)

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

###  Alternatives

[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9682.1M97](/packages/roots-acorn)[tzsk/otp

A secure, database-free One-Time Password (OTP) generator and verifier for PHP and Laravel.

241641.4k1](/packages/tzsk-otp)[dgtlss/warden

A Laravel package that proactively monitors your dependencies for security vulnerabilities by running automated composer audits and sending notifications via webhooks and email

8745.6k](/packages/dgtlss-warden)[ercsctt/laravel-file-encryption

Secure file encryption and decryption for Laravel applications

642.6k](/packages/ercsctt-laravel-file-encryption)

PHPackages © 2026

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