Skip to content
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7837aa0
feat(encryption): Add environment variables in env file for previous …
patel-vansh Dec 21, 2025
be93000
feat(encryption): Add previous keys fallback variables in encryption …
patel-vansh Dec 21, 2025
972ad6e
feat(encryption): Parse the previous keys fallback variables in BaseC…
patel-vansh Dec 21, 2025
486bf57
feat(encryption): Implement the main logic of using previous keys if …
patel-vansh Dec 21, 2025
5f45c6f
feat(encryption): Add the key names in SodiumHandler.php
patel-vansh Dec 21, 2025
55ed3a7
feat(encryption): Add previous key initialization in Encryption.php
patel-vansh Dec 21, 2025
5e53f06
refactor: cs fix
patel-vansh Dec 21, 2025
68e05ba
refactor: fix static analysis problems
patel-vansh Dec 21, 2025
772bc29
feat(encryption): Change previousKeys to a comma-separated string for…
patel-vansh Dec 21, 2025
2ad2e97
feat(encryption): Remove previousKeysFallbackEnabled property and rel…
patel-vansh Dec 21, 2025
c3f5bae
refactor: cs fix
patel-vansh Dec 21, 2025
97e32f4
refactor: fix FunctionFirstClassCallableRector
patel-vansh Dec 21, 2025
6a8694f
refactor: add #[SensitiveParameter] whenever necessary
patel-vansh Dec 23, 2025
2f58622
refactor: add support for both string and array $previousKeys and mad…
patel-vansh Dec 25, 2025
628c41d
refactor: cs fix
patel-vansh Dec 25, 2025
a1902e1
refactor: fix static code analysis problems
patel-vansh Dec 25, 2025
e1331e2
refactor: changed array<string> to list<string>
patel-vansh Dec 25, 2025
5f63c3f
refactor: added tryDecryptWithFallback method in BaseHandler
patel-vansh Dec 30, 2025
2e871d2
refactor: add decryptionFailed message and exception method
patel-vansh Dec 30, 2025
bd6e045
refactor: Throwing forAuthenticationFailed instead of forDecryptionFa…
patel-vansh Dec 30, 2025
e1315e3
refactor: fix static analysis problems
patel-vansh Dec 30, 2025
c76a1dc
refactor: fix static analysis problems
patel-vansh Dec 30, 2025
f1d8312
refactor: fix cs
patel-vansh Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/Config/Encryption.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ class Encryption extends BaseConfig
*/
public string $key = '';

/**
* --------------------------------------------------------------------------
* Previous Encryption Keys
* --------------------------------------------------------------------------
* If you want to enable decryption using previous keys, set them here.
* See the user guide for more info.
*
* @var list<string>|string
*/
public array|string $previousKeys = '';

/**
* --------------------------------------------------------------------------
* Encryption Driver to Use
Expand Down
3 changes: 3 additions & 0 deletions env
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@

# encryption.key =

# Previous keys fallback; comma-separated list
# encryption.previousKeys =

#--------------------------------------------------------------------
# SESSION
#--------------------------------------------------------------------
Expand Down
31 changes: 24 additions & 7 deletions system/Config/BaseConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,35 @@ public function __construct()
foreach ($properties as $property) {
$this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix);

if ($this instanceof Encryption && $property === 'key') {
if (str_starts_with($this->{$property}, 'hex2bin:')) {
// Handle hex2bin prefix
$this->{$property} = hex2bin(substr($this->{$property}, 8));
} elseif (str_starts_with($this->{$property}, 'base64:')) {
// Handle base64 prefix
$this->{$property} = base64_decode(substr($this->{$property}, 7), true);
if ($this instanceof Encryption) {
if ($property === 'key') {
$this->{$property} = $this->parseEncryptionKey($this->{$property});
} elseif ($property === 'previousKeys') {
$keysArray = is_string($this->{$property}) ? array_map(trim(...), explode(',', $this->{$property})) : $this->{$property};
$parsedKeys = [];

foreach ($keysArray as $key) {
$parsedKeys[] = $this->parseEncryptionKey($key);
}

$this->{$property} = $parsedKeys;
}
}
}
}

protected function parseEncryptionKey(string $key): string
{
if (str_starts_with($key, 'hex2bin:')) {
return hex2bin(substr($key, 8));
}
if (str_starts_with($key, 'base64:')) {
return base64_decode(substr($key, 7), true);
}

return $key;
}

/**
* Initialization an environment-specific configuration setting
*
Expand Down
21 changes: 15 additions & 6 deletions system/Encryption/Encryption.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ class Encryption
*/
protected $key;

/**
* Array or Comma-separated list of previous keys for fallback decryption.
*
* @var list<string>|string
*/
protected array|string $previousKeys = '';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this here?


/**
* The derived HMAC key
*
Expand Down Expand Up @@ -91,9 +98,10 @@ public function __construct(?EncryptionConfig $config = null)
{
$config ??= new EncryptionConfig();

$this->key = $config->key;
$this->driver = $config->driver;
$this->digest = $config->digest ?? 'SHA512';
$this->key = $config->key;
$this->previousKeys = $config->previousKeys;
$this->driver = $config->driver;
$this->digest = $config->digest ?? 'SHA512';

$this->handlers = [
'OpenSSL' => extension_loaded('openssl'),
Expand All @@ -116,9 +124,10 @@ public function __construct(?EncryptionConfig $config = null)
public function initialize(?EncryptionConfig $config = null)
{
if ($config instanceof EncryptionConfig) {
$this->key = $config->key;
$this->driver = $config->driver;
$this->digest = $config->digest ?? 'SHA512';
$this->key = $config->key;
$this->previousKeys = $config->previousKeys ?? '';
$this->driver = $config->driver;
$this->digest = $config->digest ?? 'SHA512';
}

if (empty($this->driver)) {
Expand Down
59 changes: 57 additions & 2 deletions system/Encryption/Handlers/OpenSSLHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ class OpenSSLHandler extends BaseHandler
*/
protected $key = '';

/**
* List of previous keys for fallback decryption.
*
* @var list<string>|string
*/
protected array|string $previousKeys = '';

/**
* Whether the cipher-text should be raw. If set to false, then it will be base64 encoded.
*/
Expand Down Expand Up @@ -127,8 +134,56 @@ public function decrypt($data, #[SensitiveParameter] $params = null)
throw EncryptionException::forNeedsStarterKey();
}

// Only use fallback keys if no custom key was provided in params
$useFallback = ! isset($params['key']);
Comment thread
michalsn marked this conversation as resolved.
Outdated

$attemptDecrypt = function ($key) use ($data): array {
try {
$result = $this->decryptWithKey($data, $key);

return ['success' => true, 'data' => $result];
} catch (EncryptionException $e) {
return ['success' => false, 'exception' => $e];
}
};

$result = $attemptDecrypt($this->key);

if ($result['success']) {
return $result['data'];
}

$originalException = $result['exception'];

// If primary key failed and fallback is allowed, try previous keys
if ($useFallback && ! in_array($this->previousKeys, ['', '0', []], true)) {
foreach ($this->previousKeys as $previousKey) {
$fallbackResult = $attemptDecrypt($previousKey);

if ($fallbackResult['success']) {
return $fallbackResult['data'];
}
}
}

// All attempts failed - throw the original exception
throw $originalException;
}

/**
* Decrypt the data with the provided key
*
* @param string $data
* @param string $key
*
* @return false|string
*
* @throws EncryptionException
*/
protected function decryptWithKey($data, #[SensitiveParameter] $key)
{
// derive a secret key
$authKey = \hash_hkdf($this->digest, $this->key, 0, $this->authKeyInfo);
$authKey = \hash_hkdf($this->digest, $key, 0, $this->authKeyInfo);

$hmacLength = $this->rawData
? $this->digestSize[$this->digest]
Expand All @@ -152,7 +207,7 @@ public function decrypt($data, #[SensitiveParameter] $params = null)
}

// derive a secret key
$encryptKey = \hash_hkdf($this->digest, $this->key, 0, $this->encryptKeyInfo);
$encryptKey = \hash_hkdf($this->digest, $key, 0, $this->encryptKeyInfo);

return \openssl_decrypt($data, $this->cipher, $encryptKey, OPENSSL_RAW_DATA, $iv);
}
Expand Down
62 changes: 59 additions & 3 deletions system/Encryption/Handlers/SodiumHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ class SodiumHandler extends BaseHandler
*/
protected $key = '';

/**
* List of previous keys for fallback decryption.
*
* @var list<string>|string
*/
protected array|string $previousKeys = '';

/**
* Block size for padding message.
*
Expand Down Expand Up @@ -80,6 +87,56 @@ public function decrypt($data, #[SensitiveParameter] $params = null)
throw EncryptionException::forNeedsStarterKey();
}

// Only use fallback keys if no custom key was provided in params
$useFallback = ! isset($params['key']);

$attemptDecrypt = function ($key) use ($data): array {
try {
$result = $this->decryptWithKey($data, $key);
sodium_memzero($key);

return ['success' => true, 'data' => $result];
} catch (EncryptionException $e) {
sodium_memzero($key);

return ['success' => false, 'exception' => $e];
}
};

$result = $attemptDecrypt($this->key);

if ($result['success']) {
return $result['data'];
}

$originalException = $result['exception'];

// If primary key failed and fallback is allowed, try previous keys
if ($useFallback && ! in_array($this->previousKeys, ['', '0', []], true)) {
foreach ($this->previousKeys as $previousKey) {
$fallbackResult = $attemptDecrypt($previousKey);

if ($fallbackResult['success']) {
return $fallbackResult['data'];
}
}
}

throw $originalException;
}

/**
* Decrypt the data with the provided key
*
* @param string $data
* @param string $key
*
* @return string
*
* @throws EncryptionException
*/
protected function decryptWithKey($data, #[SensitiveParameter] $key)
{
if (mb_strlen($data, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)) {
// message was truncated
throw EncryptionException::forAuthenticationFailed();
Expand All @@ -90,7 +147,7 @@ public function decrypt($data, #[SensitiveParameter] $params = null)
$ciphertext = self::substr($data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);

// decrypt data
$data = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->key);
$data = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);

if ($data === false) {
// message was tampered in transit
Expand All @@ -106,7 +163,6 @@ public function decrypt($data, #[SensitiveParameter] $params = null)

// cleanup buffers
sodium_memzero($ciphertext);
sodium_memzero($this->key);

return $data;
}
Expand All @@ -120,7 +176,7 @@ public function decrypt($data, #[SensitiveParameter] $params = null)
*
* @throws EncryptionException If key is empty
*/
protected function parseParams($params)
protected function parseParams(#[SensitiveParameter] $params)
{
if ($params === null) {
return;
Expand Down
Loading