skip to content

In Laravel, how does Crypt::encryptString() differ from Crypt::encrypt() and the encrypt() helper, and why must each be paired with its own decrypt call?

level: middleimportance: should knowfreq 38%

answer

  1. one of them serializes first
  2. encrypt($value, $serialize = true)
  3. decryptString returns the serialized text
  4. decrypt() calls unserialize()
  5. ErrorException, not DecryptException

basics

~10 s

Crypt::encrypt() and the encrypt() helper run PHP serialize() before encrypting, so arrays and objects round-trip through decrypt(); encryptString() encrypts the raw string. Mixing the pairs returns serialized text or makes unserialize() fail.

solid answer

~40 s

`Crypt::encrypt($value, $serialize = true)` and the `encrypt()` helper pass the value through PHP's `serialize()` first, so an array, an integer or an object comes back with its type from `decrypt()`, which calls `unserialize()`. `Crypt::encryptString()` is simply `encrypt($value, false)`: the raw string is encrypted, and `decryptString()` returns it unchanged. The MAC and the cipher are identical in both paths, so a mismatch is not caught as tampering: `decryptString(encrypt('secret'))` returns `s:6:"secret";`, and `decrypt(encryptString('secret'))` makes `unserialize()` emit a warning that Laravel's error handler turns into an `ErrorException`. Prefer the string pair for stored strings and for anything a non-PHP service might read, and avoid calling `unserialize` on data you did not need serialized.

code

php · 12 lines
php
<?php

use Illuminate\Support\Facades\Crypt;

$token = Crypt::encryptString('sk_live_abc123');
Crypt::decryptString($token);          // 'sk_live_abc123'

$prefs = encrypt(['plan' => 'pro', 'trial' => false]);
decrypt($prefs);                         // ['plan' => 'pro', 'trial' => false]

Crypt::decryptString(encrypt('secret')); // 's:6:"secret";'
// decrypt(Crypt::encryptString('secret')) -> unserialize() warning -> ErrorException

go deeper

for a junior

Remember that encrypt() serializes and encryptString() does not, and that each must be decrypted with its own partner call.

for a middle

Explain what serialize() produces, why the MAC cannot detect a pair mismatch, and why a mismatch surfaces as ErrorException or odd text.

for a senior

Choose the string pair for persisted and shared data, and explain why unserialize on decrypted input raises the stakes of a key leak.

for a principal

Set a team convention for which encrypter calls are allowed where, weighing structured payload convenience against interoperability and deserialization risk.

## Two entry points into one encrypter Laravel's encrypter (`Illuminate\Encryption\Encrypter`, reached through the `Crypt` facade or the `encrypt()`/`decrypt()` helpers) has one real encryption method: `encrypt($value, $serialize = true)`. Everything else is a thin wrapper: - `Crypt::encrypt($value)` and the helper `encrypt($value)` call it with `$serialize = true`; - `Crypt::encryptString($value)` calls `encrypt($value, false)`; - `decrypt($payload, $unserialize = true)` and `decryptString($payload)` mirror them on the way back. **Serialization** means PHP's `serialize()`: it turns any PHP value into a string such as `s:6:"secret";` (a string of six bytes) or `a:1:{s:4:"plan";s:3:"pro";}` (an array). `unserialize()` rebuilds the value from that text. ## What each pair is for | Call | Before encryption | After decryption | Accepts | |---|---|---|---| | `encrypt()` / `Crypt::encrypt()` | `serialize($value)` | `unserialize($plain)` | any serializable PHP value | | `Crypt::encryptString()` | nothing | nothing | a string | Use `encrypt()` when you really need to carry a structured PHP value, such as a small array of flags in a cookie-like token, and the only reader is this PHP application. Use `encryptString()` when the value already is a string: an API token for a third-party service, a note, an identifier. It is the one to choose when: 1. the ciphertext may be read by something other than PHP (serialized PHP text means nothing to a Node or Go service); 2. you want decryption to return exactly the bytes you stored; 3. you want to avoid running `unserialize()` on decrypted data at all. ## What happens when the pairs are mixed The MAC is computed over the IV and the ciphertext, not over the plaintext format, so the encrypter cannot tell which pair produced a payload. Both mistakes pass the integrity check and fail later: - `Crypt::decryptString(encrypt('secret'))` succeeds and returns the literal serialized text `s:6:"secret";`. Code that compares it with `'secret'` fails silently, and a stored value gains stray characters. - `Crypt::decrypt(Crypt::encryptString('secret'))` hands `secret` to `unserialize()`, which cannot parse it. PHP emits an `E_WARNING` ("Error at offset ..."), and Laravel's error handler converts warnings into an `ErrorException`. It is **not** a `DecryptException`, so a `catch (DecryptException $e)` block around the call does not catch it. The fix is always the same: decrypt with the partner of the call that encrypted. ## Why the serialize step matters for security `unserialize()` can instantiate objects and trigger their magic methods. With a valid key, that is safe because only your application can produce a payload that passes the MAC. It turns into a risk when the key is not secret any more: whoever holds a leaked `APP_KEY` can build a payload that passes the MAC and contains a crafted object graph, and any code path that calls `decrypt()` with its default `$unserialize = true` on attacker-supplied input will rebuild it. This is one reason a leaked key is treated as an incident, and a reason to use the string pair wherever a plain string is enough. ## What the framework itself chooses Laravel's own code makes the same choice you should. The cookie-encryption middleware keeps a static `$serialize` flag that is `false` by default, so cookie values are encrypted as plain strings and never unserialized on the way back in. That default exists because cookies are attacker-supplied input: with serialization off, even a forged cookie cannot smuggle an object graph into `unserialize()`. Queued jobs are different, because a job object has to be rebuilt in the worker, so encrypted jobs rely on PHP serialization by design. The lesson carries over directly to application code: serialize when you genuinely need a PHP object back, and treat plain strings as the default everywhere else. ## Practical guidance - Default to `encryptString()`/`decryptString()` for strings you persist. - Keep `encrypt()`/`decrypt()` for PHP values you control end to end. - Never switch one side of a pair without migrating what is already stored. - Catch `DecryptException` for tampered or foreign ciphertext, but remember a pair mismatch surfaces as an `ErrorException` or as silently wrong data instead.

  • Can you tell from a stored Laravel payload whether it was produced by encrypt() or encryptString()?
    Not from the envelope: both produce the same base64 JSON with `iv`, `value`, `mac` and `tag`, and the MAC does not cover the plaintext format. Only after decrypting does it show, because serialized text starts with a type marker such as `s:6:`. That is why the pairing has to be a convention in your code.
  • Why is decrypt() with its default unserialize step riskier after an APP_KEY leak?
    The MAC only proves the payload was made with a listed key. Someone holding the leaked key can craft a payload containing a serialized object graph, and `decrypt()` will pass it to `unserialize()`, which can instantiate classes and run their magic methods. `decryptString()` never unserializes, so string data avoids that path.

saying these in an interview costs you the question

  • encryptString() uses a weaker cipher than encrypt().
  • A mismatched pair is caught as a DecryptException.
  • decryptString() of an encrypt() payload returns the original string.
  • encrypt() stores the PHP type in the MAC so decrypt() can check it.
  • Serialization only matters for performance, never for security.