Skip to content

Commit a46eeee

Browse files
authored
fix: inconsistent key handling in encryption (#9868)
1 parent 5f104f6 commit a46eeee

File tree

8 files changed

+162
-38
lines changed

8 files changed

+162
-38
lines changed

system/Encryption/Handlers/OpenSSLHandler.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,16 @@ class OpenSSLHandler extends BaseHandler
8383
public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $params = null)
8484
{
8585
// Allow key override
86-
if ($params !== null) {
87-
$this->key = is_array($params) && isset($params['key']) ? $params['key'] : $params;
88-
}
86+
$key = $params !== null
87+
? (is_array($params) && isset($params['key']) ? $params['key'] : $params)
88+
: $this->key;
8989

90-
if (empty($this->key)) {
90+
if (empty($key)) {
9191
throw EncryptionException::forNeedsStarterKey();
9292
}
9393

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

9797
// basic encryption
9898
$iv = ($ivSize = \openssl_cipher_iv_length($this->cipher)) ? \openssl_random_pseudo_bytes($ivSize) : null;
@@ -106,7 +106,7 @@ public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $para
106106
$result = $this->rawData ? $iv . $data : base64_encode($iv . $data);
107107

108108
// derive a secret key
109-
$authKey = \hash_hkdf($this->digest, $this->key, 0, $this->authKeyInfo);
109+
$authKey = \hash_hkdf($this->digest, $key, 0, $this->authKeyInfo);
110110

111111
$hmacKey = \hash_hmac($this->digest, $result, $authKey, $this->rawData);
112112

@@ -119,16 +119,16 @@ public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $para
119119
public function decrypt($data, #[SensitiveParameter] $params = null)
120120
{
121121
// Allow key override
122-
if ($params !== null) {
123-
$this->key = is_array($params) && isset($params['key']) ? $params['key'] : $params;
124-
}
122+
$key = $params !== null
123+
? (is_array($params) && isset($params['key']) ? $params['key'] : $params)
124+
: $this->key;
125125

126-
if (empty($this->key)) {
126+
if (empty($key)) {
127127
throw EncryptionException::forNeedsStarterKey();
128128
}
129129

130130
// derive a secret key
131-
$authKey = \hash_hkdf($this->digest, $this->key, 0, $this->authKeyInfo);
131+
$authKey = \hash_hkdf($this->digest, $key, 0, $this->authKeyInfo);
132132

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

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

157157
return \openssl_decrypt($data, $this->cipher, $encryptKey, OPENSSL_RAW_DATA, $iv);
158158
}

system/Encryption/Handlers/SodiumHandler.php

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,28 +43,36 @@ class SodiumHandler extends BaseHandler
4343
*/
4444
public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $params = null)
4545
{
46-
$this->parseParams($params);
46+
// Allow key override
47+
$key = $params !== null
48+
? (is_array($params) && isset($params['key']) ? $params['key'] : $params)
49+
: $this->key;
4750

48-
if (empty($this->key)) {
51+
// Allow blockSize override
52+
$blockSize = (is_array($params) && isset($params['blockSize']))
53+
? $params['blockSize']
54+
: $this->blockSize;
55+
56+
if (empty($key)) {
4957
throw EncryptionException::forNeedsStarterKey();
5058
}
5159

5260
// create a nonce for this operation
5361
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); // 24 bytes
5462

5563
// add padding before we encrypt the data
56-
if ($this->blockSize <= 0) {
64+
if ($blockSize <= 0) {
5765
throw EncryptionException::forEncryptionFailed();
5866
}
5967

60-
$data = sodium_pad($data, $this->blockSize);
68+
$data = sodium_pad($data, $blockSize);
6169

6270
// encrypt message and combine with nonce
63-
$ciphertext = $nonce . sodium_crypto_secretbox($data, $nonce, $this->key);
71+
$ciphertext = $nonce . sodium_crypto_secretbox($data, $nonce, $key);
6472

6573
// cleanup buffers
6674
sodium_memzero($data);
67-
sodium_memzero($this->key);
75+
sodium_memzero($key);
6876

6977
return $ciphertext;
7078
}
@@ -74,9 +82,17 @@ public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $para
7482
*/
7583
public function decrypt($data, #[SensitiveParameter] $params = null)
7684
{
77-
$this->parseParams($params);
85+
// Allow key override
86+
$key = $params !== null
87+
? (is_array($params) && isset($params['key']) ? $params['key'] : $params)
88+
: $this->key;
89+
90+
// Allow blockSize override
91+
$blockSize = (is_array($params) && isset($params['blockSize']))
92+
? $params['blockSize']
93+
: $this->blockSize;
7894

79-
if (empty($this->key)) {
95+
if (empty($key)) {
8096
throw EncryptionException::forNeedsStarterKey();
8197
}
8298

@@ -90,23 +106,23 @@ public function decrypt($data, #[SensitiveParameter] $params = null)
90106
$ciphertext = self::substr($data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
91107

92108
// decrypt data
93-
$data = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->key);
109+
$data = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
94110

95111
if ($data === false) {
96112
// message was tampered in transit
97113
throw EncryptionException::forAuthenticationFailed(); // @codeCoverageIgnore
98114
}
99115

100116
// remove extra padding during encryption
101-
if ($this->blockSize <= 0) {
117+
if ($blockSize <= 0) {
102118
throw EncryptionException::forAuthenticationFailed();
103119
}
104120

105-
$data = sodium_unpad($data, $this->blockSize);
121+
$data = sodium_unpad($data, $blockSize);
106122

107123
// cleanup buffers
108124
sodium_memzero($ciphertext);
109-
sodium_memzero($this->key);
125+
sodium_memzero($key);
110126

111127
return $data;
112128
}
@@ -119,6 +135,8 @@ public function decrypt($data, #[SensitiveParameter] $params = null)
119135
* @return void
120136
*
121137
* @throws EncryptionException If key is empty
138+
*
139+
* @deprecated 4.7.0 No longer used.
122140
*/
123141
protected function parseParams($params)
124142
{

tests/system/Encryption/Handlers/OpenSSLHandlerTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,27 @@ public function testWithWrongKeyArray(): void
137137
$key2 = 'Holy cow, batman!';
138138
$this->assertNotSame($message1, $encrypter->decrypt($encoded, ['key' => $key2]));
139139
}
140+
141+
public function testInternalKeyNotModifiedByParams(): void
142+
{
143+
$params = new EncryptionConfig();
144+
$params->driver = 'OpenSSL';
145+
$params->key = 'original-key-value';
146+
147+
$encrypter = $this->encryption->initialize($params);
148+
149+
$this->assertSame('original-key-value', $encrypter->key);
150+
151+
$message = 'This is a plain-text message.';
152+
$differentKey = 'temporary-param-key';
153+
$encoded = $encrypter->encrypt($message, ['key' => $differentKey]);
154+
155+
$this->assertSame('original-key-value', $encrypter->key);
156+
157+
$message2 = 'Another message.';
158+
$encoded2 = $encrypter->encrypt($message2);
159+
$this->assertSame($message2, $encrypter->decrypt($encoded2));
160+
161+
$this->assertSame($message, $encrypter->decrypt($encoded, ['key' => $differentKey]));
162+
}
140163
}

tests/system/Encryption/Handlers/SodiumHandlerTest.php

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,22 @@ public function testInvalidBlockSizeThrowsErrorOnEncrypt(): void
7878
$encrypter->encrypt('Some message.');
7979
}
8080

81-
public function testEmptyKeyThrowsErrorOnDecrypt(): void
81+
public function testHandlerCanBeReusedAfterEncryption(): void
8282
{
83-
$this->expectException(EncryptionException::class);
83+
$encrypter = $this->encryption->initialize($this->config);
84+
$message = 'Some message to encrypt';
8485

85-
$encrypter = $this->encryption->initialize($this->config);
86-
$ciphertext = $encrypter->encrypt('Some message to encrypt');
87-
// After encrypt, the message and key are wiped from buffer
88-
$encrypter->decrypt($ciphertext);
86+
$ciphertext = $encrypter->encrypt($message);
87+
$plaintext = $encrypter->decrypt($ciphertext);
88+
89+
$this->assertSame($message, $plaintext);
90+
91+
// Should also work for another encryption
92+
$message2 = 'Another message';
93+
$ciphertext2 = $encrypter->encrypt($message2);
94+
$plaintext2 = $encrypter->decrypt($ciphertext2);
95+
96+
$this->assertSame($message2, $plaintext2);
8997
}
9098

9199
public function testInvalidBlockSizeThrowsErrorOnDecrypt(): void
@@ -121,4 +129,26 @@ public function testDecryptingMessages(): void
121129
$this->assertSame($msg, $encrypter->decrypt($ciphertext, $key));
122130
$this->assertNotSame('A plain-text message for you.', $encrypter->decrypt($ciphertext, $key));
123131
}
132+
133+
public function testInternalKeyNotModifiedByParams(): void
134+
{
135+
$originalKey = sodium_crypto_secretbox_keygen();
136+
137+
$this->config->key = $originalKey;
138+
$encrypter = $this->encryption->initialize($this->config);
139+
140+
$this->assertSame($originalKey, $encrypter->key);
141+
142+
$message = 'This is a plain-text message.';
143+
$differentKey = sodium_crypto_secretbox_keygen();
144+
$encoded = $encrypter->encrypt($message, ['key' => $differentKey]);
145+
146+
$this->assertSame($originalKey, $encrypter->key);
147+
148+
$message2 = 'Another message.';
149+
$encoded2 = $encrypter->encrypt($message2);
150+
$this->assertSame($message2, $encrypter->decrypt($encoded2, ['key' => $originalKey]));
151+
152+
$this->assertSame($message, $encrypter->decrypt($encoded, ['key' => $differentKey]));
153+
}
124154
}

user_guide_src/source/changelogs/v4.7.0.rst

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,61 @@ parameter is ``true``. Previously, properties containing arrays were not recursi
111111
If you were relying on the old behavior where arrays remained unconverted, you will need to update
112112
your code.
113113

114+
Encryption Handlers
115+
-------------------
116+
117+
The ``OpenSSLHandler`` and ``SodiumHandler`` no longer modify the handler's ``$key`` property
118+
when encryption/decryption parameters are passed via the ``$params`` argument. Keys passed through
119+
``$params`` are now used as local variables, ensuring the handler's state remains unchanged.
120+
121+
**What changed:**
122+
123+
- Previously, passing a key via ``$params`` to ``encrypt()`` or ``decrypt()`` would permanently
124+
modify the handler's internal ``$key`` property.
125+
- Now, the handler's ``$key`` property is only set during handler creation via ``Config\Encryption``.
126+
Passing keys through ``$params`` uses them as temporary local variables without modifying the handler's state.
127+
- ``SodiumHandler::encrypt()`` no longer calls ``sodium_memzero($this->key)``, which previously
128+
destroyed the encryption key after the first use, preventing handler reuse.
129+
130+
**Impact:**
131+
132+
**You are only affected if** you passed a key via ``$params`` to ``encrypt()`` or ``decrypt()``
133+
and expected that the ``key`` will persist for subsequent operations. Most users are **not affected**:
134+
135+
- **Not affected:** You always pass the key via ``$params`` for each operation
136+
- **Not affected:** You never use ``$params`` and always configure keys via ``Config\Encryption``
137+
- **Affected:** You passed a key via ``$params`` once and expected it to be remembered
138+
139+
If affected, configure the key properly via ``Config\Encryption`` or pass a custom config to the
140+
service instead of relying on ``$params`` side effects.
141+
142+
**Example of affected code:**
143+
144+
.. code-block:: php
145+
146+
$config = config('Encryption');
147+
$config->key = 'your-encryption-key';
148+
$handler = service('encrypter', $config);
149+
$handler->encrypt($data, 'temporary-key');
150+
// Old: $handler->key is now 'temporary-key'
151+
// New: $handler->key remains unchanged ('your-encryption-key')
152+
153+
$handler->encrypt($moreData);
154+
// Old: Would use 'temporary-key'
155+
// New: Uses default key ('your-encryption-key')
156+
157+
**Migration:**
158+
159+
To use a different encryption key permanently, pass a custom config when creating the service:
160+
161+
.. code-block:: php
162+
163+
$config = config('Encryption');
164+
$config->key = 'your-custom-encryption-key';
165+
166+
// Get a new handler instance with the custom config (not shared)
167+
$handler = service('encrypter', $config, false);
168+
114169
Interface Changes
115170
=================
116171

@@ -254,6 +309,8 @@ Changes
254309
Deprecations
255310
************
256311

312+
- **Encryption:**
313+
- The method ``CodeIgniter\Encryption\Handlers\SodiumHandler::parseParams()`` has been deprecated. Parameters are now handled directly in ``encrypt()`` and ``decrypt()`` methods.
257314
- **Image:**
258315
- The config property ``Config\Image::libraryPath`` has been deprecated. No longer used.
259316
- The exception method ``CodeIgniter\Images\Exceptions\ImageException::forInvalidImageLibraryPath`` has been deprecated. No longer used.

user_guide_src/source/libraries/encryption.rst

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,6 @@ sending secret messages in an end-to-end scenario. To encrypt and/or authenticat
223223
a shared-key, such as symmetric encryption, Sodium uses the XSalsa20 algorithm to encrypt and
224224
HMAC-SHA512 for the authentication.
225225

226-
.. note:: CodeIgniter's ``SodiumHandler`` uses ``sodium_memzero`` in every encryption or decryption
227-
session. After each session, the message (whether plaintext or ciphertext) and starter key are
228-
wiped out from the buffers. You may need to provide again the key before starting a new session.
229-
230226
Message Length
231227
==============
232228

utils/phpstan-baseline/loader.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# total 2637 errors
1+
# total 2639 errors
22

33
includes:
44
- argument.type.neon

utils/phpstan-baseline/property.notFound.neon

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# total 45 errors
1+
# total 47 errors
22

33
parameters:
44
ignoreErrors:
@@ -74,7 +74,7 @@ parameters:
7474

7575
-
7676
message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$key\.$#'
77-
count: 2
77+
count: 3
7878
path: ../../tests/system/Encryption/Handlers/OpenSSLHandlerTest.php
7979

8080
-
@@ -89,7 +89,7 @@ parameters:
8989

9090
-
9191
message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$key\.$#'
92-
count: 1
92+
count: 2
9393
path: ../../tests/system/Encryption/Handlers/SodiumHandlerTest.php
9494

9595
-

0 commit comments

Comments
 (0)