Skip to content

Commit f77c6ae

Browse files
authored
Merge pull request from GHSA-v427-c49j-8w6x
fix: Cleartext Storage of Sensitive Information in HMAC SHA256 Authentication
2 parents 7d41423 + 363183d commit f77c6ae

File tree

15 files changed

+818
-40
lines changed

15 files changed

+818
-40
lines changed

UPGRADING.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,28 @@ protected function redirectToDeniedUrl(): RedirectResponse
4545
{
4646
return redirect()->to(config('Auth')->groupDeniedRedirect())
4747
->with('error', lang('Auth.notEnoughPrivilege'));
48-
}
48+
}
4949
```
5050

51+
### Fix to HMAC Secret Key Encryption
52+
53+
#### Config\AuthToken
54+
55+
If you are using the HMAC authentication you need to update the encryption settings in **app/Config/AuthToken.php**.
56+
You will need to update and set the encryption key in `$hmacEncryptionKeys`. This should be set using **.env** and/or
57+
system environment variables. Instructions on how to do that can be found in the
58+
[Setting Your Encryption Key](https://codeigniter.com/user_guide/libraries/encryption.html#setting-your-encryption-key)
59+
section of the CodeIgniter 4 documentation and in [HMAC SHA256 Token Authenticator](./docs/references/authentication/hmac.md#hmac-secret-key-encryption).
60+
61+
You also may wish to adjust the default Driver `$hmacEncryptionDefaultDriver` and the default Digest
62+
`$hmacEncryptionDefaultDigest`, these currently default to `'OpenSSL'` and `'SHA512'` respectively.
63+
64+
#### Encrypt Existing Keys
65+
66+
After updating the key in `$hmacEncryptionKeys` value, you will need to run `php spark shield:hmac encrypt` in order
67+
to encrypt any existing HMAC tokens. This only needs to be run if you have existing unencrypted HMAC secretKeys in
68+
stored in the database.
69+
5170
## Version 1.0.0-beta.6 to 1.0.0-beta.7
5271

5372
### The minimum CodeIgniter version

docs/guides/api_hmac_keys.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ API. When making requests using HMAC keys, the token should be included in the `
1515
setting the `$authenticatorHeader['hmac']` value in the **app/Config/AuthToken.php** config file.
1616

1717
Tokens are issued with the `generateHmacToken()` method on the user. This returns a
18-
`CodeIgniter\Shield\Entities\AccessToken` instance. These shared keys are saved to the database in plain text. The
19-
`AccessToken` object returned when you generate it will include a `secret` field which will be the `key` and a `secret2`
20-
field that will be the `secretKey`. You should display the `secretKey` to your user once, so they have a chance to copy
21-
it somewhere safe, as this is the only time you should reveal this key.
18+
`CodeIgniter\Shield\Entities\AccessToken` instance. The `AccessToken` object returned will include a `secret` field
19+
which will be the '**key**' and a `rawSecretKey` field that will be the '**secretKey**'. You should display the
20+
'**secretKey**' to your user immediately, so they have a chance to copy it somewhere safe, as this is the only time
21+
you can reveal this key. The '**key**' and '**secretKey**' are saved to the database. The '**secretKey**' is stored
22+
encrypted.
2223

2324
The `generateHmacToken()` method requires a name for the token. These are free strings and are often used to identify
2425
the user/device the token was generated from/for, like 'Johns MacBook Air'.
@@ -27,7 +28,7 @@ the user/device the token was generated from/for, like 'Johns MacBook Air'.
2728
$routes->get('hmac/token', static function () {
2829
$token = auth()->user()->generateHmacToken(service('request')->getVar('token_name'));
2930

30-
return json_encode(['key' => $token->secret, 'secretKey' => $token->secret2]);
31+
return json_encode(['key' => $token->secret, 'secretKey' => $token->rawSecretKey]);
3132
});
3233
```
3334

@@ -62,7 +63,7 @@ token is granted all access to all scopes. This might be enough for a smaller AP
6263

6364
```php
6465
$token = $user->generateHmacToken('token-name', ['users-read']);
65-
return json_encode(['key' => $token->secret, 'secretKey' => $token->secret2]);
66+
return json_encode(['key' => $token->secret, 'secretKey' => $token->rawSecretKey]);
6667
```
6768

6869
!!! note
@@ -87,6 +88,25 @@ $user->revokeHmacToken($key);
8788
$user->revokeAllHmacTokens();
8889
```
8990

91+
## HMAC Secret Key Encryption
92+
93+
The HMAC Secret Key is stored encrypted. Before you start using HMAC, you will need to set/override the encryption key
94+
in `$hmacEncryptionKeys` in **app/Config/AuthToken.php**. This should be set using **.env** and/or system
95+
environment variables. Instructions on how to do that can be found in the
96+
[Setting Your Encryption Key](https://codeigniter.com/user_guide/libraries/encryption.html#setting-your-encryption-key)
97+
section of the CodeIgniter 4 documentation.
98+
99+
You will also be able to adjust the default Driver `$hmacEncryptionDefaultDriver` and the default Digest
100+
`$hmacEncryptionDefaultDigest`, these default to `'OpenSSL'` and `'SHA512'` respectively.
101+
102+
See [HMAC SHA256 Token Authenticator](../references/authentication/hmac.md#hmac-secret-key-encryption) for additional
103+
details on setting these values.
104+
105+
### Encryption Key Rotation
106+
107+
See [HMAC SHA256 Token Authenticator](../references/authentication/hmac.md#hmac-secret-key-encryption) for information on
108+
how to set, rotate encryption keys and re-encrypt existing HMAC `'secretKey'` values.
109+
90110
## Protecting Routes
91111

92112
The first way to specify which routes are protected is to use the `hmac` controller filter.

docs/references/authentication/hmac.md

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ access to your API. These keys typically have a very long expiration time, often
77

88
These are also suitable for use with mobile applications. In this case, the user would register/sign-in
99
with their email/password. The application would create a new access token for them, with a recognizable
10-
name, like John's iPhone 12, and return it to the mobile application, where it is stored and used
10+
name, like "John's iPhone 12", and return it to the mobile application, where it is stored and used
1111
in all future requests.
1212

1313
!!! note
@@ -67,19 +67,19 @@ $token = $user->generateHmacToken('Work Laptop');
6767
```
6868

6969
This creates the keys/tokens using a cryptographically secure random string. The keys operate as shared keys.
70-
This means they are stored as-is in the database. The method returns an instance of
71-
`CodeIgniters\Shield\Authentication\Entities\AccessToken`. The field `secret` is the 'key' the field `secret2` is
72-
the shared 'secretKey'. Both are required to when using this authentication method.
70+
The '**key**' is stored as plain text in the database, the '**secretKey**' is stored encrypted. The method returns an
71+
instance of `CodeIgniters\Shield\Authentication\Entities\AccessToken`. The field `secret` is the '**key**' the field
72+
`rawSecretKey` is the shared '**secretKey**'. Both are required to when using this authentication method.
7373

7474
**The plain text version of these keys should be displayed to the user immediately, so they can copy it for
75-
their use.** It is recommended that after that only the 'key' field is displayed to a user. If a user loses the
76-
'secretKey', they should be required to generate a new set of keys to use.
75+
their use.** It is recommended that after that only the '**key**' field is displayed to a user. If a user loses the
76+
'**secretKey**', they should be required to generate a new set of keys to use.
7777

7878
```php
7979
$token = $user->generateHmacToken('Work Laptop');
8080

8181
echo 'Key: ' . $token->secret;
82-
echo 'SecretKey: ' . $token->secret2;
82+
echo 'SecretKey: ' . $token->rawSecretKey;
8383
```
8484

8585
## Revoking HMAC Keys
@@ -156,3 +156,66 @@ if ($user->hmacTokenCant('forums.manage')) {
156156
// do something....
157157
}
158158
```
159+
160+
## HMAC Secret Key Encryption
161+
162+
The HMAC Secret Key is stored encrypted. Before you start using HMAC, you will need to set/override the encryption key
163+
in `$hmacEncryptionKeys` in **app/Config/AuthToken.php**. This should be set using **.env** and/or system
164+
environment variables. Instructions on how to do that can be found in the
165+
[Setting Your Encryption Key](https://codeigniter.com/user_guide/libraries/encryption.html#setting-your-encryption-key)
166+
section of the CodeIgniter 4 documentation.
167+
168+
You will also be able to adjust the default Driver `$hmacEncryptionDefaultDriver` and the default Digest
169+
`$hmacEncryptionDefaultDigest`, these default to `'OpenSSL'` and `'SHA512'` respectively. These can also be
170+
overridden for an individual key by including them in the keys array.
171+
172+
```php
173+
public $hmacEncryptionKeys = [
174+
'k1' => [
175+
'key' => 'hex2bin:923dfab5ddca0c7784c2c388a848a704f5e048736c1a852c862959da62ade8c7',
176+
],
177+
];
178+
179+
public string $hmacEncryptionCurrentKey = 'k1';
180+
public string $hmacEncryptionDefaultDriver = 'OpenSSL';
181+
public string $hmacEncryptionDefaultDigest = 'SHA512';
182+
```
183+
184+
When it is time to update your encryption keys you will need to add an additional key to the above
185+
`$hmacEncryptionKeys` array. Then adjust the `$hmacEncryptionCurrentKey` to point at the new key. After the new
186+
encryption key is in place, run `php spark shield:hmac reencrypt` to re-encrypt all existing keys with the new
187+
encryption key. You will need to leave the old key in the array as it will be used read the existing 'Secret Keys'
188+
during re-encryption.
189+
190+
```php
191+
public $hmacEncryptionKeys = [
192+
'k1' => [
193+
'key' => 'hex2bin:923dfab5ddca0c7784c2c388a848a704f5e048736c1a852c862959da62ade8c7',
194+
],
195+
'k2' => [
196+
'key' => 'hex2bin:451df599363b19be1434605fff8556a0bbfc50bede1bb33793dcde4d97fce4b0',
197+
'digest' => 'SHA256',
198+
],
199+
];
200+
201+
public string $hmacEncryptionCurrentKey = 'k2';
202+
public string $hmacEncryptionDefaultDriver = 'OpenSSL';
203+
public string $hmacEncryptionDefaultDigest = 'SHA512';
204+
205+
```
206+
207+
```console
208+
php spark shield:hmac reencrypt
209+
```
210+
211+
You can (and should) set these values using environment variable and/or the **.env** file. To do this you will need to set
212+
the values as JSON strings:
213+
214+
```text
215+
authtoken.hmacEncryptionKeys = '{"k1":{"key":"hex2bin:923dfab5ddca0c7784c2c388a848a704f5e048736c1a852c862959da62ade8c7"},"k2":{"key":"hex2bin:451df599363b19be1434605fff8556a0bbfc50bede1bb33793dcde4d97fce4b0"}}'
216+
authtoken.hmacEncryptionCurrentKey = k2
217+
```
218+
219+
Depending on the set length of the Secret Key and the type of encryption used, it is possible for the encrypted value to
220+
exceed the database column character limit of 255 characters. If this happens, creation of a new HMAC identity will
221+
throw a `RuntimeException`.

phpunit.xml.dist

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@
9393
<!-- https://getcomposer.org/xdebug -->
9494
<env name="COMPOSER_DISABLE_XDEBUG_WARN" value="1"/>
9595

96-
<!-- Database configuration -->
96+
<!-- Default HMAC encryption key -->
97+
<env name="authtoken.hmacEncryptionKeys" value="{&quot;k1&quot;:{&quot;key&quot;:&quot;hex2bin:178ed94fd0b6d57dd31dd6b22fc601fab8ad191efac165a5f3f30a8ac09d813d&quot;},&quot;k2&quot;:{&quot;key&quot;:&quot;hex2bin:b0ab85bd0320824c496db2f40eb47c8712a6dfcfdf99b805988e22bdea6b9203&quot;}}"/>
98+
99+
<!-- Database configuration -->
97100
<env name="database.tests.strictOn" value="true"/>
98101
<!-- Uncomment to use alternate testing database configuration
99102
<env name="database.tests.hostname" value="localhost"/>

src/Authentication/Authenticators/HmacSha256.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use CodeIgniter\I18n\Time;
1818
use CodeIgniter\Shield\Authentication\AuthenticationException;
1919
use CodeIgniter\Shield\Authentication\AuthenticatorInterface;
20+
use CodeIgniter\Shield\Authentication\HMAC\HmacEncrypter;
2021
use CodeIgniter\Shield\Config\Auth;
2122
use CodeIgniter\Shield\Entities\User;
2223
use CodeIgniter\Shield\Exceptions\InvalidArgumentException;
@@ -159,8 +160,11 @@ public function check(array $credentials): Result
159160
]);
160161
}
161162

163+
$encrypter = new HmacEncrypter();
164+
$secretKey = $encrypter->decrypt($token->secret2);
165+
162166
// Check signature...
163-
$hash = hash_hmac('sha256', $credentials['body'], $token->secret2);
167+
$hash = hash_hmac('sha256', $credentials['body'], $secretKey);
164168
if ($hash !== $signature) {
165169
return new Result([
166170
'success' => false,
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter Shield.
7+
*
8+
* (c) CodeIgniter Foundation <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\Shield\Authentication\HMAC;
15+
16+
use CodeIgniter\Encryption\EncrypterInterface;
17+
use CodeIgniter\Encryption\Exceptions\EncryptionException;
18+
use CodeIgniter\Shield\Auth;
19+
use CodeIgniter\Shield\Config\AuthToken;
20+
use CodeIgniter\Shield\Exceptions\RuntimeException;
21+
use Config\Encryption;
22+
use Config\Services;
23+
use Exception;
24+
25+
/**
26+
* HMAC Encrypter class
27+
*
28+
* This class handles the setup and configuration of the HMAC Encryption
29+
*/
30+
class HmacEncrypter
31+
{
32+
/**
33+
* Codeigniter Encrypter
34+
*
35+
* @var array<string, EncrypterInterface>
36+
*/
37+
private array $encrypter;
38+
39+
/**
40+
* Auth Token config
41+
*/
42+
private AuthToken $authConfig;
43+
44+
/**
45+
* Constructor
46+
* Setup encryption configuration
47+
*/
48+
public function __construct()
49+
{
50+
$this->authConfig = config('AuthToken');
51+
52+
$this->getEncrypter($this->authConfig->hmacEncryptionCurrentKey);
53+
}
54+
55+
/**
56+
* Decrypt
57+
*
58+
* @param string $encString Encrypted string
59+
*
60+
* @return string Raw decrypted string
61+
*
62+
* @throws EncryptionException
63+
*/
64+
public function decrypt(string $encString): string
65+
{
66+
$matches = [];
67+
// check for a match
68+
if (preg_match('/^\$b6\$(\w+?)\$(.+)\z/', $encString, $matches) !== 1) {
69+
throw new EncryptionException('Unable to decrypt string');
70+
}
71+
72+
$encrypter = $this->getEncrypter($matches[1]);
73+
74+
return $encrypter->decrypt(base64_decode($matches[2], true));
75+
}
76+
77+
/**
78+
* Encrypt
79+
*
80+
* @param string $rawString Raw string to encrypt
81+
*
82+
* @return string Encrypted string
83+
*
84+
* @throws EncryptionException
85+
* @throws RuntimeException
86+
*/
87+
public function encrypt(string $rawString): string
88+
{
89+
$currentKey = $this->authConfig->hmacEncryptionCurrentKey;
90+
91+
$encryptedString = '$b6$' . $currentKey . '$' . base64_encode($this->encrypter[$currentKey]->encrypt($rawString));
92+
93+
if (strlen($encryptedString) > $this->authConfig->secret2StorageLimit) {
94+
throw new RuntimeException('Encrypted key too long. Unable to store value.');
95+
}
96+
97+
return $encryptedString;
98+
}
99+
100+
/**
101+
* Check if the string already encrypted
102+
*/
103+
public function isEncrypted(string $string): bool
104+
{
105+
return preg_match('/^\$b6\$/', $string) === 1;
106+
}
107+
108+
/**
109+
* Check if the string already encrypted with the Current Set Key
110+
*/
111+
public function isEncryptedWithCurrentKey(string $string): bool
112+
{
113+
$currentKey = $this->authConfig->hmacEncryptionCurrentKey;
114+
115+
return preg_match('/^\$b6\$' . $currentKey . '\$/', $string) === 1;
116+
}
117+
118+
/**
119+
* Generate Key
120+
*
121+
* @return string Secret Key in base64 format
122+
*
123+
* @throws Exception
124+
*/
125+
public function generateSecretKey(): string
126+
{
127+
return base64_encode(random_bytes($this->authConfig->hmacSecretKeyByteSize));
128+
}
129+
130+
/**
131+
* Retrieve encrypter for selected key
132+
*
133+
* @param string $encrypterKey Index Key for selected Encrypter
134+
*/
135+
private function getEncrypter(string $encrypterKey): EncrypterInterface
136+
{
137+
if (! isset($this->encrypter[$encrypterKey])) {
138+
if (! isset($this->authConfig->hmacEncryptionKeys[$encrypterKey]['key'])) {
139+
throw new RuntimeException('Encryption key does not exist.');
140+
}
141+
142+
$config = new Encryption();
143+
144+
$config->key = $this->authConfig->hmacEncryptionKeys[$encrypterKey]['key'];
145+
$config->driver = $this->authConfig->hmacEncryptionKeys[$encrypterKey]['driver'] ?? $this->authConfig->hmacEncryptionDefaultDriver;
146+
$config->digest = $this->authConfig->hmacEncryptionKeys[$encrypterKey]['digest'] ?? $this->authConfig->hmacEncryptionDefaultDigest;
147+
148+
$this->encrypter[$encrypterKey] = Services::encrypter($config);
149+
}
150+
151+
return $this->encrypter[$encrypterKey];
152+
}
153+
}

0 commit comments

Comments
 (0)