Skip to content

Commit

Permalink
Merge pull request from GHSA-v427-c49j-8w6x
Browse files Browse the repository at this point in the history
fix: Cleartext Storage of Sensitive Information in HMAC SHA256 Authentication
  • Loading branch information
kenjis authored Nov 22, 2023
2 parents 7d41423 + 363183d commit f77c6ae
Show file tree
Hide file tree
Showing 15 changed files with 818 additions and 40 deletions.
21 changes: 20 additions & 1 deletion UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,28 @@ protected function redirectToDeniedUrl(): RedirectResponse
{
return redirect()->to(config('Auth')->groupDeniedRedirect())
->with('error', lang('Auth.notEnoughPrivilege'));
}
}
```

### Fix to HMAC Secret Key Encryption

#### Config\AuthToken

If you are using the HMAC authentication you need to update the encryption settings in **app/Config/AuthToken.php**.
You will need to update and set the encryption key in `$hmacEncryptionKeys`. This should be set using **.env** and/or
system environment variables. Instructions on how to do that can be found in the
[Setting Your Encryption Key](https://codeigniter.com/user_guide/libraries/encryption.html#setting-your-encryption-key)
section of the CodeIgniter 4 documentation and in [HMAC SHA256 Token Authenticator](./docs/references/authentication/hmac.md#hmac-secret-key-encryption).

You also may wish to adjust the default Driver `$hmacEncryptionDefaultDriver` and the default Digest
`$hmacEncryptionDefaultDigest`, these currently default to `'OpenSSL'` and `'SHA512'` respectively.

#### Encrypt Existing Keys

After updating the key in `$hmacEncryptionKeys` value, you will need to run `php spark shield:hmac encrypt` in order
to encrypt any existing HMAC tokens. This only needs to be run if you have existing unencrypted HMAC secretKeys in
stored in the database.

## Version 1.0.0-beta.6 to 1.0.0-beta.7

### The minimum CodeIgniter version
Expand Down
32 changes: 26 additions & 6 deletions docs/guides/api_hmac_keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ API. When making requests using HMAC keys, the token should be included in the `
setting the `$authenticatorHeader['hmac']` value in the **app/Config/AuthToken.php** config file.

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

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

return json_encode(['key' => $token->secret, 'secretKey' => $token->secret2]);
return json_encode(['key' => $token->secret, 'secretKey' => $token->rawSecretKey]);
});
```

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

```php
$token = $user->generateHmacToken('token-name', ['users-read']);
return json_encode(['key' => $token->secret, 'secretKey' => $token->secret2]);
return json_encode(['key' => $token->secret, 'secretKey' => $token->rawSecretKey]);
```

!!! note
Expand All @@ -87,6 +88,25 @@ $user->revokeHmacToken($key);
$user->revokeAllHmacTokens();
```

## HMAC Secret Key Encryption

The HMAC Secret Key is stored encrypted. Before you start using HMAC, you will need to set/override the encryption key
in `$hmacEncryptionKeys` in **app/Config/AuthToken.php**. This should be set using **.env** and/or system
environment variables. Instructions on how to do that can be found in the
[Setting Your Encryption Key](https://codeigniter.com/user_guide/libraries/encryption.html#setting-your-encryption-key)
section of the CodeIgniter 4 documentation.

You will also be able to adjust the default Driver `$hmacEncryptionDefaultDriver` and the default Digest
`$hmacEncryptionDefaultDigest`, these default to `'OpenSSL'` and `'SHA512'` respectively.

See [HMAC SHA256 Token Authenticator](../references/authentication/hmac.md#hmac-secret-key-encryption) for additional
details on setting these values.

### Encryption Key Rotation

See [HMAC SHA256 Token Authenticator](../references/authentication/hmac.md#hmac-secret-key-encryption) for information on
how to set, rotate encryption keys and re-encrypt existing HMAC `'secretKey'` values.

## Protecting Routes

The first way to specify which routes are protected is to use the `hmac` controller filter.
Expand Down
77 changes: 70 additions & 7 deletions docs/references/authentication/hmac.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ access to your API. These keys typically have a very long expiration time, often

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

!!! note
Expand Down Expand Up @@ -67,19 +67,19 @@ $token = $user->generateHmacToken('Work Laptop');
```

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

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

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

echo 'Key: ' . $token->secret;
echo 'SecretKey: ' . $token->secret2;
echo 'SecretKey: ' . $token->rawSecretKey;
```

## Revoking HMAC Keys
Expand Down Expand Up @@ -156,3 +156,66 @@ if ($user->hmacTokenCant('forums.manage')) {
// do something....
}
```

## HMAC Secret Key Encryption

The HMAC Secret Key is stored encrypted. Before you start using HMAC, you will need to set/override the encryption key
in `$hmacEncryptionKeys` in **app/Config/AuthToken.php**. This should be set using **.env** and/or system
environment variables. Instructions on how to do that can be found in the
[Setting Your Encryption Key](https://codeigniter.com/user_guide/libraries/encryption.html#setting-your-encryption-key)
section of the CodeIgniter 4 documentation.

You will also be able to adjust the default Driver `$hmacEncryptionDefaultDriver` and the default Digest
`$hmacEncryptionDefaultDigest`, these default to `'OpenSSL'` and `'SHA512'` respectively. These can also be
overridden for an individual key by including them in the keys array.

```php
public $hmacEncryptionKeys = [
'k1' => [
'key' => 'hex2bin:923dfab5ddca0c7784c2c388a848a704f5e048736c1a852c862959da62ade8c7',
],
];

public string $hmacEncryptionCurrentKey = 'k1';
public string $hmacEncryptionDefaultDriver = 'OpenSSL';
public string $hmacEncryptionDefaultDigest = 'SHA512';
```

When it is time to update your encryption keys you will need to add an additional key to the above
`$hmacEncryptionKeys` array. Then adjust the `$hmacEncryptionCurrentKey` to point at the new key. After the new
encryption key is in place, run `php spark shield:hmac reencrypt` to re-encrypt all existing keys with the new
encryption key. You will need to leave the old key in the array as it will be used read the existing 'Secret Keys'
during re-encryption.

```php
public $hmacEncryptionKeys = [
'k1' => [
'key' => 'hex2bin:923dfab5ddca0c7784c2c388a848a704f5e048736c1a852c862959da62ade8c7',
],
'k2' => [
'key' => 'hex2bin:451df599363b19be1434605fff8556a0bbfc50bede1bb33793dcde4d97fce4b0',
'digest' => 'SHA256',
],
];

public string $hmacEncryptionCurrentKey = 'k2';
public string $hmacEncryptionDefaultDriver = 'OpenSSL';
public string $hmacEncryptionDefaultDigest = 'SHA512';

```

```console
php spark shield:hmac reencrypt
```

You can (and should) set these values using environment variable and/or the **.env** file. To do this you will need to set
the values as JSON strings:

```text
authtoken.hmacEncryptionKeys = '{"k1":{"key":"hex2bin:923dfab5ddca0c7784c2c388a848a704f5e048736c1a852c862959da62ade8c7"},"k2":{"key":"hex2bin:451df599363b19be1434605fff8556a0bbfc50bede1bb33793dcde4d97fce4b0"}}'
authtoken.hmacEncryptionCurrentKey = k2
```

Depending on the set length of the Secret Key and the type of encryption used, it is possible for the encrypted value to
exceed the database column character limit of 255 characters. If this happens, creation of a new HMAC identity will
throw a `RuntimeException`.
5 changes: 4 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@
<!-- https://getcomposer.org/xdebug -->
<env name="COMPOSER_DISABLE_XDEBUG_WARN" value="1"/>

<!-- Database configuration -->
<!-- Default HMAC encryption key -->
<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;}}"/>

<!-- Database configuration -->
<env name="database.tests.strictOn" value="true"/>
<!-- Uncomment to use alternate testing database configuration
<env name="database.tests.hostname" value="localhost"/>
Expand Down
6 changes: 5 additions & 1 deletion src/Authentication/Authenticators/HmacSha256.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use CodeIgniter\I18n\Time;
use CodeIgniter\Shield\Authentication\AuthenticationException;
use CodeIgniter\Shield\Authentication\AuthenticatorInterface;
use CodeIgniter\Shield\Authentication\HMAC\HmacEncrypter;
use CodeIgniter\Shield\Config\Auth;
use CodeIgniter\Shield\Entities\User;
use CodeIgniter\Shield\Exceptions\InvalidArgumentException;
Expand Down Expand Up @@ -159,8 +160,11 @@ public function check(array $credentials): Result
]);
}

$encrypter = new HmacEncrypter();
$secretKey = $encrypter->decrypt($token->secret2);

// Check signature...
$hash = hash_hmac('sha256', $credentials['body'], $token->secret2);
$hash = hash_hmac('sha256', $credentials['body'], $secretKey);
if ($hash !== $signature) {
return new Result([
'success' => false,
Expand Down
153 changes: 153 additions & 0 deletions src/Authentication/HMAC/HmacEncrypter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter Shield.
*
* (c) CodeIgniter Foundation <[email protected]>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Shield\Authentication\HMAC;

use CodeIgniter\Encryption\EncrypterInterface;
use CodeIgniter\Encryption\Exceptions\EncryptionException;
use CodeIgniter\Shield\Auth;
use CodeIgniter\Shield\Config\AuthToken;
use CodeIgniter\Shield\Exceptions\RuntimeException;
use Config\Encryption;
use Config\Services;
use Exception;

/**
* HMAC Encrypter class
*
* This class handles the setup and configuration of the HMAC Encryption
*/
class HmacEncrypter
{
/**
* Codeigniter Encrypter
*
* @var array<string, EncrypterInterface>
*/
private array $encrypter;

/**
* Auth Token config
*/
private AuthToken $authConfig;

/**
* Constructor
* Setup encryption configuration
*/
public function __construct()
{
$this->authConfig = config('AuthToken');

$this->getEncrypter($this->authConfig->hmacEncryptionCurrentKey);
}

/**
* Decrypt
*
* @param string $encString Encrypted string
*
* @return string Raw decrypted string
*
* @throws EncryptionException
*/
public function decrypt(string $encString): string
{
$matches = [];
// check for a match
if (preg_match('/^\$b6\$(\w+?)\$(.+)\z/', $encString, $matches) !== 1) {
throw new EncryptionException('Unable to decrypt string');
}

$encrypter = $this->getEncrypter($matches[1]);

return $encrypter->decrypt(base64_decode($matches[2], true));
}

/**
* Encrypt
*
* @param string $rawString Raw string to encrypt
*
* @return string Encrypted string
*
* @throws EncryptionException
* @throws RuntimeException
*/
public function encrypt(string $rawString): string
{
$currentKey = $this->authConfig->hmacEncryptionCurrentKey;

$encryptedString = '$b6$' . $currentKey . '$' . base64_encode($this->encrypter[$currentKey]->encrypt($rawString));

if (strlen($encryptedString) > $this->authConfig->secret2StorageLimit) {
throw new RuntimeException('Encrypted key too long. Unable to store value.');
}

return $encryptedString;
}

/**
* Check if the string already encrypted
*/
public function isEncrypted(string $string): bool
{
return preg_match('/^\$b6\$/', $string) === 1;
}

/**
* Check if the string already encrypted with the Current Set Key
*/
public function isEncryptedWithCurrentKey(string $string): bool
{
$currentKey = $this->authConfig->hmacEncryptionCurrentKey;

return preg_match('/^\$b6\$' . $currentKey . '\$/', $string) === 1;
}

/**
* Generate Key
*
* @return string Secret Key in base64 format
*
* @throws Exception
*/
public function generateSecretKey(): string
{
return base64_encode(random_bytes($this->authConfig->hmacSecretKeyByteSize));
}

/**
* Retrieve encrypter for selected key
*
* @param string $encrypterKey Index Key for selected Encrypter
*/
private function getEncrypter(string $encrypterKey): EncrypterInterface
{
if (! isset($this->encrypter[$encrypterKey])) {
if (! isset($this->authConfig->hmacEncryptionKeys[$encrypterKey]['key'])) {
throw new RuntimeException('Encryption key does not exist.');
}

$config = new Encryption();

$config->key = $this->authConfig->hmacEncryptionKeys[$encrypterKey]['key'];
$config->driver = $this->authConfig->hmacEncryptionKeys[$encrypterKey]['driver'] ?? $this->authConfig->hmacEncryptionDefaultDriver;
$config->digest = $this->authConfig->hmacEncryptionKeys[$encrypterKey]['digest'] ?? $this->authConfig->hmacEncryptionDefaultDigest;

$this->encrypter[$encrypterKey] = Services::encrypter($config);
}

return $this->encrypter[$encrypterKey];
}
}
Loading

0 comments on commit f77c6ae

Please sign in to comment.