Laravel's Crypt facade handles symmetric encryption using AES-256-CBC (or AES-128-CBC, depending on key length) with a message authentication code, keyed by your app's APP_KEY. It's the right tool for values you need to store and later read back in plain form — not for passwords, which should always use one-way hashing instead.
Encrypting
use Illuminate\Support\Facades\Crypt;
$encrypted = Crypt::encryptString($ssn);
Decrypting
try {
$decrypted = Crypt::decryptString($encrypted);
} catch (\Illuminate\Contracts\Encryption\DecryptException $e) {
// the value was tampered with, corrupted, or encrypted under a different APP_KEY
}
Always wrap decryption in a try/catch — a tampered or corrupted ciphertext throws rather than silently returning garbage, which is a deliberate security property, not a bug to work around.
Automatic encryption on a model attribute
Laravel's encrypted cast handles this transparently at the model level:
protected function casts(): array
{
return [
'tax_id' => 'encrypted',
];
}
Reading $model->tax_id decrypts automatically; setting it encrypts automatically on save.
The mistake: encrypting a column you need to query
Encrypted values are non-deterministic — encrypting the same plaintext twice produces different ciphertext each time (by design, to prevent pattern analysis). That means WHERE tax_id = ? against an encrypted column can never match, and there's no query-level workaround. If you need to search or look up a sensitive value, store a separate deterministic hash (e.g. HMAC) alongside the encrypted value for lookups, and use the encrypted column only for retrieving the original plaintext.
APP_KEY rotation breaks existing encrypted data
Changing APP_KEY makes every previously-encrypted value permanently undecryptable under the new key — there is no built-in re-encryption migration. Losing or rotating the key without a plan is the most common way teams accidentally lose encrypted data.