Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 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
18 changes: 18 additions & 0 deletions app/Config/Encryption.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@
*/
public string $key = '';

/**
* --------------------------------------------------------------------------
* Previous Encryption Keys fallback enabled
* --------------------------------------------------------------------------
* If you want to enable decryption using previous keys, set this to true.
* See the user guide for more info.
*/
public bool $previousKeysFallbackEnabled = false;
Comment thread
patel-vansh marked this conversation as resolved.
Outdated

/**
* --------------------------------------------------------------------------
* Previous Encryption Keys
* --------------------------------------------------------------------------
* If you want to enable decryption using previous keys, set them here.
* See the user guide for more info.
*/
public array $previousKeys = [];

Check failure on line 42 in app/Config/Encryption.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Property Config\Encryption::$previousKeys type has no value type specified in iterable type array.
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.

The encryption config can take in values from the .env file and having array properties can be hard to be populated. I suggest this takes a string of comma separated app keys instead.

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.

We can do that. So, this would be changed to comma separated string, right? So, we will have to do like, convert hex2bin or base64_decode on the fly when needed, or maybe, convert all previous keys and implode them as comma separated keys?

Which approach would be more better?

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 currently set the second approach as for now (while initializing in the BaseConfig, do the parsing stuff and then implode as comma-separated string for future use), but we can use the other approach as well.


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

# encryption.key =

# Previous keys fallback is disabled by default.
# If you want to enable it, uncomment the line below.
# encryption.previousKeysFallbackEnabled = true
# 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 @@
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 === 'previousKeysFallbackEnabled') {
// previousKeysFallbackEnabled must be boolean
$this->{$property} = (bool) $this->{$property};
} elseif ($property === 'previousKeys') {
// previousKeys must be an array
if (is_string($this->{$property})) {

Check failure on line 141 in system/Config/BaseConfig.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Call to function is_string() with array will always evaluate to false.
$this->{$property} = array_map(fn ($item) => $this->parseEncryptionKey($item), explode(',', $this->{$property}));

Check failure on line 142 in system/Config/BaseConfig.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis (8.2)

NoValue

system/Config/BaseConfig.php:142:117: NoValue: All possible types for this argument were invalidated - This may be dead code (see https://psalm.dev/179)
}
}
}
}
}

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

/**
* Whether to fall back to previous keys when decryption fails.
*/
protected bool $previousKeysFallbackEnabled = false;

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

/**
* The derived HMAC key
*
Expand Down Expand Up @@ -91,9 +103,11 @@ 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->previousKeysFallbackEnabled = $config->previousKeysFallbackEnabled;
$this->previousKeys = $config->previousKeys;
$this->driver = $config->driver;
$this->digest = $config->digest ?? 'SHA512';

$this->handlers = [
'OpenSSL' => extension_loaded('openssl'),
Expand All @@ -116,9 +130,11 @@ 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->previousKeysFallbackEnabled = $config->previousKeysFallbackEnabled ?? false;
$this->previousKeys = $config->previousKeys ?? [];
$this->driver = $config->driver;
$this->digest = $config->digest ?? 'SHA512';
}

if (empty($this->driver)) {
Expand Down
56 changes: 53 additions & 3 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,18 @@
*/
protected $key = '';

/**
* Whether to fall back to previous keys when decryption fails.
*/
protected bool $previousKeysFallbackEnabled = false;

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

/**
* Whether the cipher-text should be raw. If set to false, then it will be base64 encoded.
*/
Expand All @@ -80,14 +91,14 @@
/**
* {@inheritDoc}
*/
public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $params = null)

Check failure on line 94 in system/Encryption/Handlers/OpenSSLHandler.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist.

Check failure on line 94 in system/Encryption/Handlers/OpenSSLHandler.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist.

Check failure on line 94 in system/Encryption/Handlers/OpenSSLHandler.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis (8.2)

UndefinedAttributeClass

system/Encryption/Handlers/OpenSSLHandler.php:94:60: UndefinedAttributeClass: Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist (see https://psalm.dev/241)

Check failure on line 94 in system/Encryption/Handlers/OpenSSLHandler.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis (8.2)

UndefinedAttributeClass

system/Encryption/Handlers/OpenSSLHandler.php:94:31: UndefinedAttributeClass: Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist (see https://psalm.dev/241)
{
// Allow key override
if ($params !== null) {
$this->key = is_array($params) && isset($params['key']) ? $params['key'] : $params;
}

if (empty($this->key)) {

Check failure on line 101 in system/Encryption/Handlers/OpenSSLHandler.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Ignored error pattern #^Construct empty\(\) is not allowed\. Use more strict comparison\.$# in path /home/runner/work/CodeIgniter4/CodeIgniter4/system/Encryption/Handlers/OpenSSLHandler.php is expected to occur 2 times, but occurred 3 times.
throw EncryptionException::forNeedsStarterKey();
}

Expand Down Expand Up @@ -116,7 +127,7 @@
/**
* {@inheritDoc}
*/
public function decrypt($data, #[SensitiveParameter] $params = null)

Check failure on line 130 in system/Encryption/Handlers/OpenSSLHandler.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist.

Check failure on line 130 in system/Encryption/Handlers/OpenSSLHandler.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis (8.2)

UndefinedAttributeClass

system/Encryption/Handlers/OpenSSLHandler.php:130:38: UndefinedAttributeClass: Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist (see https://psalm.dev/241)
{
// Allow key override
if ($params !== null) {
Expand All @@ -127,8 +138,47 @@
throw EncryptionException::forNeedsStarterKey();
}

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

if ($result === false && $this->previousKeysFallbackEnabled && ! empty($this->previousKeys)) {

Check failure on line 148 in system/Encryption/Handlers/OpenSSLHandler.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Construct empty() is not allowed. Use more strict comparison.
foreach ($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, #[SensitiveParameter] $key)

Check failure on line 178 in system/Encryption/Handlers/OpenSSLHandler.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist.

Check failure on line 178 in system/Encryption/Handlers/OpenSSLHandler.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis (8.2)

UndefinedAttributeClass

system/Encryption/Handlers/OpenSSLHandler.php:178:48: UndefinedAttributeClass: Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist (see https://psalm.dev/241)
{
// 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 +202,7 @@
}

// 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
57 changes: 54 additions & 3 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,18 @@
*/
protected $key = '';

/**
* Whether to fall back to previous keys when decryption fails.
*/
protected bool $previousKeysFallbackEnabled = false;

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

/**
* Block size for padding message.
*
Expand All @@ -41,7 +52,7 @@
/**
* {@inheritDoc}
*/
public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $params = null)

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

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist.

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

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist.

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

View workflow job for this annotation

GitHub Actions / Psalm Analysis (8.2)

UndefinedAttributeClass

system/Encryption/Handlers/SodiumHandler.php:55:60: UndefinedAttributeClass: Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist (see https://psalm.dev/241)

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

View workflow job for this annotation

GitHub Actions / Psalm Analysis (8.2)

UndefinedAttributeClass

system/Encryption/Handlers/SodiumHandler.php:55:31: UndefinedAttributeClass: Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist (see https://psalm.dev/241)
Comment thread
patel-vansh marked this conversation as resolved.
{
$this->parseParams($params);

Expand Down Expand Up @@ -72,7 +83,7 @@
/**
* {@inheritDoc}
*/
public function decrypt($data, #[SensitiveParameter] $params = null)

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

View workflow job for this annotation

GitHub Actions / Psalm Analysis (8.2)

UndefinedAttributeClass

system/Encryption/Handlers/SodiumHandler.php:86:38: UndefinedAttributeClass: Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist (see https://psalm.dev/241)
{
$this->parseParams($params);

Expand All @@ -80,6 +91,47 @@
throw EncryptionException::forNeedsStarterKey();
}

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

if ($result === false && $this->previousKeysFallbackEnabled && ! empty($this->previousKeys)) {
foreach ($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;
}

/**
* Decrypt the data with the provided key
*
* @param string $data
* @param string $key
*
* @return string
*
* @throws EncryptionException
*/
protected function decryptWithKey($data, #[SensitiveParameter] $key)

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

View workflow job for this annotation

GitHub Actions / Psalm Analysis (8.2)

UndefinedAttributeClass

system/Encryption/Handlers/SodiumHandler.php:133:48: UndefinedAttributeClass: Attribute class CodeIgniter\Encryption\Handlers\SensitiveParameter does not exist (see https://psalm.dev/241)
{
if (mb_strlen($data, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)) {
// message was truncated
throw EncryptionException::forAuthenticationFailed();
Expand All @@ -90,7 +142,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 +158,6 @@

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

return $data;
}
Expand Down
Loading