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 needs a migration plan
Changing APP_KEY means ciphertext created with the old key cannot be decrypted by the new key alone. Laravel 11+ supports graceful key rotation through APP_PREVIOUS_KEYS: new values use the current key, while decryption can fall back to listed previous keys. That buys you a safe migration window, but you still need to retain the old key until stored values have been re-encrypted or are no longer needed.
Encryption is not hashing
The deciding question is whether the original value must ever be recovered. Passwords should be verified, not decrypted, so use Laravel's hashing facilities for them. A tax identifier, API credential, or private note may need to be read back later, which is where reversible encryption fits. Using encryption for passwords creates unnecessary key-management risk; using hashing for data you later need in plaintext makes recovery impossible by design.
Encrypted casts also change storage requirements
The ciphertext is significantly longer than the original plaintext and its length is not fixed in a way that makes a narrow VARCHAR a safe choice. Laravel's documentation recommends a TEXT-sized column or larger for encrypted casts. This matters most when an existing short column is converted to an encrypted cast without a migration — the application code looks correct while the database may truncate the stored ciphertext.
Use a blind lookup value only if equality lookup is genuinely required
A keyed HMAC alongside ciphertext can support exact-match lookup without storing the plaintext, but it is a separate security design, not a magic "search encrypted data" switch:
$lookup = hash_hmac('sha256', $taxId, config('app.lookup_key'));
The HMAC key should be managed separately from ordinary public data, and the lookup column reveals equality patterns — identical inputs produce identical lookup values. It does not support arbitrary substring, range, or sorting queries over the protected plaintext.
Key rotation is complete only when old keys can eventually be retired
APP_PREVIOUS_KEYS prevents an abrupt outage, but leaving every historical key configured forever defeats part of the reason to rotate. A mature rotation process decrypts with an old key when necessary, writes the value back under the current key, tracks migration progress, and removes a previous key only after no required ciphertext depends on it.