Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Encryption Bundle Laravel Package

wemxo/encryption-bundle

wemxo/encryption-bundle provides simple, secure application-level encryption for PHP projects, helping protect sensitive data at rest with configurable keys and a clean service-based API for encrypting and decrypting values.

View on GitHub
Deep Wiki
Context7

Getting Started

This release (v1.1.0) adds deterministic encryption as an option alongside the existing randomised encryption, so encrypted columns can now be matched with plain WHERE email = ? equality queries without a separate blind index. That is the headline change; everything below assumes the package is your encryption layer for Eloquent attributes.

Minimal path from zero to working:

  1. Install and publish config

    composer require <vendor>/laravel-ciphersweet
    php artisan vendor:publish --tag="ciphersweet-config"
    
  2. Generate keys before writing any data. Encryption and blind-index keys are separate, must be 32 bytes, and are usually base64-encoded in .env:

    php artisan ciphersweet:generate-key
    
    CIPHERSWEET_KEY=base64:...
    CIPHERSWEET_BLIND_INDEX_KEY=base64:...
    

    Committing a key or reusing one across environments is the single most common mistake — treat these like APP_KEY but with a proper rotation plan (see Gotchas).

  3. Cast an attribute on a model

    use Vendor\CipherSweet\Casts\Encrypted;
    
    protected $casts = [
        'ssn' => Encrypted::class,
    ];
    

    Read/write code stays unchanged: $user->ssn = '123-45-6789'; $user->save(); The column stores ciphertext; the cast handles encrypt on set and decrypt on get.

  4. First real use case — searchable PII. Pick a field you actually query on (email, national ID, external reference). If you only need equality lookups, prefer a blind index (randomised ciphertext + HMAC index column). Reach for the new deterministic mode when you want to avoid the extra column and accept that equal plaintexts produce equal ciphertexts.

Look first at: the published config (providers, keys, default driver), the cast/field class list (to see whether deterministic is a separate cast, a cast parameter, or a model-level flag in this release), and the package's migration/backfill helpers if present.

Implementation Patterns

Randomised ciphertext + blind index (default, safest). Keep the column non-deterministic and add a companion index column populated on save. Queries go through the index column, never the ciphertext:

protected $casts = [
    'email' => Encrypted::class,
    'email_index' => BlindIndex::class, // or a HasBlindIndexes trait on the model
];

User::where('email_index', User::blindIndex('email', $email))->first();

This keeps full semantic security on the stored value while still supporting exact-match lookups and unique constraints.

Deterministic mode (new in v1.1.0). Enable it per attribute so the same plaintext under the same key always yields the same ciphertext:

protected $casts = [
    'external_ref' => Encrypted::class.':deterministic',
];
// now a plain equality lookup works:
User::where('external_ref', $ref)->first();

Use it only where the leak is acceptable: opaque IDs, tenant-scoped reference numbers, de-duplication keys, join keys between two tables. Do not use it for free-text, dates of birth, names, or anything with low entropy that an attacker could enumerate — determinism turns the ciphertext into an oracle for "does this value exist in the table".

Choosing between the two

Need Use
Encrypt and never search Randomised cast
Exact-match lookup, strong secrecy Randomised + blind index
Exact-match lookup, want one column / no index column Deterministic (accepting equality leak)
LIKE, prefix, range, sort Neither — store a derived, non-sensitive search column

Backfilling existing rows. Changing a column from randomised to deterministic (a one-way change — the ciphertexts differ) requires decrypt-then-re-encrypt. Do it in a chunked job with the old and new configs both available, and never in a single UPDATE on a large table:

User::query()->chunkById(500, function ($users) {
    foreach ($users as $user) {
        $plain = $user->getRawOriginal('external_ref'); // still ciphertext here
        $user->external_ref = decrypt_old($plain);
        $user->saveQuietly(); // avoid observers that touch other encrypted fields
    }
});

Run the backfill in a transaction-free, idempotent command so a failure mid-run is resumable, and keep the old key in the provider config until the job is verified complete.

Multi-tenant / per-tenant keys. If a provider is resolved per tenant, deterministic ciphertexts are only comparable within that tenant — the same email under two tenant keys produces two different digests. Design unique constraints and cross-tenant admin search around that, and make sure queue jobs and console commands boot the correct tenant provider before touching encrypted attributes.

Testing. Encrypted attributes are transparent to factories and assertions, but assertDatabaseHas on the raw column will fail. Assert via the model, or compute the expected ciphertext/blind index through the package so the test isn't coupled to the KDF output.

Gotchas and Tips

  • Deterministic encryption is not "encryption with search". It preserves equality and nothing else. No ordering, no LIKE, no range queries, no ORDER BY on the raw column — sorting by ciphertext sorts by random bytes.
  • where() on the encrypted column is the only thing that works. where('email', 'like', '%foo%'), whereBetween, and orderBy all silently compare ciphertexts and return wrong-but-plausible results. Code review should treat any raw query touching an encrypted column as suspect; the cast does not apply to query builder calls.
  • Never mix modes on one column. Rows written randomised will not match rows written deterministic. Pick a mode, backfill, and add a test that asserts the mode by comparing two writes of the same plaintext.
  • Key length and encoding. Keys must be exactly 32 bytes after decoding; a base64 key missing the base64: prefix, or a key generated with the wrong driver, fails at first write rather than at boot. Validate keys in a health-check command that runs in CI.
  • Blind index length. Index columns need enough room for the digest and a unique index sized for your DB. Truncating an index to save space increases collision risk and can produce false-positive matches in first() lookups.
  • Decryption failures are loud but vague. A DecryptException on a single row usually means a rotated key, a row written under a different tenant provider, or a value that was written before encryption was enabled. Log the model key and column, not the ciphertext, then re-run with DB::listen to find the offending write.
  • Serialization and logging. Queue payloads, Model::toArray(), and Log::info($user) all go through the cast and will expose plaintext. Add the encrypted attributes to $hidden and audit any ->toJson() in API resources.
  • update() with an array bypasses nothing but saveQuietly bypasses events — if your blind index or deterministic re-index lives in a model observer, mass updates and saveQuietly will leave stale indexes. Prefer computing indexes inside the cast/acessor so they cannot be skipped.
  • Extension points: custom field/row classes let you add AAD (e.g. tenant ID or model ID) so a ciphertext copied between rows fails to decrypt. This is the cheapest defence against row-swapping attacks and is worth wiring up before you have data to migrate.
  • Version note: v1.1.0 is additive — no breaking changes reported — so existing randomised casts keep working. The only migration cost is opting individual columns into deterministic mode and backfilling them.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource