sidus/encryption-bundle
Sidus Encryption Bundle is a Symfony/Doctrine integration for transparent field-level encryption, automatically encrypting and decrypting sensitive entity fields so data is protected at rest while application code keeps using normal entity properties.
First, confirm the architectural premise: this is a Symfony Bundle built on Doctrine ORM lifecycle events, not a Laravel package. There is no Eloquent analogue for postLoad/prePersist interception. As a Laravel developer you have two paths:
require from Packagist unguarded — see Gotchas), register it, and use it as designed.sodium_crypto_secretbox and a KMS master key.If you do proceed on Symfony/Doctrine:
path repository in composer.json — the repo is dormant (2 stars, 0 dependents), so treat upstream as frozen.SidusEncryptionBundle in config/bundles.php and configure the encryptor + key provider under config/packages/sidus_encryption.yaml.EncryptionKey entity table. Note the default key storage is the same database as the ciphertext — weak against the most common threat (DB dump). Plan to override the key provider with KMS/Vault before rollout.First use case: a single low-risk PII field (national ID, DOB, free-text health note) on one entity in a non-production environment. Measure hydration overhead before expanding. Avoid starting with email, username, or anything you filter/sort/LIKE on — encrypted columns are non-deterministic and unqueryable.
For the Laravel-native equivalent, mirror the seam: EncryptedCast for columns, a Flysystem decorator for files, per-record data keys wrapped by a KMS master key cached per request.
Field-level via Doctrine custom type. Mark the property with the bundle's encrypted attribute and the custom type. Writes encrypt in prePersist/preUpdate; reads decrypt on hydration. Code keeps reading plaintext; the DB holds ciphertext. This is the seam to replicate — in Eloquent, it's a CastsAttributes implementation.
Encrypted files alongside entities. The bundle stores an encrypted file path on the entity and encrypts the blob on write. The interesting design element is the hybrid RSA+AES approach for file payloads — asymmetric wrap of a symmetric key. Do not encrypt large files with RSA directly; it is prohibitively slow per row/object. If you need this pattern in Laravel, encrypt on the storage-driver boundary (a Flysystem decorator) so S3/local behave identically and streaming is preserved.
Pluggable encryptors and key providers via Symfony DI. This is the extension point you would actually use — swap the default file-based key provider for a KMS/Vault-backed service so key material lives outside the database. In Laravel, the equivalent is a KeyProvider contract bound in a service provider.
Key rotation via multiple key versions. The bundle supports versioned keys; rotation is O(rows) and requires a resumable, idempotent background re-encryption job. Never attempt a synchronous table rewrite. In Laravel, model it as encryption_key_id on the row + a queued job that re-wraps/re-encrypts per chunk.
Testing against the seam. Because encryption is transparent at the persistence layer, unit tests using in-memory repositories won't exercise it. Write integration tests against a real DB and verify the raw column is ciphertext before asserting the hydrated value is plaintext. For Laravel, add a test asserting the DB driver sees ciphertext (not the cast's plaintext).
Searchability via blind index. Where you genuinely need WHERE/ORDER BY on a sensitive field, add a separate column holding an HMAC of the normalized value (a deterministic blind index) — never attempt to query ciphertext. Document this constraint with product before promising filters.
EncryptionKey persistence puts key material next to ciphertext. Against a backup/dump adversary this is weak protection. Override the key provider with KMS/Vault before any production use.toArray()/JSON responses, in logs, in queue payloads, and in cache. Redact explicitly where serialization, logging, or job dispatch occurs — the bundle gives you nothing here.VARCHAR(255) on an encrypted field will silently truncate.EncryptionKey association will be fetched per row on list endpoints unless you cache per request. Profile list pages specifically.postLoad and can fail an entire query — including unrelated rows. Consider a nullable/tryDecrypt variant in your own fork that returns a sentinel and alerts rather than 500ing a page.How can I help you explore Laravel packages today?