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

Encrypted Fields Bundle Laravel Package

dwgebler/encrypted-fields-bundle

Symfony bundle that encrypts and decrypts Doctrine entity fields via a simple attribute, using a master key to generate secure per-row encryption keys stored in a separate table. Supports key rotation, UUID and composite primary keys.

View on GitHub
Deep Wiki
Context7

Getting Started

First, confront the framing. dwgebler/encrypted-fields-bundle is a Symfony bundle built around Doctrine ORM, not a Laravel/Eloquent package. There is no service provider, no Artisan command, no Eloquent cast, and no Flex recipe that applies to Laravel. If your app is Eloquent-only, stop here — the effort of adopting laravel-doctrine/orm is an architecture change, not an integration task. If you already run laravel-doctrine/orm (or are willing to), this package is a plausible fit.

Minimal steps for a Laravel + Doctrine setup:

  1. Confirm prerequisites. PHP openssl extension enabled; Doctrine ORM wired into Laravel via laravel-doctrine/orm. This determines whether the rest is viable at all.
  2. Install.
    composer require dwgebler/encrypted-fields-bundle
    
  3. Generate a master key (32 bytes hex for the default AES-256-GCM cipher):
    php -r "file_put_contents('master.key', bin2hex(random_bytes(32)));"
    
    Store the file (or its value) in a secrets manager — not committed, not a plain .env in production.
  4. Configure. The README uses Symfony YAML (config/packages/gebler_encrypted_fields.yaml). In Laravel you must translate this into a config/encrypted_fields.php and pass the values into the bundle's config when you register it. You will not get the Flex recipe — expect to hand-wire this.
  5. Wire the bundle into Doctrine's event system. The bundle works by attaching Doctrine lifecycle listeners/subscribers. In Laravel with laravel-doctrine/orm, that means registering the listener/subscriber on the entity manager's EventManager in a service provider. This is the step that most often trips people up; verify listeners are actually attached before assuming encryption is happening.
  6. Create the encryption_key table. The README's make:migration generates the schema against the bundle's mapping. In Laravel you will hand-write the migration — the table stores per-row encrypted data keys. Run it before persisting any encrypted entity.
  7. First use case. Pick one low-risk sensitive field on one entity. Add #[EncryptedField], persist a row, then inspect the raw column in the database to confirm ciphertext, and reload the entity to confirm the plaintext round-trips. Only then expand.

Implementation Patterns

Declarative field encryption. Annotate entity properties and let the persistence layer handle the rest:

use Gebler\EncryptedFieldsBundle\Attribute\EncryptedField;

class Customer
{
    #[EncryptedField]
    private string $email;

    #[EncryptedField(elements: ['ssn', 'dob'])]
    private array $profile;

    #[EncryptedField(useMasterKey: true)]
    private string $internalToken;

    #[EncryptedField(key: 'tenant_a_pii')]
    private string $address;
}

You then use the entity through normal Doctrine repositories — no encryption calls in your code. Encryption fires on prePersist/preUpdate, decryption on hydration.

Event registration in Laravel. In a service provider, resolve the Doctrine EntityManager and attach the bundle's listener/subscriber to its EventManager. Keep the listener classes resolved through the container so the master key and cipher config are injected correctly.

Searchable data pattern (blind index). Because GCM ciphertext is non-deterministic, you cannot WHERE, ORDER BY, or LIKE an encrypted column. For anything you need to filter on, store a separate deterministic hash column (e.g. hash('sha256', $normalisedValue) with a per-tenant salt) alongside the ciphertext, and query that. Plan this into your migrations before you encrypt the column, because retrofitting it means decrypting the whole table.

Key rotation. The README documents gebler:encryption:rotate-key with --generate-new-key, --database-key, and --database-key-file. This is a Symfony console command; under Laravel you will invoke it via the Symfony bin/console kernel or port the logic into an Artisan command. Treat rotation as a scheduled, audited operation with a backup taken immediately beforehand. Version 2.0 fixed rotation for useMasterKey and custom key: fields — do not attempt rotation on 1.x.

Multi-tenant scoping. Use the key: option to isolate per-tenant keys where a tenant should not share a wrapping key with others, and ensure the encryption_key table rows are access-controlled per tenant if you do.

Testing. Pin a known test master key in phpunit.xml/test config, and include at least one assertion that reads the raw DB column and asserts it is not the plaintext. Round-trip tests alone will not catch a listener that silently failed to attach.

Gotchas and Tips

  • This is not a Laravel package. Expect zero Flex automation under Laravel. Budget a day for wiring config, listeners, and the migration, and validate that the listener truly fires before touching production data.
  • Encrypted columns become unqueryable. No WHERE, ORDER BY, LIKE, GROUP BY, or unique constraints on encrypted columns. A TPM/architect should scope which fields can realistically be encrypted, because reworking every filter and report that touches those fields is often 5–10x the effort of the encryption work.
  • Silent data loss on 1.x. The preUpdate bug in 1.x caused silent data loss. Upgrade to 2.0 and read UPGRADE-2.0.md before doing anything else.
  • Coverage gaps are the real security risk. Doctrine listeners fire on entity-level flush. Bulk DQL UPDATE/DELETE, raw SQL, DBAL-level writes, and some partial-hydration paths bypass the listener and silently write plaintext into a column you believe is encrypted. Audit your write paths; for bulk operations, either avoid them on encrypted tables or encrypt explicitly before executing.
  • Column widths and types. Ciphertext (IV + GCM tag + base64) is materially larger than plaintext. Widen VARCHAR columns and migrate typed columns (int, bool, date) to TEXT/BLOB — the ciphertext is not type-preserving. Do this migration before enabling encryption on existing data.
  • Existing plaintext data. The README describes no backfill command. You need your own one-off job that reads plaintext, loads each row through the now-configured entity, and flushes so the listener encrypts it. Do not build a "fall back to plaintext on decrypt failure" read path — it quietly defeats the control.
  • Master key hygiene. A single master key compromise exposes every per-row key, so it is the entire security boundary. Store it in AWS Secrets Manager / Vault, restrict encryption_key table access, and log rotation events. There is no HSM or FIPS 140-2 validation here.
  • Proxy objects. Doctrine proxy fields were missed by the listener prior to 1.2.1; ensure you are on a version where this is fixed (2.0 is fine).
  • UUID and composite primary keys are only supported in 2.0. If your entities use them, 1.x will not work.
  • Debugging checklist. When a field "isn't encrypting": (1) query the raw column directly — do not trust the app layer; (2) confirm the listener is attached to the correct EntityManager; (3) confirm the attribute is on the real class, not a mapped superclass the listener doesn't resolve; (4) confirm the encryption_key table exists and the row was inserted; (5) check the cipher length matches what you configured.
  • Change-detection is a feature, not a bug. 2.0 skips re-encryption of unchanged values, so a preUpdate flush with no field change will not produce new ciphertext — if you were relying on that side effect, update your expectations.
  • Cipher agility. cipher is configurable; if you move off aes-256-gcm, you re-validate the key length and lose the authenticated-encryption property unless your replacement provides it. Keep GCM unless you have a strong reason.
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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator