skip to content

With Eloquent's encrypted casts, what is stored in the column, and what can you no longer do with an encrypted attribute?

level: seniorimportance: should knowfreq 30%

answer

  1. ciphertext from the app encrypter
  2. column must be TEXT or larger
  3. no where, orderBy or unique index
  4. encrypted:array and AsEncryptedCollection
  5. APP_KEY loss means unreadable data

basics

~20 s

The column stores ciphertext from Laravel's encrypter, keyed by APP_KEY; Eloquent decrypts on read and encrypts on write. The payload is long, so use a TEXT column, and SQL can no longer filter, sort, index or join on it.

solid answer

~40 s

`'tax_id' => 'encrypted'` makes Eloquent encrypt the value with the application's encrypter when you set it and decrypt it when you read it; `encrypted:array`, `encrypted:collection`, `encrypted:object`, `AsEncryptedArrayObject` and `AsEncryptedCollection` do the same for structured values. The stored text is a base64 JSON payload with an IV, the ciphertext and a MAC, so it is much longer than the plain value and unpredictable in length: use a `TEXT` column. Each encryption uses a fresh IV, so the same tax ID encrypts differently every time. That means `where('tax_id', $value)`, `orderBy`, `LIKE`, unique indexes and joins on the column no longer work. The key comes from `APP_KEY`; losing or changing it without keeping the old key makes every row unreadable. `Model::encryptUsing()` can swap in a dedicated encrypter.

code

php · 13 lines
php
<?php

use App\Models\Invoice;

$invoice->tax_id = 'DE123456789';
$invoice->save();                    // column holds a long base64 payload

echo $invoice->tax_id;               // DE123456789 (decrypted on read)

Invoice::where('tax_id', 'DE123456789')->exists();   // false: ciphertext differs

// equality lookups need a separate keyed-hash column you maintain yourself
Invoice::where('tax_id_hash', hash_hmac('sha256', 'DE123456789', $pepper))->first();

go deeper

for a junior

Recall that the encrypted cast stores ciphertext and gives you plain values in code.

for a middle

Explain the payload with IV and MAC, why the column must be TEXT, and why filtering and sorting stop working.

for a senior

Plan around lost queryability with blind-index columns, protect APP_KEY, and handle backups restored under a different key.

for a principal

Decide which data warrants application-level encryption versus database or disk encryption, weighing searchability, key custody and compliance.

## What the cast does On a subscription invoice, the customer's tax ID and bank-reference notes are sensitive. Declaring ```php protected function casts(): array { return [ 'tax_id' => 'encrypted', 'billing_notes' => 'encrypted:array', ]; } ``` tells Eloquent to **encrypt on write and decrypt on read**. Code sees plain values; the database sees only ciphertext. Reading the framework source, the cast calls the model's current encrypter, which is the `Crypt` facade's encrypter unless `Model::encryptUsing($encrypter)` installed another one. ## The family of encrypted casts | Cast | Read value | |---|---| | `encrypted` | the decrypted string | | `encrypted:array` / `encrypted:json` | decoded array | | `encrypted:object` | decoded `stdClass` | | `encrypted:collection` | `Collection`, rebuilt each read | | `AsEncryptedArrayObject::class` | `ArrayObject`, offset edits persist | | `AsEncryptedCollection::class` | `Collection`, offset edits persist | ## What is stored Laravel's encrypter uses an authenticated cipher. The stored value is a base64-encoded JSON payload carrying the **IV**, the **ciphertext** and a **MAC** (or tag), so: - It is several times longer than the plain value, and its length is not predictable; the documentation asks for a `TEXT` column or larger. A `VARCHAR(20)` sized for a tax ID truncates the payload and makes the row undecryptable. - A fresh random IV is used on every encryption, so the same value produces **different** ciphertext each time. - Tampering is detected: decrypting a modified payload throws a `DecryptException`. ## What you lose Because the database cannot see the value and equal inputs produce different ciphertext: 1. **No filtering:** `Invoice::where('tax_id', $taxId)` never matches. 2. **No sorting or range queries:** `orderBy('tax_id')` sorts ciphertext. 3. **No uniqueness:** a unique index on the column enforces nothing useful. 4. **No joins or `LIKE` searches** on the value. 5. **No database-side JSON queries** into `encrypted:array` columns. If you must look records up by a secret value, a common pattern is a separate column holding a keyed hash (a "blind index") of the value, which supports equality lookups only. That is an application design choice, not a built-in cast. ## Operational consequences - **The key is the data.** Everything depends on `APP_KEY`. Losing it makes every encrypted column unreadable; changing it without keeping the old key (Laravel supports previous keys for graceful rotation) has the same effect. Rotation itself is an encryption-configuration topic. - **Backups and copies.** A production dump restored into staging with a different `APP_KEY` cannot decrypt these columns. - **Serialization.** `toArray()` and JSON contain the **decrypted** value; hide the attribute if it must not leave the server. - **Performance.** Every read decrypts and every write encrypts; that is negligible per row but noticeable when exporting millions of rows. ## Adding encryption to an existing column Switching a populated plain-text column to `encrypted` breaks reads immediately: the cast tries to decrypt plain text, the payload check fails, and a `DecryptException` ("The payload is invalid.") is thrown for every old row. A safe rollout: 1. Widen the column to `TEXT` if needed. 2. Run a one-off job that reads each row's plain value with a query that bypasses the model cast, encrypts it with the same encrypter, and writes it back, in batches. 3. Deploy the model change with the `encrypted` cast only after every row is converted, or have the job and the deploy share a maintenance window. Reverse the steps to remove encryption. Either way, test against a copy of production data first. ## When to use it Use encrypted casts for **low-volume secrets that are displayed but never searched**: tax IDs, API credentials for a customer's integration, private notes. Do not use them for columns that drive queries, and remember that passwords want the `hashed` cast (one-way), not encryption.

  • Why does the same tax ID produce different stored values on two invoices?
    Laravel's encrypter generates a random IV for every encryption, and the IV is part of the stored payload. Identical plain text therefore yields different ciphertext, which protects against spotting equal values but also rules out equality lookups and unique indexes on the column.
  • Should passwords use the encrypted cast?
    No. Passwords should never be recoverable, so they use the `hashed` cast, which stores a one-way hash on assignment and is checked with the hasher. `encrypted` is reversible by design: anyone with `APP_KEY` can read the value.

An encrypted cast is like a safe-deposit box: the bank stores the box and can hand it back to you, but it cannot sort boxes by what is inside or tell you which boxes hold the same thing. Only the key holder sees the contents.

saying these in an interview costs you the question

  • Saying where('tax_id', $value) still works on an encrypted column
  • Sizing the encrypted column like the plain value, e.g. VARCHAR(20)
  • Believing encrypted casts hide the value from toArray() and JSON
  • Using the encrypted cast for passwords instead of hashed
  • Assuming a new APP_KEY can still decrypt existing rows