Skip to content

Commit e5d81e4

Browse files
committed
Rework of encrypter so it can decrypt all
old keys. Added parent config to AuthToken.php. Update documentation to reflect change.
1 parent 493ebc6 commit e5d81e4

7 files changed

Lines changed: 123 additions & 104 deletions

File tree

docs/references/authentication/hmac.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,13 @@ public array $hmacEncryption = [
177177
'driver' => ['k1' => 'OpenSSL'],
178178
'digest' => ['k1' => 'SHA512'],
179179
'currentKey' => 'k1',
180-
'deprecatedKey' => null,
181180
];
182181
```
183182

184183
When it is time to update your encryption keys you will need to add an additional key to the above arrays. Then adjust
185-
the `$hmacEncryption['currentKey']` to point at the new key and adjust `$hmacEncryption['deprecatedKey']` to point at the
186-
old key. After the new encryption key is in place, run `php spark shield:hmac reencrypt` to re-encrypt all existing keys
187-
with the new encryption key.
184+
the `$hmacEncryption['currentKey']` to point at the new key. After the new encryption key is in place, run
185+
`php spark shield:hmac reencrypt` to re-encrypt all existing keys with the new encryption key. You will need to leave
186+
the old key in the array as it will be used read the existing keys during re-encryption.
188187

189188
```php
190189
public array $hmacEncryption = [
@@ -201,7 +200,6 @@ public array $hmacEncryption = [
201200
'k2' => 'SHA512',
202201
],
203202
'currentKey' => 'k2',
204-
'deprecatedKey' => 'k1',
205203
];
206204
```
207205

@@ -217,7 +215,6 @@ authtoken.hmacEncryption.key = '{"k1":"hex2bin:923dfab5ddca0c7784c2c388a848a704f
217215
authtoken.hmacEncryption.driver = '{"k1":"OpenSSL","k2":"OpenSSL"}'
218216
authtoken.hmacEncryption.digest = '{"k1":"SHA512","k2":"SHA512"}'
219217
authtoken.hmacEncryption.currentKey = k2
220-
authtoken.hmacEncryption.deprecatedKey = k1
221218
```
222219

223220
Depending on the set length of the Secret Key and the type of encryption used, it is possible for the encrypted value to

src/Authentication/HMAC/HmacEncrypter.php

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,41 +22,25 @@ class HmacEncrypter
2222
{
2323
/**
2424
* Codeigniter Encrypter
25+
*
26+
* @var array<string, EncrypterInterface>
2527
*/
26-
private EncrypterInterface $encrypter;
28+
private array $encrypter;
2729

2830
/**
2931
* Auth Token config
3032
*/
3133
private AuthToken $authConfig;
3234

33-
/**
34-
* Selected Key Index
35-
*/
36-
private string $keyIndex;
37-
3835
/**
3936
* Constructor
4037
* Setup encryption configuration
41-
*
42-
* @param bool $deprecatedKey Use the deprecated key (useful when re-encrypting on key rotation) [default: false]
4338
*/
44-
public function __construct(bool $deprecatedKey = false)
39+
public function __construct()
4540
{
46-
/** @var AuthToken $authConfig */
47-
$authConfig = config('AuthToken');
48-
$config = new Encryption();
49-
50-
// identify which encryption key should be used
51-
$this->keyIndex = $deprecatedKey ? $authConfig->hmacEncryption['deprecatedKey'] : $authConfig->hmacEncryption['currentKey'];
41+
$this->authConfig = config('AuthToken');
5242

53-
$config->key = $authConfig->hmacEncryption['key'][$this->keyIndex];
54-
$config->driver = $authConfig->hmacEncryption['driver'][$this->keyIndex];
55-
$config->digest = $authConfig->hmacEncryption['digest'][$this->keyIndex];
56-
57-
$this->authConfig = $authConfig;
58-
// decrypt secret key so signature can be validated
59-
$this->encrypter = Services::encrypter($config);
43+
$this->getEncrypter($this->authConfig->hmacEncryption['currentKey']);
6044
}
6145

6246
/**
@@ -70,10 +54,15 @@ public function __construct(bool $deprecatedKey = false)
7054
*/
7155
public function decrypt(string $encString): string
7256
{
73-
// Removes `$b6$keyIndex$` for base64 format.
74-
$encString = substr($encString, strlen($this->keyIndex) + 5);
57+
$matches = [];
58+
// check for a match
59+
if (preg_match('/^\$b6\$(\w+?)\$(.+)$/', $encString, $matches) !== 1) {
60+
throw new EncryptionException('Unable to decrypt string');
61+
}
62+
63+
$encrypter = $this->getEncrypter($matches[1]);
7564

76-
return $this->encrypter->decrypt(base64_decode($encString, true));
65+
return $encrypter->decrypt(base64_decode($matches[2], true));
7766
}
7867

7968
/**
@@ -88,7 +77,9 @@ public function decrypt(string $encString): string
8877
*/
8978
public function encrypt(string $rawString): string
9079
{
91-
$encryptedString = '$b6$' . $this->keyIndex . '$' . base64_encode($this->encrypter->encrypt($rawString));
80+
$currentKey = $this->authConfig->hmacEncryption['currentKey'];
81+
82+
$encryptedString = '$b6$' . $currentKey . '$' . base64_encode($this->encrypter[$currentKey]->encrypt($rawString));
9283

9384
if (strlen($encryptedString) > $this->authConfig->secret2StorageLimit) {
9485
throw new RuntimeException('Encrypted key too long. Unable to store value.');
@@ -102,15 +93,17 @@ public function encrypt(string $rawString): string
10293
*/
10394
public function isEncrypted(string $string): bool
10495
{
105-
return (bool) preg_match('/^\$b6\$/', $string);
96+
return preg_match('/^\$b6\$/', $string) === 1;
10697
}
10798

10899
/**
109-
* Check if the string already encrypted
100+
* Check if the string already encrypted with the Current Set Key
110101
*/
111-
public function isEncryptedWithSetKey(string $string): bool
102+
public function isEncryptedWithCurrentKey(string $string): bool
112103
{
113-
return preg_match('/^\$b6\$' . $this->keyIndex . '\$/', $string) === 1;
104+
$currentKey = $this->authConfig->hmacEncryption['currentKey'];
105+
106+
return preg_match('/^\$b6\$' . $currentKey . '\$/', $string) === 1;
114107
}
115108

116109
/**
@@ -124,4 +117,28 @@ public function generateSecretKey(): string
124117
{
125118
return base64_encode(random_bytes($this->authConfig->hmacSecretKeyByteSize));
126119
}
120+
121+
/**
122+
* Retrieve encrypter for selected key
123+
*
124+
* @param string $encrypterKey Index Key for selected Encrypter
125+
*/
126+
private function getEncrypter(string $encrypterKey): EncrypterInterface
127+
{
128+
if (! isset($this->encrypter[$encrypterKey])) {
129+
if (! isset($this->authConfig->hmacEncryption['key'][$encrypterKey])) {
130+
throw new RuntimeException('Encryption key does not exist.');
131+
}
132+
133+
$config = new Encryption();
134+
135+
$config->key = $this->authConfig->hmacEncryption['key'][$encrypterKey];
136+
$config->driver = $this->authConfig->hmacEncryption['driver'][$encrypterKey];
137+
$config->digest = $this->authConfig->hmacEncryption['digest'][$encrypterKey];
138+
139+
$this->encrypter[$encrypterKey] = Services::encrypter($config);
140+
}
141+
142+
return $this->encrypter[$encrypterKey];
143+
}
127144
}

src/Commands/Hmac.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,21 +176,20 @@ public function reEncrypt(): void
176176
{
177177
$uIdModel = new UserIdentityModel();
178178
$uIdModelSub = new UserIdentityModel(); // For saving.
179-
$decrypter = new HmacEncrypter(true);
180179
$encrypter = $this->encrypter;
181180

182181
$that = $this;
183182

184183
$uIdModel->where('type', 'hmac_sha256')->orderBy('id')->chunk(
185184
100,
186-
static function ($identity) use ($uIdModelSub, $decrypter, $encrypter, $that): void {
187-
if (! $decrypter->isEncryptedWithSetKey($identity->secret2)) {
185+
static function ($identity) use ($uIdModelSub, $encrypter, $that): void {
186+
if ($encrypter->isEncryptedWithCurrentKey($identity->secret2)) {
188187
$that->write('id: ' . $identity->id . ', not re-encrypted, skipped.');
189188

190189
return;
191190
}
192191

193-
$identity->secret2 = $decrypter->decrypt($identity->secret2);
192+
$identity->secret2 = $encrypter->decrypt($identity->secret2);
194193
$identity->secret2 = $encrypter->encrypt($identity->secret2);
195194
$uIdModelSub->save($identity);
196195

src/Commands/Setup.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,8 @@ private function publishConfigAuthToken(): void
140140
{
141141
$file = 'Config/AuthToken.php';
142142
$replaces = [
143-
'namespace CodeIgniter\Shield\Config' => 'namespace Config',
144-
'use CodeIgniter\\Config\\BaseConfig;' => 'use CodeIgniter\\Shield\\Config\\AuthToken as ShieldAuthToken;',
145-
'extends BaseConfig' => 'extends ShieldAuthToken',
143+
'namespace CodeIgniter\Shield\Config;' => "namespace Config;\n\nuse CodeIgniter\\Shield\\Config\\AuthToken as ShieldAuthToken;",
144+
'extends BaseAuthToken' => 'extends ShieldAuthToken',
146145
];
147146

148147
$this->copyAndReplace($file, $replaces);

src/Config/AuthToken.php

Lines changed: 10 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@
1313

1414
namespace CodeIgniter\Shield\Config;
1515

16-
use CodeIgniter\Config\BaseConfig;
17-
1816
/**
1917
* Configuration for Token Auth and HMAC Auth
2018
*/
21-
class AuthToken extends BaseConfig
19+
class AuthToken extends BaseAuthToken
2220
{
2321
/**
2422
* --------------------------------------------------------------------
@@ -79,7 +77,7 @@ class AuthToken extends BaseConfig
7977
* This sets the key to be used when encrypting a user's HMAC Secret Key.
8078
*
8179
* 'key' is an array of keys which will facilitate key rotation. Valid
82-
* keyTitles must include only [a-zA-z0-9_] and should be kept to a
80+
* keyTitles must include only [a-zA-Z0-9_] and should be kept to a
8381
* max of 8 characters.
8482
*
8583
* 'driver' is used when encrypting HMAC Secret Key for storage.
@@ -96,65 +94,18 @@ class AuthToken extends BaseConfig
9694
* This is an array of digest values. The keys MUST match and correlate
9795
* to the 'key' array keys.
9896
*
99-
* The valid and old/deprecated keys are identified using 'currentKey'
100-
* and 'deprecatedKey'.
97+
* The valid/current key is identified using 'currentKey'
10198
*
102-
* 'deprecatedKey' reflects which 'key' is recently deprecated. This
103-
* is required and used when rotating keys. Effectively, this is the
104-
* index selector for the old key.
99+
* Old keys will are used to decrypt existing Secret Keys. It is encouraged
100+
* to run 'php spark shield:hmac reencrypt' to update existing Secret
101+
* Key encryptions.
105102
*
106103
* @see https://codeigniter.com/user_guide/libraries/encryption.html
107104
*/
108105
public array $hmacEncryption = [
109-
'key' => ['k1' => ''],
110-
'driver' => ['k1' => 'OpenSSL'],
111-
'digest' => ['k1' => 'SHA512'],
112-
'currentKey' => 'k1',
113-
'deprecatedKey' => '',
106+
'key' => ['k1' => ''],
107+
'driver' => ['k1' => 'OpenSSL'],
108+
'digest' => ['k1' => 'SHA512'],
109+
'currentKey' => 'k1',
114110
];
115-
116-
/**
117-
* AuthToken Config Constructor
118-
*/
119-
public function __construct()
120-
{
121-
parent::__construct();
122-
123-
$overwriteHmacEncryptionFields = [
124-
'key',
125-
'driver',
126-
'digest',
127-
];
128-
129-
foreach ($overwriteHmacEncryptionFields as $fieldName) {
130-
if (is_string($this->hmacEncryption[$fieldName])) {
131-
$array = json_decode($this->hmacEncryption[$fieldName], true);
132-
if (is_array($array)) {
133-
$this->hmacEncryption[$fieldName] = $array;
134-
}
135-
}
136-
}
137-
}
138-
139-
/**
140-
* Override parent initEnvValue() to allow for direct setting to array properties values from ENV
141-
*
142-
* In order to set array properties via ENV vars we need to set the property to a string value first.
143-
*
144-
* @param mixed $property
145-
*/
146-
protected function initEnvValue(&$property, string $name, string $prefix, string $shortPrefix): void
147-
{
148-
switch ($name) {
149-
case 'hmacEncryption.key':
150-
case 'hmacEncryption.driver':
151-
case 'hmacEncryption.digest':
152-
// if attempting to set property from ENV, first set to empty string
153-
if ($this->getEnvValue($name, $prefix, $shortPrefix) !== null) {
154-
$property = '';
155-
}
156-
}
157-
158-
parent::initEnvValue($property, $name, $prefix, $shortPrefix);
159-
}
160111
}

src/Config/BaseAuthToken.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CodeIgniter\Shield\Config;
6+
7+
use CodeIgniter\Config\BaseConfig;
8+
9+
class BaseAuthToken extends BaseConfig
10+
{
11+
public array $hmacEncryption;
12+
13+
/**
14+
* AuthToken Config Constructor
15+
*/
16+
public function __construct()
17+
{
18+
parent::__construct();
19+
20+
$overwriteHmacEncryptionFields = [
21+
'key',
22+
'driver',
23+
'digest',
24+
];
25+
26+
foreach ($overwriteHmacEncryptionFields as $fieldName) {
27+
if (is_string($this->hmacEncryption[$fieldName])) {
28+
$array = json_decode($this->hmacEncryption[$fieldName], true);
29+
if (is_array($array)) {
30+
$this->hmacEncryption[$fieldName] = $array;
31+
}
32+
}
33+
}
34+
}
35+
36+
/**
37+
* Override parent initEnvValue() to allow for direct setting to array properties values from ENV
38+
*
39+
* In order to set array properties via ENV vars we need to set the property to a string value first.
40+
*
41+
* @param mixed $property
42+
*/
43+
protected function initEnvValue(&$property, string $name, string $prefix, string $shortPrefix): void
44+
{
45+
switch ($name) {
46+
case 'hmacEncryption.key':
47+
case 'hmacEncryption.driver':
48+
case 'hmacEncryption.digest':
49+
// if attempting to set property from ENV, first set to empty string
50+
if ($this->getEnvValue($name, $prefix, $shortPrefix) !== null) {
51+
$property = '';
52+
}
53+
}
54+
55+
parent::initEnvValue($property, $name, $prefix, $shortPrefix);
56+
}
57+
}

tests/Commands/HmacTest.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ public function testReEncrypt(): void
9393
/** @var AuthToken $config */
9494
$config = config('AuthToken');
9595

96-
$config->hmacEncryption['currentKey'] = 'k2';
97-
$config->hmacEncryption['deprecatedKey'] = 'k1';
96+
$config->hmacEncryption['currentKey'] = 'k2';
9897

9998
// new key generated with updated encryption
10099
$token2 = $user->generateHmacToken('bar');

0 commit comments

Comments
 (0)