Description
The decrypt() method in Encrypter.php iterates over all encryption keys and short-circuits MAC validation once a valid key is found:
File: src/Illuminate/Encryption/Encrypter.php lines 170-185
$foundValidMac = false;
foreach ($this->getAllKeys() as $key) {
if (
$this->shouldValidateMac() &&
! ($foundValidMac = $foundValidMac || $this->validMacForKey($payload, $key))
) {
continue;
}
$decrypted = \openssl_decrypt(...);
if ($decrypted !== false) {
break;
}
}
While validMacForKey() correctly uses hash_equals() for timing-safe comparison per-key, the boolean short-circuit $foundValidMac || ... creates a measurable timing difference:
- Key is first in rotation: 1 ×
validMacForKey() + 1 × openssl_decrypt()
- Key is last in rotation (N keys): N ×
validMacForKey() + 1 × openssl_decrypt()
The difference is (N-1) × hash_equals() time, which is measurable with enough samples.
Impact
An attacker with access to many encrypted payloads and precise timing measurements can determine which position in the key rotation was used to encrypt a given payload. During key rotation, this reveals whether data was encrypted with the old or new key — useful metadata for targeted cryptographic attacks.
Suggested Fix
Always evaluate MAC for all keys to ensure constant-time behavior regardless of key position:
$validKeyIndex = null;
foreach ($this->getAllKeys() as $index => $key) {
if ($this->shouldValidateMac() && $this->validMacForKey($payload, $key)) {
$validKeyIndex ??= $index; // Record first valid, but don't short-circuit
}
}
if ($this->shouldValidateMac() && $validKeyIndex === null) {
throw new DecryptException('The MAC is invalid.');
}
// Now decrypt with the valid key
$decrypted = \openssl_decrypt(
$payload['value'], strtolower($this->cipher),
$this->getAllKeys()[$validKeyIndex], 0, $iv, $tag ?? ''
);
Versions
- Laravel 13.x
- Affects any version using key rotation with
APP_PREVIOUS_KEYS
Description
The
decrypt()method inEncrypter.phpiterates over all encryption keys and short-circuits MAC validation once a valid key is found:File:
src/Illuminate/Encryption/Encrypter.phplines 170-185While
validMacForKey()correctly useshash_equals()for timing-safe comparison per-key, the boolean short-circuit$foundValidMac || ...creates a measurable timing difference:validMacForKey()+ 1 ×openssl_decrypt()validMacForKey()+ 1 ×openssl_decrypt()The difference is
(N-1) × hash_equals()time, which is measurable with enough samples.Impact
An attacker with access to many encrypted payloads and precise timing measurements can determine which position in the key rotation was used to encrypt a given payload. During key rotation, this reveals whether data was encrypted with the old or new key — useful metadata for targeted cryptographic attacks.
Suggested Fix
Always evaluate MAC for all keys to ensure constant-time behavior regardless of key position:
Versions
APP_PREVIOUS_KEYS