Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 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
9 changes: 9 additions & 0 deletions app/Config/Encryption.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ 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.
*/
public string $previousKeys = '';
Comment thread
patel-vansh marked this conversation as resolved.
Outdated

/**
* --------------------------------------------------------------------------
* 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
30 changes: 23 additions & 7 deletions system/Config/BaseConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,34 @@ 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 = array_map(trim(...), explode(',', $this->{$property}));
$parsedKeys = [];

foreach ($keysArray as $key) {
$parsedKeys[] = $this->parseEncryptionKey($key);
}
$this->{$property} = implode(',', $parsedKeys);
Comment thread
patel-vansh marked this conversation as resolved.
Outdated
}
}
}
}

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
19 changes: 13 additions & 6 deletions system/Encryption/Encryption.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class Encryption
*/
protected $key;

/**
* Comma-separated list of previous keys for fallback decryption.
*/
protected string $previousKeys = '';

/**
* The derived HMAC key
*
Expand Down Expand Up @@ -91,9 +96,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 +122,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
54 changes: 49 additions & 5 deletions system/Encryption/Handlers/OpenSSLHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
namespace CodeIgniter\Encryption\Handlers;

use CodeIgniter\Encryption\Exceptions\EncryptionException;
use SensitiveParameter;

/**
* Encryption handling for OpenSSL library
Expand Down Expand Up @@ -56,6 +55,11 @@ class OpenSSLHandler extends BaseHandler
*/
protected $key = '';

/**
* List of previous keys for fallback decryption.
*/
protected string $previousKeys = '';

/**
* Whether the cipher-text should be raw. If set to false, then it will be base64 encoded.
*/
Expand All @@ -80,7 +84,7 @@ class OpenSSLHandler extends BaseHandler
/**
* {@inheritDoc}
*/
public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $params = null)
public function encrypt($data, $params = null)
{
// Allow key override
if ($params !== null) {
Expand Down Expand Up @@ -116,7 +120,7 @@ public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $para
/**
* {@inheritDoc}
*/
public function decrypt($data, #[SensitiveParameter] $params = null)
public function decrypt($data, $params = null)
{
// Allow key override
if ($params !== null) {
Expand All @@ -127,8 +131,48 @@ public function decrypt($data, #[SensitiveParameter] $params = null)
throw EncryptionException::forNeedsStarterKey();
}

$result = false;

try {
$result = $this->decryptWithKey($data, $this->key);
} catch (EncryptionException $e) {
$exception = $e;
}

if ($result === false && $this->previousKeys !== '') {
foreach (explode(',', $this->previousKeys) as $previousKey) {
try {
$result = $this->decryptWithKey($data, $previousKey);
if ($result !== false) {
return $result;
}
} catch (EncryptionException) {
// Try next key
}
}
}

if (isset($exception)) {
throw $exception;
}

return $result;
}

/**
* Decrypt the data with the provided key
*
* @param string $data
* @param string $key
*
* @return false|string
*
* @throws EncryptionException
*/
protected function decryptWithKey($data, $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 +196,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
55 changes: 50 additions & 5 deletions system/Encryption/Handlers/SodiumHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
namespace CodeIgniter\Encryption\Handlers;

use CodeIgniter\Encryption\Exceptions\EncryptionException;
use SensitiveParameter;

/**
* SodiumHandler uses libsodium in encryption.
Expand All @@ -31,6 +30,11 @@
*/
protected $key = '';

/**
* List of previous keys for fallback decryption.
*/
protected string $previousKeys = '';

/**
* Block size for padding message.
*
Expand All @@ -41,7 +45,7 @@
/**
* {@inheritDoc}
*/
public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $params = null)
Comment thread
patel-vansh marked this conversation as resolved.
public function encrypt($data, $params = null)
{
$this->parseParams($params);

Expand Down Expand Up @@ -72,14 +76,56 @@
/**
* {@inheritDoc}
*/
public function decrypt($data, #[SensitiveParameter] $params = null)
public function decrypt($data, $params = null)
{
$this->parseParams($params);

if (empty($this->key)) {
throw EncryptionException::forNeedsStarterKey();
}

$result = false;

try {
$result = $this->decryptWithKey($data, $this->key);
sodium_memzero($this->key);
} catch (EncryptionException $e) {
$exception = $e;
sodium_memzero($this->key);
}

if ($result === false && $this->previousKeys !== '') {
foreach (explode(',', $this->previousKeys) as $previousKey) {
try {
$result = $this->decryptWithKey($data, $previousKey);
if (isset($result)) {
return $result;
}
} catch (EncryptionException) {
// Try next key
}
}
}

if (isset($exception)) {
throw $exception;
}

return $result;

Check failure on line 114 in system/Encryption/Handlers/SodiumHandler.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Variable $result might not be defined.
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.

I find this hard to read in its current form. Wrapping the logic in a callback method might make it clearer and easier to maintain, especially since the existing method logic won't change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I tried to make it a more readable, please check now.

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.

I was thinking of adding this method to the BaseHandler:

protected function tryDecryptWithFallback($data, #[SensitiveParameter] $params, callable $decryptCallback)
{
   try {
       return $decryptCallback($data, $params);
   } catch (EncryptionException $e) {
       if ($this->previousKeys === []) {
           throw $e;
       }

       if (is_string($params) || (is_array($params) && isset($params['key']))) {
           throw $e;
       }

       foreach ($this->previousKeys as $previousKey) {
           try {
               $previousParams = is_array($params)
                   ? array_merge($params, ['key' => $previousKey])
                   : $previousKey;

               return $decryptCallback($data, $previousParams);
           } catch (EncryptionException) {
               continue;
           }
       }

       throw $e;
   }
}

and then using it like:

public function decrypt($data, #[SensitiveParameter] $params = null)
{
    return $this->tryDecryptWithFallback($data, $params, function ($data, $params): string {
        // original method content
    });
}

From my perspective, this is clearer, as the original methods remain largely unchanged and a single shared method handles both handlers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, adding it in BaseHandler.php was also my first thought, but I thought that anyone that has created their own encryption handler would have to implement this method after update.

So, will it be okay to introduce a new method in base handler?

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.

Yes, as long as it's universal and can be used with any handler.

Copy link
Copy Markdown
Contributor Author

@patel-vansh patel-vansh Dec 26, 2025

Choose a reason for hiding this comment

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

One thing I would like to note here is, in the OpenSSLHandler.php, openssl_decrypt method returns string|false. So, only relying on catching EncryptionException to go for fallback would not be best especially for OpenSSL Handler.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was thinking something like this for BaseHandler.php

protected function tryDecryptWithFallback($data, #[SensitiveParameter] $params, callable $decryptCallback)
{
    $exception = null;
    
    try {
        $result = $decryptCallback($data, $params);
        
        // Handle case where decryption returns false (like OpenSSL)
        if ($result !== false) {
            return $result;
        }
    } catch (EncryptionException $e) {
        $exception = $e;
    }
    
    // Only try fallback keys if: 
    // 1. We have previous keys configured
    // 2. No custom key was provided in params
    if ($this->previousKeys === []) {
        if ($exception !== null) {
            throw $exception;
        }
        return false;
    }
    
    if (is_string($params) || (is_array($params) && isset($params['key']))) {
        if ($exception !== null) {
            throw $exception;
        }
        return false;
    }
    
    // Try each previous key
    foreach ($this->previousKeys as $previousKey) {
        try {
            $previousParams = is_array($params)
                ? array_merge($params, ['key' => $previousKey])
                : $previousKey;
            
            $result = $decryptCallback($data, $previousParams);
            
            // Return on success (not false)
            if ($result !== false) {
                return $result;
            }
        } catch (EncryptionException) {
            // Continue to next key
            continue;
        }
    }
    
    // All keys failed - throw original exception or return false
    if ($exception !== null) {
        throw $exception;
    }
    
    return false;
}

What are your thoughts on this?

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.

Good catch on openssl_decrypt() returning false. Given the existing pre-checks, this case should be extremely unlikely. I suggest updating OpenSSLHandler to throw EncryptionException::forAuthenticationFailed() when the result is false, which would allow us to simplify the BaseHandler logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've done that. Please check.

}

/**
* Decrypt the data with the provided key
*
* @param string $data
* @param string $key
*
* @return string
*
* @throws EncryptionException
*/
protected function decryptWithKey($data, $key)
{
if (mb_strlen($data, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)) {
// message was truncated
throw EncryptionException::forAuthenticationFailed();
Expand All @@ -90,7 +136,7 @@
$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 +152,6 @@

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

return $data;
}
Expand Down
Loading