Skip to content

Commit 5f63c3f

Browse files
committed
refactor: added tryDecryptWithFallback method in BaseHandler
1 parent e1331e2 commit 5f63c3f

3 files changed

Lines changed: 119 additions & 169 deletions

File tree

system/Encryption/Handlers/BaseHandler.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,22 @@
1414
namespace CodeIgniter\Encryption\Handlers;
1515

1616
use CodeIgniter\Encryption\EncrypterInterface;
17+
use CodeIgniter\Encryption\Exceptions\EncryptionException;
1718
use Config\Encryption;
19+
use SensitiveParameter;
1820

1921
/**
2022
* Base class for encryption handling
2123
*/
2224
abstract class BaseHandler implements EncrypterInterface
2325
{
26+
/**
27+
* List of previous keys for fallback decryption.
28+
*
29+
* @var list<string>
30+
*/
31+
protected array $previousKeys = [];
32+
2433
/**
2534
* Constructor
2635
*/
@@ -50,6 +59,47 @@ protected static function substr($str, $start, $length = null)
5059
return mb_substr($str, $start, $length, '8bit');
5160
}
5261

62+
/**
63+
* Attempts to decrypt using the provided callback, and if it fails,
64+
* tries again with any previous keys we may have.
65+
*
66+
* @param string $data Data to decrypt
67+
* @param array|string|null $params Decryption parameters
68+
* @param callable $decryptCallback Callback that performs decryption
69+
*
70+
* @return string Decrypted data
71+
*
72+
* @throws EncryptionException
73+
*/
74+
protected function tryDecryptWithFallback($data, #[SensitiveParameter] $params, callable $decryptCallback)
75+
{
76+
try {
77+
return $decryptCallback($data, $params);
78+
} catch (EncryptionException $e) {
79+
if ($this->previousKeys === []) {
80+
throw $e;
81+
}
82+
83+
if (is_string($params) || (is_array($params) && isset($params['key']))) {
84+
throw $e;
85+
}
86+
87+
foreach ($this->previousKeys as $previousKey) {
88+
try {
89+
$previousParams = is_array($params)
90+
? array_merge($params, ['key' => $previousKey])
91+
: $previousKey;
92+
93+
return $decryptCallback($data, $previousParams);
94+
} catch (EncryptionException) {
95+
continue;
96+
}
97+
}
98+
99+
throw $e;
100+
}
101+
}
102+
53103
/**
54104
* __get() magic, providing readonly access to some of our properties
55105
*

system/Encryption/Handlers/OpenSSLHandler.php

Lines changed: 44 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@
2323
*/
2424
class OpenSSLHandler extends BaseHandler
2525
{
26+
/**
27+
* Encryption key info.
28+
* This setting is only used by OpenSSLHandler.
29+
*
30+
* Set to 'encryption' for CI3 Encryption compatibility.
31+
*/
32+
public string $encryptKeyInfo = '';
33+
34+
/**
35+
* Authentication key info.
36+
* This setting is only used by OpenSSLHandler.
37+
*
38+
* Set to 'authentication' for CI3 Encryption compatibility.
39+
*/
40+
public string $authKeyInfo = '';
41+
2642
/**
2743
* HMAC digest to use
2844
*
@@ -56,34 +72,11 @@ class OpenSSLHandler extends BaseHandler
5672
*/
5773
protected $key = '';
5874

59-
/**
60-
* List of previous keys for fallback decryption.
61-
*
62-
* @var list<string>|string
63-
*/
64-
protected array|string $previousKeys = '';
65-
6675
/**
6776
* Whether the cipher-text should be raw. If set to false, then it will be base64 encoded.
6877
*/
6978
protected bool $rawData = true;
7079

71-
/**
72-
* Encryption key info.
73-
* This setting is only used by OpenSSLHandler.
74-
*
75-
* Set to 'encryption' for CI3 Encryption compatibility.
76-
*/
77-
public string $encryptKeyInfo = '';
78-
79-
/**
80-
* Authentication key info.
81-
* This setting is only used by OpenSSLHandler.
82-
*
83-
* Set to 'authentication' for CI3 Encryption compatibility.
84-
*/
85-
public string $authKeyInfo = '';
86-
8780
/**
8881
* {@inheritDoc}
8982
*/
@@ -125,90 +118,48 @@ public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $para
125118
*/
126119
public function decrypt($data, #[SensitiveParameter] $params = null)
127120
{
128-
// Allow key override
129-
if ($params !== null) {
130-
$this->key = is_array($params) && isset($params['key']) ? $params['key'] : $params;
131-
}
121+
$decryptParams = $params ?? $this->key;
132122

133-
if (empty($this->key)) {
123+
if (empty($decryptParams)) {
134124
throw EncryptionException::forNeedsStarterKey();
135125
}
136126

137-
// Only use fallback keys if no custom key was provided in params
138-
$useFallback = ! isset($params['key']);
139-
140-
$attemptDecrypt = function ($key) use ($data): array {
141-
try {
142-
$result = $this->decryptWithKey($data, $key);
143-
144-
return ['success' => true, 'data' => $result];
145-
} catch (EncryptionException $e) {
146-
return ['success' => false, 'exception' => $e];
147-
}
148-
};
149-
150-
$result = $attemptDecrypt($this->key);
127+
return $this->tryDecryptWithFallback($data, $decryptParams, function ($data, $params): string {
128+
$key = is_array($params) && isset($params['key']) ? $params['key'] : $params;
151129

152-
if ($result['success']) {
153-
return $result['data'];
154-
}
130+
$authKey = \hash_hkdf($this->digest, $key, 0, $this->authKeyInfo);
155131

156-
$originalException = $result['exception'];
132+
$hmacLength = $this->rawData
133+
? $this->digestSize[$this->digest]
134+
: $this->digestSize[$this->digest] * 2;
157135

158-
// If primary key failed and fallback is allowed, try previous keys
159-
if ($useFallback && ! in_array($this->previousKeys, ['', '0', []], true)) {
160-
foreach ($this->previousKeys as $previousKey) {
161-
$fallbackResult = $attemptDecrypt($previousKey);
136+
$hmacKey = self::substr($data, 0, $hmacLength);
137+
$data = self::substr($data, $hmacLength);
138+
$hmacCalc = \hash_hmac($this->digest, $data, $authKey, $this->rawData);
162139

163-
if ($fallbackResult['success']) {
164-
return $fallbackResult['data'];
165-
}
140+
if (! hash_equals($hmacKey, $hmacCalc)) {
141+
throw EncryptionException::forAuthenticationFailed();
166142
}
167-
}
168143

169-
// All attempts failed - throw the original exception
170-
throw $originalException;
171-
}
144+
$data = $this->rawData ? $data : base64_decode($data, true);
172145

173-
/**
174-
* Decrypt the data with the provided key
175-
*
176-
* @param string $data
177-
* @param string $key
178-
*
179-
* @return false|string
180-
*
181-
* @throws EncryptionException
182-
*/
183-
protected function decryptWithKey($data, #[SensitiveParameter] $key)
184-
{
185-
// derive a secret key
186-
$authKey = \hash_hkdf($this->digest, $key, 0, $this->authKeyInfo);
187-
188-
$hmacLength = $this->rawData
189-
? $this->digestSize[$this->digest]
190-
: $this->digestSize[$this->digest] * 2;
191-
192-
$hmacKey = self::substr($data, 0, $hmacLength);
193-
$data = self::substr($data, $hmacLength);
194-
$hmacCalc = \hash_hmac($this->digest, $data, $authKey, $this->rawData);
146+
if ($ivSize = \openssl_cipher_iv_length($this->cipher)) {
147+
$iv = self::substr($data, 0, $ivSize);
148+
$data = self::substr($data, $ivSize);
149+
} else {
150+
$iv = null;
151+
}
195152

196-
if (! hash_equals($hmacKey, $hmacCalc)) {
197-
throw EncryptionException::forAuthenticationFailed();
198-
}
153+
// derive a secret key
154+
$encryptKey = \hash_hkdf($this->digest, $key, 0, $this->encryptKeyInfo);
199155

200-
$data = $this->rawData ? $data : base64_decode($data, true);
156+
$decryptedData = \openssl_decrypt($data, $this->cipher, $encryptKey, OPENSSL_RAW_DATA, $iv);
201157

202-
if ($ivSize = \openssl_cipher_iv_length($this->cipher)) {
203-
$iv = self::substr($data, 0, $ivSize);
204-
$data = self::substr($data, $ivSize);
205-
} else {
206-
$iv = null;
207-
}
208-
209-
// derive a secret key
210-
$encryptKey = \hash_hkdf($this->digest, $key, 0, $this->encryptKeyInfo);
158+
if ($decryptedData === false) {
159+
throw EncryptionException::forDecryptionFailed();
160+
}
211161

212-
return \openssl_decrypt($data, $this->cipher, $encryptKey, OPENSSL_RAW_DATA, $iv);
162+
return $decryptedData;
163+
});
213164
}
214165
}

system/Encryption/Handlers/SodiumHandler.php

Lines changed: 25 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ class SodiumHandler extends BaseHandler
3131
*/
3232
protected $key = '';
3333

34-
/**
35-
* List of previous keys for fallback decryption.
36-
*
37-
* @var list<string>|string
38-
*/
39-
protected array|string $previousKeys = '';
40-
4134
/**
4235
* Block size for padding message.
4336
*
@@ -83,88 +76,44 @@ public function decrypt($data, #[SensitiveParameter] $params = null)
8376
{
8477
$this->parseParams($params);
8578

86-
if (empty($this->key)) {
79+
$decryptParams = $params ?? $this->key;
80+
81+
if (empty($decryptParams)) {
8782
throw EncryptionException::forNeedsStarterKey();
8883
}
8984

90-
// Only use fallback keys if no custom key was provided in params
91-
$useFallback = ! isset($params['key']);
92-
93-
$attemptDecrypt = function ($key) use ($data): array {
94-
try {
95-
$result = $this->decryptWithKey($data, $key);
96-
sodium_memzero($key);
97-
98-
return ['success' => true, 'data' => $result];
99-
} catch (EncryptionException $e) {
100-
sodium_memzero($key);
85+
return $this->tryDecryptWithFallback($data, $decryptParams, function ($data, $params): string {
86+
$key = is_array($params) && isset($params['key']) ? $params['key'] : $params;
10187

102-
return ['success' => false, 'exception' => $e];
88+
if (mb_strlen($data, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)) {
89+
// message was truncated
90+
throw EncryptionException::forAuthenticationFailed();
10391
}
104-
};
10592

106-
$result = $attemptDecrypt($this->key);
93+
// Extract info from encrypted data
94+
$nonce = self::substr($data, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
95+
$ciphertext = self::substr($data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
10796

108-
if ($result['success']) {
109-
return $result['data'];
110-
}
111-
112-
$originalException = $result['exception'];
97+
// decrypt data
98+
$data = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
11399

114-
// If primary key failed and fallback is allowed, try previous keys
115-
if ($useFallback && ! in_array($this->previousKeys, ['', '0', []], true)) {
116-
foreach ($this->previousKeys as $previousKey) {
117-
$fallbackResult = $attemptDecrypt($previousKey);
118-
119-
if ($fallbackResult['success']) {
120-
return $fallbackResult['data'];
121-
}
100+
if ($data === false) {
101+
// message was tampered in transit
102+
throw EncryptionException::forAuthenticationFailed(); // @codeCoverageIgnore
122103
}
123-
}
124-
125-
throw $originalException;
126-
}
127-
128-
/**
129-
* Decrypt the data with the provided key
130-
*
131-
* @param string $data
132-
* @param string $key
133-
*
134-
* @return string
135-
*
136-
* @throws EncryptionException
137-
*/
138-
protected function decryptWithKey($data, #[SensitiveParameter] $key)
139-
{
140-
if (mb_strlen($data, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)) {
141-
// message was truncated
142-
throw EncryptionException::forAuthenticationFailed();
143-
}
144-
145-
// Extract info from encrypted data
146-
$nonce = self::substr($data, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
147-
$ciphertext = self::substr($data, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
148104

149-
// decrypt data
150-
$data = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
151-
152-
if ($data === false) {
153-
// message was tampered in transit
154-
throw EncryptionException::forAuthenticationFailed(); // @codeCoverageIgnore
155-
}
156-
157-
// remove extra padding during encryption
158-
if ($this->blockSize <= 0) {
159-
throw EncryptionException::forAuthenticationFailed();
160-
}
105+
// remove extra padding during encryption
106+
if ($this->blockSize <= 0) {
107+
throw EncryptionException::forAuthenticationFailed();
108+
}
161109

162-
$data = sodium_unpad($data, $this->blockSize);
110+
$data = sodium_unpad($data, $this->blockSize);
163111

164-
// cleanup buffers
165-
sodium_memzero($ciphertext);
112+
// cleanup buffers
113+
sodium_memzero($ciphertext);
166114

167-
return $data;
115+
return $data;
116+
});
168117
}
169118

170119
/**

0 commit comments

Comments
 (0)