dolphiq/laravel-aescrypt
dolphiq/laravel-aescrypt brings AES Crypt compatible file encryption to Laravel. It offers straightforward facades and helpers to encrypt or decrypt files using AES-256, making secure, standards-compatible file storage easy to add to any Laravel application.
Install and publish config
composer require dolphiq/laravel-aescrypt
php artisan vendor:publish --tag=aescrypt-config
The package is a thin wrapper around MySQL's AES_ENCRYPT / AES_DECRYPT, so the first thing to verify is the key and cipher in config/aescrypt.php. Put the key in .env (AESCRYPT_KEY=...) and never commit it — the whole point is that ciphertext is useless without it.
Make the columns binary-safe. Encrypted output is raw binary. Use VARBINARY (preferred) or BLOB/TEXT, not VARCHAR with a utf8 collation. Leave headroom for block padding:
$table->binary('ssn')->nullable(); // or $table->string('ssn') is wrong
If you are adding encryption to an existing table, plan a migration + backfill rather than an in-place column type change on a large table.
Attach the trait to the model and declare which attributes are encrypted (confirm the exact property/trait names against the README — the concept is the same across versions):
use Dolphiq\AesCrypt\Traits\AesCrypt;
class Customer extends Model
{
use AesCrypt;
protected $encryptable = ['ssn', 'phone'];
}
The trait hooks getAttribute/setAttribute, so $customer->ssn = '123-45-6789' writes ciphertext and reading it back returns plaintext.
First realistic use case: you have PII (SSNs, phone numbers, medical notes) that must be encrypted at rest, and you still want to be able to query it from SQL — reporting, support tooling, or an admin search screen. That is the niche this package fills; it is not a drop-in replacement for Laravel's encrypted cast.
Where to look first, in order: the README's query examples → config/aescrypt.php → the trait source (getAttribute, setAttribute, any query scope) → your MySQL version (SELECT VERSION()). The MySQL version matters more than you think (see Gotchas).
Basic CRUD is transparent. Create, update, and read work through Eloquent as normal:
$customer = Customer::create([
'name' => 'Jane Doe',
'ssn' => '123-45-6789', // encrypted on save
]);
echo $customer->ssn; // decrypted on read
Querying encrypted columns. You cannot do where('ssn', $value) — the column holds ciphertext. Use the DB function, ideally through whatever scope the trait provides:
Customer::whereRaw('AES_DECRYPT(ssn, ?) = ?', [config('aescrypt.key'), $ssn])->first();
Be explicit that this is a full table scan. For any table that grows past a few thousand rows, add a deterministic blind-index column and query that instead:
// migration
$table->string('ssn_hash', 64)->index();
// model
$customer->ssn_hash = hash_hmac('sha256', $ssn, config('aescrypt.index_key'));
Then where('ssn_hash', hash_hmac(...)) uses the index, and you only fall back to AES_DECRYPT for display.
Partial search / admin filters. AES_DECRYPT(col, key) LIKE ? works but is the worst-case query. Push these to a read replica or an offline export job, and cap result sets. For fuzzy matching, decrypt in a queued job into a search index rather than hitting MySQL per keystroke.
Reporting and exports. Use selectRaw to decrypt only the columns you need:
Customer::select('id', 'name')
->selectRaw('AES_DECRYPT(ssn, ?) as ssn', [config('aescrypt.key')])
->get();
Never SELECT * into a CSV dump without decrypting deliberately — you will hand someone raw binary.
Validation and form requests operate on plaintext before the model sees it, so Rule::unique('customers', 'ssn') will silently fail (it compares plaintext against ciphertext). Swap it for a hash-column check.
Key rotation. Store a key_version column per row, keep the old key in config, and decrypt with the row's version. Re-encrypt lazily on read/write in a job, then drop the old key. Doing it in one ALTER/mass update is a maintenance window you don't want.
Serialization. Check toArray()/JSON resources explicitly in a test. The trait usually handles attributesToArray, but if you have custom $appends or accessors that read the raw attribute, you'll leak base64/binary into API responses.
MySQL only. Tests running on SQLite or Postgres will error out (no such function: AES_ENCRYPT). Configure a MySQL service container in CI, or mock the trait in feature tests. Do not let this slip into phpunit.xml defaults.
MySQL version drift. AES_ENCRYPT changed key handling in MySQL 5.6.17; data encrypted on an older server may be undecryptable on a newer one. Pin your server version and document it.
It's ECB mode, no IV, no authentication. MySQL's AES_ENCRYPT is deterministic and unauthenticated. Identical plaintexts produce identical ciphertext, and tampering isn't detected. This is acceptable for "encrypted at rest against a stolen disk/dump" — it is not equivalent to Laravel's encrypted cast, which uses AES-256-CBC with a MAC. Don't use this for secrets where integrity matters.
The key shows up in query logs. Any whereRaw/selectRaw with the key bound in leaks it into the general log, slow query log, performance_schema, Telescope, Debugbar, and DB::listen output. Disable those in production, or accept that anyone with log access has the key. This is the single biggest operational risk of the package.
Eloquent bypasses. These skip mutators entirely and write plaintext:
DB::table('customers')->insert(...)Customer::where(...)->update([...])upsert, insertOrIgnoreGrep your codebase for these on encrypted tables. Seeders are the usual culprit.
updateOrCreate/firstOrCreate silently duplicate. The lookup where('ssn', ...) compares plaintext to ciphertext and returns nothing, so you insert a second row. Use the hash column for the match.
Cast conflicts. Don't put Laravel's 'encrypted' cast on the same attribute as the trait — it'll double-encrypt. Also watch ordering with array/json casts: the trait must decrypt to a string before the cast decodes it.
Column sizing. MySQL pads to the AES block size (16 bytes), so a 9-byte SSN can occupy 16+ bytes. Size VARBINARY generously (e.g. VARBINARY(255)) or you'll get silent truncation on longer plaintexts.
Debugging. From the MySQL client, SELECT HEX(ssn) FROM customers LIMIT 1; confirms you're storing binary, and SELECT AES_DECRYPT(ssn, 'key') FROM customers; confirms the key is right. If you see base64 garbage, your column is VARCHAR with a charset and MySQL mangled it.
Backups. Dumps contain ciphertext but the key lives in .env. Back up the key in a separate secret store — losing it means losing the data, with no recovery path.
Extension points. If the trait's query helpers don't fit, override getAttribute/setAttribute in a base model, or wrap the whereRaw call in a local scope (scopeWhereEncrypted) so the key binding lives in one place. That scope is also the right place to swap in the hash-index path later.
Queues are generally fine with SerializesModels (the job stores the model ID and reloads it), but if you serialize the model manually or pass plaintext PII in a job payload, that plaintext sits in your jobs table in the clear.
How can I help you explore Laravel packages today?