vimatech/laravel-secure-fields
Laravel package that adds transparent encryption for sensitive database fields. Protect PII, tokens, and secrets with automatic encrypt/decrypt through Eloquent casts, no schema changes or query rewrites, plus configurable keys, key rotation, and searchable blind indexes.
Architecture fit. The package intervenes at exactly the right seam: Eloquent attribute casts. Encryption becomes a model-layer concern, so controllers, form requests, and accessors are untouched and adoption cost per field is near zero. The critical architectural caveat is that the guarantee stops precisely at the model boundary — everything below it (query builder, DB::table(), replicas, BI tools, exports) sees ciphertext, and writes that bypass the model write plaintext into a column the rest of the system assumes is encrypted. Fit therefore depends less on the package than on how disciplined the codebase is about routing reads and writes through models. Codebases with heavy bulk-update, importer, or ETL paths on sensitive columns are a poor fit without remediation work first.
Integration feasibility. Mechanically easy: install, publish config, generate two independent keys (encryption + blind-index hash — no APP_KEY fallback, by design), widen target columns to TEXT or generous VARCHAR, add indexed hash columns, declare fields on models. The friction is not code, it is secrets and environments: both keys must exist in every environment that touches the data, including CI, preview deploys, queue workers, and scheduled tasks. Fail-closed behavior is a security strength and an operational tax.
Technical risk. The dominant risk is supply chain, not cryptography. Zero stars, zero dependents, a single maintainer, and a patch release (v2.0.1) that changed user-facing message punctuation are all signals that this is an early-stage, thinly-staffed, security-critical dependency. Mitigants: it delegates actual crypto to Laravel's encrypter (AES-256-GCM) rather than inventing primitives, and it is MIT-licensed so a fork is always available. The second risk tier is silent correctness failure — mass updates writing plaintext, firstOrCreate/updateOrCreate on a secure attribute creating duplicate rows on every call, narrow columns truncating ciphertext that only surfaces as "unreadable values" during a future rotation. None of these throw. The third tier is egress: decrypted values populate the attribute and therefore flow into toArray(), API resources, logs, Telescope, and queue payloads for free.
Key questions.
DB::table(), upsert, seeders, importers, replicas)? This is the gating question.Position. Adopt-with-conditions for a bounded, lower-blast-radius PII use case behind an internal adapter, pinned to an exact version. Do not adopt as the sole control for the most sensitive data tier until the fork/pin policy and rotation runbook are demonstrated in a rehearsal.
Stack fit. Laravel-native by construction: framework encrypter, standard config publishing, an artisan rotation command, and typed exceptions. Should compose cleanly with existing $casts arrays. Validation is required against our specific Laravel version, PHP version, and database matrix — MySQL strict mode, PostgreSQL TEXT defaults, and SQLite (used in tests) all behave differently enough to warrant a test pass before any real data lands.
Migration path (per field, incremental).
TEXT (ciphertext is base64 of an AEAD payload, realistically 2–4× plaintext).UNIQUE where the field is logically unique. A UNIQUE constraint on the encrypted column itself is worthless because GCM output differs per row.Key rotation is a separate, rehearsed procedure: deploy code that can decrypt both key versions to every runtime (web, queue, scheduler, replicas) → run secure-fields:rotate throttled off-peak in batches → investigate any non-zero unreadable count before continuing → retire the old key only after confirming restored backups no longer depend on it.
Compatibility work items. Decide once between the model property and the cast and standardize — mixing them destroys grep-ability, which is exactly what audits and incident response rely on. Write a one-off test asserting whether getOriginal() and isDirty() yield plaintext or ciphertext in our installed version before any observer or audit-trail work. Confirm nested JSON behavior if any secured column is also cast to array.
Sequencing.
users.phone or an internal external-ref). Prove both directions and one dry-run rotation in a non-production environment.App\Support\SecureSearch as the single call site for hashing and lookups. Pin the exact version in composer.json. Grep for ->update( and DB:: against secured tables and record the offenders.firstOrCreate/updateOrCreate on secure attributes; no bulk updates on secured tables; every secured field hidden by default. Negative test per JSON endpoint asserting plaintext is absent from the response body.Maintenance. Small surface area, disproportionate operational footprint. Ongoing obligations: two secrets managed per environment, config:clear discipline after every env change (the most common false "missing key" report), exact-version pinning, and manual review of every patch diff — v2.0.1 is direct evidence that a patch bump can alter output text that health checks or scripts may parse. Assert on exception classes and process exit codes, never message strings. The internal adapter seam keeps exit cost to one file if we fork or migrate to native encrypted casts.
Support. There is no vendor. Escalation is reading SecureFieldsException and the source. That means we must name an internal DRI and fund the tribal knowledge: the five-question debug checklist (was the row written through Eloquent? is the column wide enough? is the encrypting key version still configured? is config:cache stale? is this a raw query, replica, or DB::table() call?) answers nearly every reported symptom.
Scaling. Per-write encryption cost is trivial at normal volumes but measurable inside bulk import loops — those should be chunked and throttled rather than run in a single job. The real scaling bottleneck is rotation: batched, throttled, non-transactional, with an unreadable counter that must be read as a signal rather than a nuisance. Blind-index lookups scale like any indexed column and are fine. Analytics do not scale at all — no LIKE, ORDER BY, SUM, AVG, joins, or whereBetween on encrypted columns. Any reporting requirement must be met by exporting decrypted data to a purpose-built store with its own access controls, and that store becomes a new, larger scope item.
Failure modes, ranked by expected cost.
DB::table(), upsert, seeders, importers. Compliance break with no error and no detection until audit.firstOrCreate/updateOrCreate on a secure attribute — the lookup clause compares plaintext to ciphertext, never matches, creates a row every call. Fails silently.VARCHAR — discovered months later as unreadable rows mid-rotation.Ramp-up. Happy path is a day: install, generate keys, secure one field, verify. Genuine operational safety is one to two weeks for a single field plus one rotation rehearsal, and it never fully stops being a code-review tax on every change touching a secured table. Gate production rollout on four artifacts: a written rotation runbook, a named key owner, the per-endpoint negative test for plaintext leakage, and the three-way storage assertion on every secured field.
How can I help you explore Laravel packages today?