kielabokkie/laravel-conceal
Laravel Conceal hides sensitive model IDs in your URLs by encrypting route keys, so links reveal opaque strings instead of sequential database IDs. Add the trait, and route model binding keeps working while exposing less.
kielabokkie/laravel-conceal gives Eloquent models transparent attribute encryption: you assign plaintext, the package encrypts on write using Laravel's APP_KEY, and decrypts on read. v1.4.0 is primarily a compatibility release — it adds PHP 8 installation support and updates the CI matrix — so if you were blocked from upgrading to PHP 8, this unblocks you. There are no breaking changes, no new config, and no migration required from v1.3.0.
Minimal setup:
composer require kielabokkie/laravel-conceal
TEXT/LONGTEXT (ciphertext is much longer than the plaintext, and base64-encoded JSON payloads overflow VARCHAR(255) fast).APP_KEY is set in every environment — the package relies on Laravel's encrypter, and a missing/rotated key means unreadable data.First use case: a users table with ssn, phone, or api_token columns. Add the trait, list the attributes, and your existing $user->ssn = '...' / echo $user->ssn code keeps working while the database only ever stores ciphertext.
Trait-per-model, explicit attribute list. Apply the trait only to models that need it and keep the concealed attribute list tight. Prefer this over a global/base-model trait — it documents intent and avoids accidental encryption of columns you later want to query or index.
Column migration first. Before enabling the trait, migrate the target columns to text. If you have existing plaintext rows, plan a backfill: iterate with chunkById(), re-assign each concealed attribute from its current value (forcing a dirty write), and save. Do this in a queued job or command so it is idempotent and resumable; wrap in a transaction per batch where the model allows it.
Validate plaintext, store ciphertext. Form Requests and validation rules should operate on the plaintext value before it hits the model. The encryption layer is a persistence concern, not a validation concern — don't try to validate against decrypted DB values in rules.
Keep concealed attributes out of serialization. Add concealed fields to $hidden (or strip them in API Resources). Anything that ends up in toArray()/toJson() is plaintext and will leak through logs, queued payloads, and API responses.
Lookups need a companion hash column. Because each write uses a fresh IV, ciphertext is non-deterministic — where('ssn', $value) will never match. For "find by SSN/email" flows, add a deterministic ssn_hash column (hash('sha256', $normalized)) maintained alongside the concealed attribute, and query that. Same technique for uniqueness constraints.
Key rotation. Laravel's APP_KEY rotation invalidates all previously encrypted values unless you decrypt with the old key first. Build a one-off command that reads with the old key, writes with the new key, then decommission the old key. Keep the previous key in a secure store until the rotation job has fully completed and been verified.
Testing. Set a known APP_KEY in phpunit.xml so ciphertext is deterministic per test run (IV still random, so never assert raw DB strings). Assert behaviour — that the attribute round-trips and that the raw column differs from the plaintext — rather than exact encrypted output.
text, not varchar(255), and be generous.where, orderBy, or like on concealed columns. This is the fundamental trade-off. If you need to search or sort, store a separate hash/index column or keep a non-sensitive derivative.unique constraint on the ciphertext column is meaningless with random IVs. Enforce uniqueness on the hash column instead, and normalise (lowercase/trim) before hashing.encrypted cast or a custom encrypted cast, adding the conceal trait on top produces nested payloads that are painful to unwind. Pick one mechanism per attribute.APP_KEY, or a truncated varchar column throws a DecryptException on read. Catch it at the boundary (e.g. in an API Resource) if you need graceful degradation, and log loudly — it almost always means a key or column-type problem.isDirty()/getOriginal() behave on ciphertext. Comparison logic that inspects raw attributes will see encrypted strings. Compare via the decrypted accessor, not getRawOriginal().update() bypass nothing — they go through the model and are encrypted correctly. But DB::table()->update() and raw query builder writes bypass the trait entirely and will store plaintext. Audit any raw writes to those columns.APP_KEY. Store the key separately from the database backup, and document the pairing.^1.4 explicitly rather than relying on ^1.3 constraints resolving upward. Check your CI matrix covers PHP 8.0/8.1 as the release updated upstream's own matrix.How can I help you explore Laravel packages today?