diff --git a/config-stubs/app.php b/config-stubs/app.php index 07d0eb8bef0a..badd4168e1d3 100644 --- a/config-stubs/app.php +++ b/config-stubs/app.php @@ -121,9 +121,13 @@ | */ + 'cipher' => 'AES-256-CBC', + 'key' => env('APP_KEY'), - 'cipher' => 'AES-256-CBC', + 'previous_keys' => [ + // Previous encryption keys to be used for decryption... + ], /* |-------------------------------------------------------------------------- diff --git a/config/app.php b/config/app.php index 9842d9efc29b..8fc095a9d080 100644 --- a/config/app.php +++ b/config/app.php @@ -124,9 +124,13 @@ | */ + 'cipher' => 'AES-256-CBC', + 'key' => env('APP_KEY'), - 'cipher' => 'AES-256-CBC', + 'previous_keys' => [ + // Previous encryption keys to be used for decryption... + ], /* |-------------------------------------------------------------------------- diff --git a/src/Illuminate/Cookie/CookieValuePrefix.php b/src/Illuminate/Cookie/CookieValuePrefix.php index 3f4eb22eb83d..4e7206685e87 100644 --- a/src/Illuminate/Cookie/CookieValuePrefix.php +++ b/src/Illuminate/Cookie/CookieValuePrefix.php @@ -32,13 +32,17 @@ public static function remove($cookieValue) * * @param string $cookieName * @param string $cookieValue - * @param string $key + * @param array $keys * @return string|null */ - public static function validate($cookieName, $cookieValue, $key) + public static function validate($cookieName, $cookieValue, array $keys) { - $hasValidPrefix = str_starts_with($cookieValue, static::create($cookieName, $key)); + foreach ($keys as $key) { + $hasValidPrefix = str_starts_with($cookieValue, static::create($cookieName, $key)); - return $hasValidPrefix ? static::remove($cookieValue) : null; + if ($hasValidPrefix) { + return static::remove($cookieValue); + } + } } } diff --git a/src/Illuminate/Cookie/Middleware/EncryptCookies.php b/src/Illuminate/Cookie/Middleware/EncryptCookies.php index 0066b24503d5..236bd42f261f 100644 --- a/src/Illuminate/Cookie/Middleware/EncryptCookies.php +++ b/src/Illuminate/Cookie/Middleware/EncryptCookies.php @@ -111,7 +111,7 @@ protected function validateValue(string $key, $value) { return is_array($value) ? $this->validateArray($key, $value) - : CookieValuePrefix::validate($key, $value, $this->encrypter->getKey()); + : CookieValuePrefix::validate($key, $value, $this->encrypter->getAllKeys()); } /** diff --git a/src/Illuminate/Encryption/Encrypter.php b/src/Illuminate/Encryption/Encrypter.php index 8a8c6d85b0fc..437b3cee7c0a 100755 --- a/src/Illuminate/Encryption/Encrypter.php +++ b/src/Illuminate/Encryption/Encrypter.php @@ -17,6 +17,13 @@ class Encrypter implements EncrypterContract, StringEncrypter */ protected $key; + /** + * The previous / legacy encryption keys. + * + * @var array + */ + protected $previousKeys = []; + /** * The algorithm used for encryption. * @@ -113,7 +120,7 @@ public function encrypt($value, $serialize = true) $mac = self::$supportedCiphers[strtolower($this->cipher)]['aead'] ? '' // For AEAD-algorithms, the tag / MAC is returned by openssl_encrypt... - : $this->hash($iv, $value); + : $this->hash($iv, $value, $this->key); $json = json_encode(compact('iv', 'value', 'mac', 'tag'), JSON_UNESCAPED_SLASHES); @@ -159,9 +166,15 @@ public function decrypt($payload, $unserialize = true) // Here we will decrypt the value. If we are able to successfully decrypt it // we will then unserialize it and return it out to the caller. If we are // unable to decrypt this value we will throw out an exception message. - $decrypted = \openssl_decrypt( - $payload['value'], strtolower($this->cipher), $this->key, 0, $iv, $tag ?? '' - ); + foreach ($this->getAllKeys() as $key) { + $decrypted = \openssl_decrypt( + $payload['value'], strtolower($this->cipher), $key, 0, $iv, $tag ?? '' + ); + + if ($decrypted !== false) { + break; + } + } if ($decrypted === false) { throw new DecryptException('Could not decrypt the data.'); @@ -188,11 +201,12 @@ public function decryptString($payload) * * @param string $iv * @param mixed $value + * @param string $key * @return string */ - protected function hash($iv, $value) + protected function hash($iv, $value, $key) { - return hash_hmac('sha256', $iv.$value, $this->key); + return hash_hmac('sha256', $iv.$value, $key); } /** @@ -258,9 +272,17 @@ protected function validPayload($payload) */ protected function validMac(array $payload) { - return hash_equals( - $this->hash($payload['iv'], $payload['value']), $payload['mac'] - ); + foreach ($this->getAllKeys() as $key) { + $valid = hash_equals( + $this->hash($payload['iv'], $payload['value'], $key), $payload['mac'] + ); + + if ($valid === true) { + return true; + } + } + + return false; } /** @@ -289,4 +311,35 @@ public function getKey() { return $this->key; } + + /** + * Get the current encryption key and all previous encryption keys. + * + * @return array + */ + public function getAllKeys() + { + return [$this->key, ...$this->previousKeys]; + } + + /** + * Set the previous / legacy encryption keys that should be utilized if decryption fails. + * + * @param array $key + * @return $this + */ + public function previousKeys(array $keys) + { + foreach ($keys as $key) { + if (! static::supported($key, $this->cipher)) { + $ciphers = implode(', ', array_keys(self::$supportedCiphers)); + + throw new RuntimeException("Unsupported cipher or incorrect key length. Supported ciphers are: {$ciphers}."); + } + } + + $this->previousKeys = $keys; + + return $this; + } } diff --git a/src/Illuminate/Encryption/EncryptionServiceProvider.php b/src/Illuminate/Encryption/EncryptionServiceProvider.php index a4d27a3720b9..5307042792af 100755 --- a/src/Illuminate/Encryption/EncryptionServiceProvider.php +++ b/src/Illuminate/Encryption/EncryptionServiceProvider.php @@ -29,7 +29,11 @@ protected function registerEncrypter() $this->app->singleton('encrypter', function ($app) { $config = $app->make('config')->get('app'); - return new Encrypter($this->parseKey($config), $config['cipher']); + return (new Encrypter($this->parseKey($config), $config['cipher'])) + ->previousKeys(array_map( + fn ($key) => $this->parseKey(['key' => $key]), + $config['previous_keys'] ?? [] + )); }); } diff --git a/tests/Encryption/EncrypterTest.php b/tests/Encryption/EncrypterTest.php index 64f2e9d39502..a091ea538afe 100755 --- a/tests/Encryption/EncrypterTest.php +++ b/tests/Encryption/EncrypterTest.php @@ -26,6 +26,18 @@ public function testRawStringEncryption() $this->assertSame('foo', $e->decryptString($encrypted)); } + public function testRawStringEncryptionWithPreviousKeys() + { + $previous = new Encrypter(str_repeat('b', 16)); + $previousValue = $previous->encryptString('foo'); + + $new = new Encrypter(str_repeat('a', 16)); + $new->previousKeys([str_repeat('b', 16)]); + + $decrypted = $new->decryptString($previousValue); + $this->assertSame('foo', $decrypted); + } + public function testEncryptionUsingBase64EncodedKey() { $e = new Encrypter(random_bytes(16));