Skip to content

Commit

Permalink
Add support for AES GCM key encryption (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
kelvinmo authored Aug 26, 2023
2 parents 782d159 + 0c466ae commit 139ea3f
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/SimpleJWT/Crypt/AlgorithmFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
119 changes: 119 additions & 0 deletions src/SimpleJWT/Crypt/KeyManagement/AESGCMKeyWrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
/*
* SimpleJWT
*
* Copyright (C) Kelvin Mo 2023
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

namespace SimpleJWT\Crypt\KeyManagement;

use SimpleJWT\Crypt\BaseAlgorithm;
use SimpleJWT\Crypt\CryptException;
use SimpleJWT\Crypt\Encryption\AESGCM;
use SimpleJWT\Keys\SymmetricKey;
use SimpleJWT\Util\Util;

/**
* Implements AES GCM key encryption algorithm.
*
* @see https://tools.ietf.org/html/rfc7518#section-4.7
*/
class AESGCMKeyWrap extends BaseAlgorithm implements KeyEncryptionAlgorithm {
/** @var AESGCM $aesgcm */
private $aesgcm;

public function __construct($alg) {
$this->aesgcm = new AESGCM(substr($alg, 0, -2));
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);
}
}
?>
120 changes: 120 additions & 0 deletions tests/KeyManagement/AESGCMKeyWrapTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace SimpleJWT\Crypt\KeyManagement;

use SimpleJWT\Util\Util;
use PHPUnit\Framework\TestCase;

class AESGCMKeyWrapTest extends TestCase {
protected function isAlgAvailable($alg) {
$aesgcm = new AESGCMKeyWrap(null);
if (!in_array($alg, $aesgcm->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);
}
}
?>

0 comments on commit 139ea3f

Please sign in to comment.