From 5df1a0d1bb8a14d4db970f279e87c54757ebb1af Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sun, 26 Mar 2017 14:01:53 +0200 Subject: [PATCH 1/5] V4.0 --- .travis.yml | 16 ++-- README.md | 7 +- composer.json | 7 +- src/A128KW.php | 10 +-- src/A192KW.php | 10 +-- src/A256KW.php | 10 +-- src/AESKW.php | 87 +++++++++++++------- src/EncryptorInterface.php | 29 ------- src/OpenSSLEncryptor.php | 40 --------- tests/ExceptionTest.php | 5 +- tests/JWETest.php | 5 +- tests/One64BitBlockTest.php | 5 +- tests/Performance.php | 160 ++++++++++++++++++++++++++++++++++++ tests/RFC3394Test.php | 5 +- tests/RFC5649Test.php | 5 +- 15 files changed, 266 insertions(+), 135 deletions(-) delete mode 100644 src/EncryptorInterface.php delete mode 100644 src/OpenSSLEncryptor.php create mode 100644 tests/Performance.php diff --git a/.travis.yml b/.travis.yml index f1d2257..9cc5653 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,14 @@ language: php php: - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - 7.1 - - nightly - - hhvm + allow_failures: + - php: nightly + fast_finish: true + include: + - 7.0 + - 7.1 + - nightly + - hhvm before_script: - composer self-update @@ -19,3 +20,4 @@ script: after_success: - vendor/bin/coveralls --no-interaction + - php -r "require_once 'vendor/autoload.php'; AESKW\Tests\Performance::run();"; diff --git a/README.md b/README.md index edec0cb..e14fca6 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,17 @@ The release process [is described here](doc/Release.md). ## Prerequisites -This library needs at least `PHP 5.4`. +This library needs at least `PHP 7.0`. +It has been successfully tested using `PHP 7.0`, `PHP 7.1` and `HHVM`. -It has been successfully tested using `PHP 5.4` to `PHP 5.6`, `PHP 7.0` and `HHVM`. +For `PHP 5.4+`, please use the version `3.x` of this library. ## Installation The preferred way to install this library is to rely on Composer: ```sh -composer require "spomky-labs/aes-key-wrap" "^3.0" +composer require spomky-labs/aes-key-wrap ``` ## How to use diff --git a/composer.json b/composer.json index 62a031c..9f1db70 100644 --- a/composer.json +++ b/composer.json @@ -22,18 +22,17 @@ } }, "require": { - "php": "^5.4|^7.0", + "php": "^7.0", "ext-mbstring": "*", - "beberlei/assert": "^2.4", "lib-openssl": "*" }, "require-dev": { - "phpunit/phpunit": "^4.5|^5.0", + "phpunit/phpunit": "^6.0", "satooshi/php-coveralls": "^1.0" }, "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "4.0.x-dev" } } } diff --git a/src/A128KW.php b/src/A128KW.php index efb03b7..c499a47 100644 --- a/src/A128KW.php +++ b/src/A128KW.php @@ -11,17 +11,15 @@ namespace AESKW; -use Assert\Assertion; - -class A128KW +final class A128KW { use AESKW; /** - * @param string $kek The Key Encryption Key + * {@inheritdoc} */ - protected static function checkKEKSize($kek) + protected static function getExpectedKEKSize(): int { - Assertion::eq(mb_strlen($kek, '8bit'), 16, 'Bad KEK size'); + return 16; } } diff --git a/src/A192KW.php b/src/A192KW.php index fb3fc7d..ef5397b 100644 --- a/src/A192KW.php +++ b/src/A192KW.php @@ -11,17 +11,15 @@ namespace AESKW; -use Assert\Assertion; - -class A192KW +final class A192KW { use AESKW; /** - * @param string $kek The Key Encryption Key + * {@inheritdoc} */ - protected static function checkKEKSize($kek) + protected static function getExpectedKEKSize(): int { - Assertion::eq(mb_strlen($kek, '8bit'), 24, 'Bad KEK size'); + return 24; } } diff --git a/src/A256KW.php b/src/A256KW.php index aa4904e..97918db 100644 --- a/src/A256KW.php +++ b/src/A256KW.php @@ -11,17 +11,15 @@ namespace AESKW; -use Assert\Assertion; - -class A256KW +final class A256KW { use AESKW; /** - * @param string $kek The Key Encryption Key + * {@inheritdoc} */ - protected static function checkKEKSize($kek) + protected static function getExpectedKEKSize(): int { - Assertion::eq(mb_strlen($kek, '8bit'), 32, 'Bad KEK size'); + return 32; } } diff --git a/src/AESKW.php b/src/AESKW.php index 6903c8d..4774c81 100644 --- a/src/AESKW.php +++ b/src/AESKW.php @@ -11,8 +11,6 @@ namespace AESKW; -use Assert\Assertion; - trait AESKW { /** @@ -27,7 +25,7 @@ trait AESKW * * @see https://tools.ietf.org/html/rfc3394#section-2.2.3.1 */ - private static function getInitialValue(&$key, $padding_enabled) + private static function getInitialValue(string &$key, bool $padding_enabled): string { if (false === $padding_enabled) { return hex2bin('A6A6A6A6A6A6A6A6'); @@ -49,7 +47,7 @@ private static function getInitialValue(&$key, $padding_enabled) * * @return bool */ - private static function checkInitialValue(&$key, $padding_enabled, $iv) + private static function checkInitialValue(string &$key, bool $padding_enabled, string $iv): bool { // RFC3394 compliant if ($iv === hex2bin('A6A6A6A6A6A6A6A6')) { @@ -88,10 +86,14 @@ private static function checkInitialValue(&$key, $padding_enabled, $iv) * @param string $key The Key to wrap * @param bool $padding_enabled */ - private static function checkKeySize($key, $padding_enabled) + private static function checkKeySize(string $key, bool $padding_enabled) { - Assertion::false(false === $padding_enabled && 0 !== mb_strlen($key, '8bit') % 8, 'Bad key size'); - Assertion::greaterOrEqualThan(mb_strlen($key, '8bit'), 1, 'Bad key size'); + if (empty($key)) { + throw new \InvalidArgumentException('Bad key size'); + } + if (false === $padding_enabled && 0 !== mb_strlen($key, '8bit') % 8) { + throw new \InvalidArgumentException('Bad key size'); + } } /** @@ -101,7 +103,7 @@ private static function checkKeySize($key, $padding_enabled) * * @return string The wrapped key */ - public static function wrap($kek, $key, $padding_enabled = false) + public static function wrap(string $kek, string $key, bool $padding_enabled = false): string { self::checkKEKSize($kek); $A = self::getInitialValue($key, $padding_enabled); @@ -110,16 +112,15 @@ public static function wrap($kek, $key, $padding_enabled = false) $N = count($P); $C = []; - $encryptor = self::getEncryptor($kek); if (1 === $N) { - $B = $encryptor->encrypt($A.$P[0]); + $B = self::encrypt($kek, $A.$P[0]); $C[0] = self::getMSB($B); $C[1] = self::getLSB($B); } elseif (1 < $N) { $R = $P; for ($j = 0; $j <= 5; ++$j) { for ($i = 1; $i <= $N; ++$i) { - $B = $encryptor->encrypt($A.$R[$i - 1]); + $B = self::encrypt($kek, $A.$R[$i - 1]); $t = $i + $j * $N; $A = self::toXBits(64, $t) ^ self::getMSB($B); $R[$i - 1] = self::getLSB($B); @@ -138,18 +139,17 @@ public static function wrap($kek, $key, $padding_enabled = false) * * @return string The key unwrapped */ - public static function unwrap($kek, $key, $padding_enabled = false) + public static function unwrap(string $kek, string $key, bool $padding_enabled = false): string { self::checkKEKSize($kek); $P = str_split($key, 8); $A = $P[0]; $N = count($P); - Assertion::greaterThan($N, 1, 'Bad data'); - $encryptor = self::getEncryptor($kek); - - if (2 === $N) { - $B = $encryptor->decrypt($P[0].$P[1]); + if (2 > $N) { + throw new \InvalidArgumentException('Bad data'); + } elseif (2 === $N) { + $B = self::decrypt($kek, $P[0].$P[1]); $unwrapped = self::getLSB($B); $A = self::getMSB($B); } else { @@ -157,7 +157,7 @@ public static function unwrap($kek, $key, $padding_enabled = false) for ($j = 5; $j >= 0; --$j) { for ($i = $N - 1; $i >= 1; --$i) { $t = $i + $j * ($N - 1); - $B = $encryptor->decrypt((self::toXBits(64, $t) ^ $A).$R[$i]); + $B = self::decrypt($kek, (self::toXBits(64, $t) ^ $A).$R[$i]); $A = self::getMSB($B); $R[$i] = self::getLSB($B); } @@ -166,18 +166,25 @@ public static function unwrap($kek, $key, $padding_enabled = false) $unwrapped = implode('', $R); } - Assertion::true(self::checkInitialValue($unwrapped, $padding_enabled, $A), 'Integrity check failed'); + if (false === self::checkInitialValue($unwrapped, $padding_enabled, $A)) { + throw new \InvalidArgumentException('Integrity check failed!'); + } return $unwrapped; } + /** + * @return int + */ + abstract protected static function getExpectedKEKSize(): int; + /** * @param int $bits * @param int $value * * @return string */ - private static function toXBits($bits, $value) + private static function toXBits(int $bits, int $value): string { return hex2bin(str_pad(dechex($value), $bits / 4, '0', STR_PAD_LEFT)); } @@ -187,7 +194,7 @@ private static function toXBits($bits, $value) * * @return string */ - private static function getMSB($value) + private static function getMSB(string $value): string { return mb_substr($value, 0, mb_strlen($value, '8bit') / 2, '8bit'); } @@ -197,22 +204,44 @@ private static function getMSB($value) * * @return string */ - private static function getLSB($value) + private static function getLSB(string $value): string { return mb_substr($value, mb_strlen($value, '8bit') / 2, null, '8bit'); } /** - * @param string $kek - * - * @return \AESKW\EncryptorInterface + * {@inheritdoc} */ - private static function getEncryptor($kek) + private static function encrypt(string $kek, string $data): string { - if (extension_loaded('openssl')) { - return new OpenSSLEncryptor($kek); + return openssl_encrypt($data, self::getMethod($kek), $kek, OPENSSL_ZERO_PADDING | OPENSSL_RAW_DATA); + } + + /** + * {@inheritdoc} + */ + private static function decrypt(string $kek, string $data): string + { + return openssl_decrypt($data, self::getMethod($kek), $kek, OPENSSL_ZERO_PADDING | OPENSSL_RAW_DATA); + } + + /** + * @param string $kek The Key Encryption Key + */ + private static function checkKEKSize(string $kek) + { + if (mb_strlen($kek, '8bit') !== self::getExpectedKEKSize()) { + throw new \InvalidArgumentException('Bad KEK size'); } + } - throw new \RuntimeException('Please install OpenSSL extension.'); + /** + * @param string $kek + * + * @return string + */ + private static function getMethod(string $kek): string + { + return sprintf('aes-%d-ecb', mb_strlen($kek, '8bit') * 8); } } diff --git a/src/EncryptorInterface.php b/src/EncryptorInterface.php deleted file mode 100644 index 5480044..0000000 --- a/src/EncryptorInterface.php +++ /dev/null @@ -1,29 +0,0 @@ -kek = $kek; - $this->method = 'aes-'.(mb_strlen($kek, '8bit') * 8).'-ecb'; - } - - /** - * {@inheritdoc} - */ - public function encrypt($data) - { - return openssl_encrypt($data, $this->method, $this->kek, OPENSSL_ZERO_PADDING | OPENSSL_RAW_DATA); - } - - /** - * {@inheritdoc} - */ - public function decrypt($data) - { - return openssl_decrypt($data, $this->method, $this->kek, OPENSSL_ZERO_PADDING | OPENSSL_RAW_DATA); - } -} diff --git a/tests/ExceptionTest.php b/tests/ExceptionTest.php index ee6f9a8..ead2bf6 100644 --- a/tests/ExceptionTest.php +++ b/tests/ExceptionTest.php @@ -9,16 +9,19 @@ * of the MIT license. See the LICENSE file for details. */ +namespace AESKW\Tests; + use AESKW\A128KW; use AESKW\A192KW; use AESKW\A256KW; +use PHPUnit\Framework\TestCase; /** * These tests come from the RFC3394. * * @see https://www.ietf.org/rfc/rfc3394.txt#4 */ -class ExceptionTest extends \PHPUnit_Framework_TestCase +final class ExceptionTest extends TestCase { /** * @expectedException InvalidArgumentException diff --git a/tests/JWETest.php b/tests/JWETest.php index 887fef8..695acb9 100644 --- a/tests/JWETest.php +++ b/tests/JWETest.php @@ -9,14 +9,17 @@ * of the MIT license. See the LICENSE file for details. */ +namespace AESKW\Tests; + use AESKW\A128KW; +use PHPUnit\Framework\TestCase; /** * This test comes from the JWE specification. * * @see https://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-39#appendix-A.3.3 */ -class JWETest extends \PHPUnit_Framework_TestCase +final class JWETest extends TestCase { public function testCEKEncryption() { diff --git a/tests/One64BitBlockTest.php b/tests/One64BitBlockTest.php index 1fc56da..90091d0 100644 --- a/tests/One64BitBlockTest.php +++ b/tests/One64BitBlockTest.php @@ -9,16 +9,19 @@ * of the MIT license. See the LICENSE file for details. */ +namespace AESKW\Tests; + use AESKW\A128KW; use AESKW\A192KW; use AESKW\A256KW; +use PHPUnit\Framework\TestCase; /** * These tests come from the RFC3394. * * @see https://www.ietf.org/rfc/rfc3394.txt#4 */ -class One64BitBlockTest extends \PHPUnit_Framework_TestCase +final class One64BitBlockTest extends TestCase { public function testWrap64BitsKeyDataWith128BitKEK() { diff --git a/tests/Performance.php b/tests/Performance.php new file mode 100644 index 0000000..c3bfba1 --- /dev/null +++ b/tests/Performance.php @@ -0,0 +1,160 @@ + $nb) { + throw new \InvalidArgumentException('You must perform at least 1 test.'); + } + $cases = self::getData(); + foreach ($cases as $case) { + self::wrap($nb, $case); + self::unwrap($nb, $case); + } + } + + /** + * @param int $nb + * @param array $case + */ + private static function wrap(int $nb, array $case) + { + $class = $case['class']; + $kek = $case['kek']; + $data = $case['data']; + $padding = $case['padding']; + $time = self::do($class, 'wrap', $nb, $kek, $data, $padding); + + printf('%s: %f milliseconds/wrap'.PHP_EOL, $case['name'],$time); + } + + /** + * @param int $nb + * @param array $case + */ + private static function unwrap(int $nb, array $case) + { + $class = $case['class']; + $kek = $case['kek']; + $result = $case['result']; + $padding = $case['padding']; + $time = self::do($class, 'unwrap', $nb, $kek, $result, $padding); + + printf('%s: %f milliseconds/unwrap'.PHP_EOL, $case['name'],$time); + } + + /** + * @param string $class + * @param string $method + * @param int $nb + * @param array ...$args + * + * @return float + */ + private static function do(string $class, string $method, int $nb, ...$args): float + { + $time_start = microtime(true); + for ($i = 0; $i < $nb; $i++) { + call_user_func_array([$class, $method], $args); + } + $time_end = microtime(true); + + return ($time_end - $time_start) / $nb * 1000; + } + + /** + * @return array + */ + private static function getData(): array + { + return [ + [ + 'class' => A128KW::class, + 'name' => 'RFC3394: 128 bits data with 128 bits KEK', + 'kek' => hex2bin('000102030405060708090A0B0C0D0E0F'), + 'data' => hex2bin('00112233445566778899AABBCCDDEEFF'), + 'result' => hex2bin('1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5'), + 'padding' => false, + ], + [ + 'class' => A192KW::class, + 'name' => 'RFC3394: 128 bits data with 192 bits KEK', + 'kek' => hex2bin('000102030405060708090A0B0C0D0E0F1011121314151617'), + 'data' => hex2bin('00112233445566778899AABBCCDDEEFF'), + 'result' => hex2bin('96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D'), + 'padding' => false, + ], + [ + 'class' => A256KW::class, + 'name' => 'RFC3394: 128 bits data with 256 bits KEK', + 'kek' => hex2bin('000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F'), + 'data' => hex2bin('00112233445566778899AABBCCDDEEFF'), + 'result' => hex2bin('64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7'), + 'padding' => false, + ], + [ + 'class' => A192KW::class, + 'name' => 'RFC3394: 192 bits data with 192 bits KEK', + 'kek' => hex2bin('000102030405060708090A0B0C0D0E0F1011121314151617'), + 'data' => hex2bin('00112233445566778899AABBCCDDEEFF0001020304050607'), + 'result' => hex2bin('031D33264E15D33268F24EC260743EDCE1C6C7DDEE725A936BA814915C6762D2'), + 'padding' => false, + ], + [ + 'class' => A256KW::class, + 'name' => 'RFC3394: 192 bits data with 256 bits KEK', + 'kek' => hex2bin('000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F'), + 'data' => hex2bin('00112233445566778899AABBCCDDEEFF0001020304050607'), + 'result' => hex2bin('A8F9BC1612C68B3FF6E6F4FBE30E71E4769C8B80A32CB8958CD5D17D6B254DA1'), + 'padding' => false, + ], + [ + 'class' => A256KW::class, + 'name' => 'RFC3394: 256 bits data with 256 bits KEK', + 'kek' => hex2bin('000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F'), + 'data' => hex2bin('00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F'), + 'result' => hex2bin('28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD21'), + 'padding' => false, + ], + [ + 'class' => A192KW::class, + 'name' => 'RFC5649 160 bits data with 192 bits KEK', + 'kek' => hex2bin('5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8'), + 'data' => hex2bin('c37b7e6492584340bed12207808941155068f738'), + 'result' => hex2bin('138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a'), + 'padding' => true, + ], + [ + 'class' => A192KW::class, + 'name' => 'RFC5649 56 bits data with 192 bits KEK', + 'kek' => hex2bin('5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8'), + 'data' => hex2bin('466f7250617369'), + 'result' => hex2bin('afbeb0f07dfbf5419200f2ccb50bb24f'), + 'padding' => true, + ], + ]; + } +} diff --git a/tests/RFC3394Test.php b/tests/RFC3394Test.php index 3860101..cdf9dfb 100644 --- a/tests/RFC3394Test.php +++ b/tests/RFC3394Test.php @@ -9,16 +9,19 @@ * of the MIT license. See the LICENSE file for details. */ +namespace AESKW\Tests; + use AESKW\A128KW; use AESKW\A192KW; use AESKW\A256KW; +use PHPUnit\Framework\TestCase; /** * These tests come from the RFC3394. * * @see https://www.ietf.org/rfc/rfc3394.txt#4 */ -class RFC3394Test extends \PHPUnit_Framework_TestCase +final class RFC3394Test extends TestCase { public function testWrap128BitsKeyDataWith128BitKEK() { diff --git a/tests/RFC5649Test.php b/tests/RFC5649Test.php index 0963ddf..89d667a 100644 --- a/tests/RFC5649Test.php +++ b/tests/RFC5649Test.php @@ -9,14 +9,17 @@ * of the MIT license. See the LICENSE file for details. */ +namespace AESKW\Tests; + use AESKW\A192KW; +use PHPUnit\Framework\TestCase; /** * These tests come from the RFCRFC5649Test. * * @see https://tools.ietf.org/html/rfc5649#section-6 */ -class RFC5649Test extends \PHPUnit_Framework_TestCase +final class RFC5649Test extends TestCase { public function testWrap20BytesKeyDataWith192BitKEK() { From 216824f682b9e6e84fdd2cab8338a870a53ebca8 Mon Sep 17 00:00:00 2001 From: Spomky Date: Sun, 26 Mar 2017 12:06:29 +0000 Subject: [PATCH 2/5] Apply fixes from StyleCI [ci skip] [skip ci] --- tests/Performance.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Performance.php b/tests/Performance.php index c3bfba1..ff8c113 100644 --- a/tests/Performance.php +++ b/tests/Performance.php @@ -20,7 +20,7 @@ * You just have to call Performance::run(); * By default, tests are performed 1000 times. * You can modify it by passing a positive integer as first argument: - * Performance::run(10000); + * Performance::run(10000);. */ final class Performance { @@ -37,7 +37,7 @@ public static function run(int $nb = 1000) } /** - * @param int $nb + * @param int $nb * @param array $case */ private static function wrap(int $nb, array $case) @@ -48,11 +48,11 @@ private static function wrap(int $nb, array $case) $padding = $case['padding']; $time = self::do($class, 'wrap', $nb, $kek, $data, $padding); - printf('%s: %f milliseconds/wrap'.PHP_EOL, $case['name'],$time); + printf('%s: %f milliseconds/wrap'.PHP_EOL, $case['name'], $time); } /** - * @param int $nb + * @param int $nb * @param array $case */ private static function unwrap(int $nb, array $case) @@ -63,7 +63,7 @@ private static function unwrap(int $nb, array $case) $padding = $case['padding']; $time = self::do($class, 'unwrap', $nb, $kek, $result, $padding); - printf('%s: %f milliseconds/unwrap'.PHP_EOL, $case['name'],$time); + printf('%s: %f milliseconds/unwrap'.PHP_EOL, $case['name'], $time); } /** From 2b4c7b2bab437a84610eaa666a77c47f10157400 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sun, 26 Mar 2017 14:08:26 +0200 Subject: [PATCH 3/5] Bug fixed --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9cc5653..c224df3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: php -php: +matrix: allow_failures: - php: nightly fast_finish: true From 8ec08992d585014c1ce9f6d7888072e8d1bf3b69 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sun, 26 Mar 2017 14:15:05 +0200 Subject: [PATCH 4/5] Bug fixed --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index c224df3..2119d3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,10 @@ matrix: - php: nightly fast_finish: true include: - - 7.0 - - 7.1 - - nightly - - hhvm + - php: 7.0 + - php: 7.1 + - php: hhvm + - php: nightly before_script: - composer self-update From a0c8fbd74e8df8a837d39acf23475a6ce40202a2 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sun, 26 Mar 2017 14:17:05 +0200 Subject: [PATCH 5/5] Failures allowed on HHVM --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2119d3e..a8dde83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: php matrix: allow_failures: + - php: hhvm - php: nightly fast_finish: true include: