From 42dd6ef68565dcaee1f319d129a6c561cd58174d Mon Sep 17 00:00:00 2001 From: Kelvin Mo Date: Sat, 26 Aug 2023 15:53:33 +1000 Subject: [PATCH 1/4] Add AES GCM key wrap --- src/SimpleJWT/Crypt/AlgorithmFactory.php | 1 + .../Crypt/KeyManagement/AESGCMKeyWrap.php | 119 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/SimpleJWT/Crypt/KeyManagement/AESGCMKeyWrap.php diff --git a/src/SimpleJWT/Crypt/AlgorithmFactory.php b/src/SimpleJWT/Crypt/AlgorithmFactory.php index 275f5e1..d15bfe5 100644 --- a/src/SimpleJWT/Crypt/AlgorithmFactory.php +++ b/src/SimpleJWT/Crypt/AlgorithmFactory.php @@ -60,6 +60,7 @@ class AlgorithmFactory { '/^RSA-OAEP$/' => 'SimpleJWT\Crypt\KeyManagement\RSAES', '/^RSA-OAEP-256$/' => 'SimpleJWT\Crypt\KeyManagement\RSAES', '/^A\d+KW$/' => 'SimpleJWT\Crypt\KeyManagement\AESKeyWrap', + '/^A\d+GCMKW$/' => 'SimpleJWT\Crypt\KeyManagement\AESGCMKeyWrap', '/^PBES2-HS\d+\\+A\d+KW$/' => 'SimpleJWT\Crypt\KeyManagement\PBES2', '/^ECDH-ES$/' => 'SimpleJWT\Crypt\KeyManagement\ECDH', '/^ECDH-ES\\+A\d+KW$/' => 'SimpleJWT\Crypt\KeyManagement\ECDH_AESKeyWrap', diff --git a/src/SimpleJWT/Crypt/KeyManagement/AESGCMKeyWrap.php b/src/SimpleJWT/Crypt/KeyManagement/AESGCMKeyWrap.php new file mode 100644 index 0000000..89de43b --- /dev/null +++ b/src/SimpleJWT/Crypt/KeyManagement/AESGCMKeyWrap.php @@ -0,0 +1,119 @@ +aesgcm = new AESGCM($alg); + parent::__construct($alg); + } + + public function getSupportedAlgs() { + $aesgcm_algs = $this->aesgcm->getSupportedAlgs(); + return array_map(function ($alg) { return $alg . 'KW'; }, $aesgcm_algs); + } + + public function getKeyCriteria() { + return [ + 'kty' => 'oct', + '~alg' => $this->getAlg(), + '@use' => 'enc', + '@key_ops' => ['wrapKey', 'unwrapKey'] + ]; + } + + /** + * {@inheritdoc} + */ + public function encryptKey($cek, $keys, &$headers, $kid = null) { + /** @var SymmetricKey $key */ + $key = $this->selectKey($keys, $kid); + if ($key == null) { + throw new CryptException('Key not found or is invalid'); + } + + $iv = Util::base64url_encode($this->generateIV()); + $results = $this->aesgcm->encryptAndSign($cek, $key->toBinary(), '', $iv); + $headers['iv'] = $iv; + $headers['tag'] = $results['tag']; + return $results['ciphertext']; + } + + /** + * {@inheritdoc} + */ + public function decryptKey($encrypted_key, $keys, $headers, $kid = null) { + /** @var SymmetricKey $key */ + $key = $this->selectKey($keys, $kid); + if ($key == null) { + throw new CryptException('Key not found or is invalid'); + } + if (!isset($headers['iv']) || !isset($headers['tag'])) { + throw new CryptException('iv or tag headers not set'); + } + + $cek = $this->aesgcm->decryptAndVerify($encrypted_key, $headers['tag'], $key->toBinary(), '', $headers['iv']); + + return $cek; + } + + /** + * Generates the initialisation vector. This uses + * {@link SimpleJWT\Util\Util::random_bytes()} to generate random bytes. + * + * @return string the initialisation vector as a binary string + */ + protected function generateIV() { + /** @var int<1, max> $len */ + $len = intval($this->aesgcm->getIVSize() / 8); + return Util::random_bytes($len); + } +} +?> From 1c87c31b0f7ca0357a886c30bd01ddeb93e40f2d Mon Sep 17 00:00:00 2001 From: Kelvin Mo Date: Sat, 26 Aug 2023 15:55:41 +1000 Subject: [PATCH 2/4] Add documentation --- CHANGELOG.md | 2 ++ README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2537969..ee8d128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. - Added: Support for `Ed25519` signatures and `X25519` key derviation algorithms +- Added: Support for AES GCM key encryption algorithms (`A128GCMKW`, + `A192GCMKW` and `A256GCMKW`) - Added: Support for COSE based keys - Changed: Use `box` to package the `jwkstool` utility - Changed: Refactored `Algorithm` (now renamed to `BaseAlgorithm`) diff --git a/README.md b/README.md index 11392d4..a27a217 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ SimpleJWT is a simple JSON web token library written in PHP. * Key agreement or direct encryption * RSAES-PKCS1-v1_5 (RSA1_5) * RSAES with OAEP (RSA-OAEP, RSA-OAEP-256) - * AES key wrap (A128KW, A192KW, A256KW) + * AES key wrap (A128KW, A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW) * PBES2 (PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW) * Elliptic Curve Diffie-Hellman (ECDH-ES), including X25519 - requires PHP 7.3 or later From dab5a5ca942759d33f6a400808d07f2f500b9b8b Mon Sep 17 00:00:00 2001 From: Kelvin Mo Date: Sat, 26 Aug 2023 17:06:29 +1000 Subject: [PATCH 3/4] Fix AESGCM algorithm selection --- src/SimpleJWT/Crypt/KeyManagement/AESGCMKeyWrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SimpleJWT/Crypt/KeyManagement/AESGCMKeyWrap.php b/src/SimpleJWT/Crypt/KeyManagement/AESGCMKeyWrap.php index 89de43b..ac52c25 100644 --- a/src/SimpleJWT/Crypt/KeyManagement/AESGCMKeyWrap.php +++ b/src/SimpleJWT/Crypt/KeyManagement/AESGCMKeyWrap.php @@ -51,7 +51,7 @@ class AESGCMKeyWrap extends BaseAlgorithm implements KeyEncryptionAlgorithm { private $aesgcm; public function __construct($alg) { - $this->aesgcm = new AESGCM($alg); + $this->aesgcm = new AESGCM(substr($alg, 0, -2)); parent::__construct($alg); } From 0c466ae5a738c429ddd1cb4d752d0f266ee502f7 Mon Sep 17 00:00:00 2001 From: Kelvin Mo Date: Sat, 26 Aug 2023 17:06:36 +1000 Subject: [PATCH 4/4] Add tests --- tests/KeyManagement/AESGCMKeyWrapTest.php | 120 ++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/KeyManagement/AESGCMKeyWrapTest.php diff --git a/tests/KeyManagement/AESGCMKeyWrapTest.php b/tests/KeyManagement/AESGCMKeyWrapTest.php new file mode 100644 index 0000000..7fb70d6 --- /dev/null +++ b/tests/KeyManagement/AESGCMKeyWrapTest.php @@ -0,0 +1,120 @@ +getSupportedAlgs())) { + $this->markTestSkipped('Alg not available: ' . $alg); + return false; + } else { + return true; + } + } + + protected function getKeySet($kek) { + return \SimpleJWT\Keys\KeySet::createFromSecret($kek, 'bin'); + } + + protected function hex2base64url($hex) { + return Util::base64url_encode(pack('H*', $hex)); + } + + function testA128GCMKeyWrap() { + if (!$this->isAlgAvailable('A128GCMKW')) return; + + $iv_hex = 'ee283a3fc75575e33efd4887'; + + $builder = $this->getMockBuilder('SimpleJWT\Crypt\KeyManagement\AESGCMKeyWrap'); + if (method_exists($builder, 'setMethods')) { + $stub = $builder->setConstructorArgs(['A128GCMKW']) + ->setMethods(['generateIV']) + ->getMock(); + } else { + $stub = $builder->setConstructorArgs(['A128GCMKW']) + ->onlyMethods(['generateIV']) + ->getMock(); + } + $stub->method('generateIV')->willReturn(hex2bin($iv_hex)); + + $headers = []; + + $key = hex2bin('d5de42b461646c255c87bd2962d3b9a2'); + $set = $this->getKeySet(hex2bin('7fddb57453c241d03efbed3ac44e371c')); + $encrypted_key = $stub->encryptKey($key, $set, $headers); + + $this->assertEquals($this->hex2base64url('2ccda4a5415cb91e135c2a0f78c9b2fd'), $encrypted_key); + $this->assertEquals($this->hex2base64url($iv_hex), $headers['iv']); + $this->assertEquals($this->hex2base64url('b36d1df9b9d5e596f83e8b7f52971cb3'), $headers['tag']); + + $decrypted_key = $stub->decryptKey($encrypted_key, $set, $headers); + $this->assertEquals($key, $decrypted_key); + } + + function testA192GCMKeyWrap() { + if (!$this->isAlgAvailable('A192GCMKW')) return; + + $iv_hex = '5f4b43e811da9c470d6a9b01'; + + $builder = $this->getMockBuilder('SimpleJWT\Crypt\KeyManagement\AESGCMKeyWrap'); + if (method_exists($builder, 'setMethods')) { + $stub = $builder->setConstructorArgs(['A192GCMKW']) + ->setMethods(['generateIV']) + ->getMock(); + } else { + $stub = $builder->setConstructorArgs(['A192GCMKW']) + ->onlyMethods(['generateIV']) + ->getMock(); + } + $stub->method('generateIV')->willReturn(hex2bin($iv_hex)); + + $headers = []; + + $key = hex2bin('d2ae38c4375954835d75b8e4c2f9bbb4'); + $set = $this->getKeySet(hex2bin('fbc0b4c56a714c83217b2d1bcadd2ed2e9efb0dcac6cc19f')); + $encrypted_key = $stub->encryptKey($key, $set, $headers); + + $this->assertEquals($this->hex2base64url('69482957e6be5c54882d00314e0259cf'), $encrypted_key); + $this->assertEquals($this->hex2base64url($iv_hex), $headers['iv']); + $this->assertEquals($this->hex2base64url('191e9f29bef63a26860c1e020a21137e'), $headers['tag']); + + $decrypted_key = $stub->decryptKey($encrypted_key, $set, $headers); + $this->assertEquals($key, $decrypted_key); + } + + function testA256GCMKeyWrap() { + if (!$this->isAlgAvailable('A256GCMKW')) return; + + $iv_hex = '0d18e06c7c725ac9e362e1ce'; + + $builder = $this->getMockBuilder('SimpleJWT\Crypt\KeyManagement\AESGCMKeyWrap'); + if (method_exists($builder, 'setMethods')) { + $stub = $builder->setConstructorArgs(['A256GCMKW']) + ->setMethods(['generateIV']) + ->getMock(); + } else { + $stub = $builder->setConstructorArgs(['A256GCMKW']) + ->onlyMethods(['generateIV']) + ->getMock(); + } + $stub->method('generateIV')->willReturn(hex2bin($iv_hex)); + + $headers = []; + + $key = hex2bin('2db5168e932556f8089a0622981d017d'); + $set = $this->getKeySet(hex2bin('31bdadd96698c204aa9ce1448ea94ae1fb4a9a0b3c9d773b51bb1822666b8f22')); + $encrypted_key = $stub->encryptKey($key, $set, $headers); + + $this->assertEquals($this->hex2base64url('fa4362189661d163fcd6a56d8bf0405a'), $encrypted_key); + $this->assertEquals($this->hex2base64url($iv_hex), $headers['iv']); + $this->assertEquals($this->hex2base64url('d636ac1bbedd5cc3ee727dc2ab4a9489'), $headers['tag']); + + $decrypted_key = $stub->decryptKey($encrypted_key, $set, $headers); + $this->assertEquals($key, $decrypted_key); + } +} +?> \ No newline at end of file