From b655bef0d4f0fa2f36c11c5122e284951e81961c Mon Sep 17 00:00:00 2001 From: Spomky Date: Tue, 22 Nov 2016 22:11:11 +0100 Subject: [PATCH] Add libCrypto and PHP7.1 AES GCM support (#4) This PR adds PHP7.1 native method and libCrypto support. Nothing changed for the user, but now is PHP7.1 or libCrypto are available, then the library is really faster (from 600 to 1000x faster). --- .travis.yml | 34 +++++--- README.md | 8 +- composer.json | 7 +- src/AESGCM.php | 181 +++++++++++++++++++++++++++++++++++------- tests/Benchmark.php | 20 ++--- tests/IEEE802Test.php | 7 +- tests/NistTest.php | 7 +- 7 files changed, 207 insertions(+), 57 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3a81cae..9cb1b42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,33 @@ language: php sudo: false -php: - - 5.4 - - 5.5 - - 5.6 - - 7 - - hhvm - - nightly - +matrix: + allow_failures: + - php: nightly + fast_finish: true + include: + - php: 5.4 + env: deps=low + - php: 5.4 + env: WITH_CRYPTO=true + - php: 5.5 + - php: 5.6 + - php: 7.0 + env: deps=low + - php: 7.0 + env: WITH_CRYPTO=true + - php: 7.1 + - php: hhvm + - php: hhvm + env: deps=low + - php: nightly + before_script: - - composer install --no-interaction + - composer self-update + - sh -c 'if [ "$WITH_CRYPTO" != "" ]; then pecl install crypto-0.2.2; fi;' - mkdir -p build/logs + - if [[ $deps = low ]]; then composer update --no-interaction --prefer-lowest ; fi + - if [[ !$deps ]]; then composer install --no-interaction ; fi script: - vendor/bin/phpunit --coverage-clover build/logs/clover.xml diff --git a/README.md b/README.md index 4a7784d..9b1c06b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,10 @@ The release process [is described here](doc/Release.md). This library needs at least ![PHP 5.4+](https://img.shields.io/badge/PHP-5.4%2B-ff69b4.svg). -It has been successfully tested using `PHP 5.4`, `PHP 5.5`, `PHP 5.6`, `HHVM` and `PHP 7` (stable and nightly branches). +It has been successfully tested using `PHP 5.4` to `PHP 7.1`, `HHVM` and nightly branches. + +If you use PHP 7.1+, this library has very good performance. **If you do not use PHP 7.1+, we highly recommend you to install the [PHP Crypto extension](https://github.com/bukka/php-crypto).** +This extension drastically increase the performance of this library. With our pure PHP method, you will have low performance. # Installation @@ -116,6 +119,9 @@ However, if the tag is appended at the end of the ciphertext and if it is not 12 ```php =5.4", "lib-openssl": "*", - "beberlei/assert": "^2.0", + "beberlei/assert": "^2.4", "symfony/polyfill-mbstring": "^1.1" }, "require-dev": { "phpunit/phpunit": "^4.5|^5.0", "satooshi/php-coveralls": "^1.0" }, + "suggest":{ + "ext-crypto": "Highly recommended for better performance." + }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } } } diff --git a/src/AESGCM.php b/src/AESGCM.php index 8629bf0..ce7a7d7 100644 --- a/src/AESGCM.php +++ b/src/AESGCM.php @@ -27,21 +27,21 @@ final class AESGCM public static function encrypt($K, $IV, $P = null, $A = null, $tag_length = 128) { Assertion::string($K, 'The key encryption key must be a binary string.'); + $key_length = mb_strlen($K, '8bit') * 8; + Assertion::inArray($key_length, [128, 192, 256], 'Bad key encryption key length.'); Assertion::string($IV, 'The Initialization Vector must be a binary string.'); Assertion::nullOrString($P, 'The data to encrypt must be null or a binary string.'); Assertion::nullOrString($A, 'The Additional Authentication Data must be null or a binary string.'); Assertion::integer($tag_length, 'Invalid tag length. Supported values are: 128, 120, 112, 104 and 96.'); Assertion::inArray($tag_length, [128, 120, 112, 104, 96], 'Invalid tag length. Supported values are: 128, 120, 112, 104 and 96.'); - list($J0, $v, $a_len_padding, $H) = self::common($K, $IV, $A); - $C = self::getGCTR($K, self::getInc(32, $J0), $P); - $u = self::calcVector($C); - $c_len_padding = self::addPadding($C); - - $S = self::getHash($H, $A.str_pad('', $v / 8, "\0").$C.str_pad('', $u / 8, "\0").$a_len_padding.$c_len_padding); - $T = self::getMSB($tag_length, self::getGCTR($K, $J0, $S)); + if (version_compare(PHP_VERSION, '7.1.0RC5') >= 0 && null !== $P) { + return self::encryptWithPHP71($K, $key_length, $IV, $P, $A, $tag_length); + } elseif (class_exists('\Crypto\Cipher')) { + return self::encryptWithCryptoExtension($K, $key_length, $IV, $P, $A, $tag_length); + } - return [$C, $T]; + return self::encryptWithPHP($K, $key_length, $IV, $P, $A, $tag_length); } /** @@ -60,6 +60,71 @@ public static function encryptAndAppendTag($K, $IV, $P = null, $A = null, $tag_l return implode(self::encrypt($K, $IV, $P, $A, $tag_length)); } + /** + * @param string $K Key encryption key + * @param string $key_length Key length + * @param string $IV Initialization vector + * @param null|string $P Data to encrypt (null for authentication) + * @param null|string $A Additional Authentication Data + * @param int $tag_length Tag length + * + * @return array + */ + private static function encryptWithPHP71($K, $key_length, $IV, $P = null, $A = null, $tag_length = 128) + { + $mode = 'aes-'.($key_length).'-gcm'; + $T = null; + $C = openssl_encrypt($P, $mode, $K, OPENSSL_RAW_DATA, $IV, $T, $A, $tag_length / 8); + Assertion::true(false !== $C, 'Unable to encrypt the data.'); + + return [$C, $T]; + } + + /** + * @param string $K Key encryption key + * @param string $key_length Key length + * @param string $IV Initialization vector + * @param null|string $P Data to encrypt (null for authentication) + * @param null|string $A Additional Authentication Data + * @param int $tag_length Tag length + * + * @return array + */ + private static function encryptWithPHP($K, $key_length, $IV, $P = null, $A = null, $tag_length = 128) + { + list($J0, $v, $a_len_padding, $H) = self::common($K, $key_length, $IV, $A); + + $C = self::getGCTR($K, $key_length, self::getInc(32, $J0), $P); + $u = self::calcVector($C); + $c_len_padding = self::addPadding($C); + + $S = self::getHash($H, $A.str_pad('', $v / 8, "\0").$C.str_pad('', $u / 8, "\0").$a_len_padding.$c_len_padding); + $T = self::getMSB($tag_length, self::getGCTR($K, $key_length, $J0, $S)); + + return [$C, $T]; + } + + /** + * @param string $K Key encryption key + * @param string $key_length Key length + * @param string $IV Initialization vector + * @param null|string $P Data to encrypt (null for authentication) + * @param null|string $A Additional Authentication Data + * @param int $tag_length Tag length + * + * @return array + */ + private static function encryptWithCryptoExtension($K, $key_length, $IV, $P = null, $A = null, $tag_length = 128) + { + $cipher = \Crypto\Cipher::aes(\Crypto\Cipher::MODE_GCM, $key_length); + $cipher->setAAD($A); + $cipher->setTagLength($tag_length / 8); + $C = $cipher->encrypt($P, $K, $IV); + $T = $cipher->getTag(); + + return [$C, $T]; + } + /** * @param string $K Key encryption key * @param string $IV Initialization vector @@ -69,9 +134,11 @@ public static function encryptAndAppendTag($K, $IV, $P = null, $A = null, $tag_l * * @return string */ - public static function decrypt($K, $IV, $C = null, $A = null, $T) + public static function decrypt($K, $IV, $C, $A, $T) { Assertion::string($K, 'The key encryption key must be a binary string.'); + $key_length = mb_strlen($K, '8bit') * 8; + Assertion::inArray($key_length, [128, 192, 256], 'Bad key encryption key length.'); Assertion::string($IV, 'The Initialization Vector must be a binary string.'); Assertion::nullOrString($C, 'The data to encrypt must be null or a binary string.'); Assertion::nullOrString($A, 'The Additional Authentication Data must be null or a binary string.'); @@ -79,19 +146,14 @@ public static function decrypt($K, $IV, $C = null, $A = null, $T) $tag_length = self::getLength($T); Assertion::integer($tag_length, 'Invalid tag length. Supported values are: 128, 120, 112, 104 and 96.'); Assertion::inArray($tag_length, [128, 120, 112, 104, 96], 'Invalid tag length. Supported values are: 128, 120, 112, 104 and 96.'); - list($J0, $v, $a_len_padding, $H) = self::common($K, $IV, $A); - $P = self::getGCTR($K, self::getInc(32, $J0), $C); - - $u = self::calcVector($C); - $c_len_padding = self::addPadding($C); - - $S = self::getHash($H, $A.str_pad('', $v / 8, "\0").$C.str_pad('', $u / 8, "\0").$a_len_padding.$c_len_padding); - $T1 = self::getMSB($tag_length, self::getGCTR($K, $J0, $S)); - $result = strcmp($T, $T1); - Assertion::eq($result, 0, 'Unable to decrypt or to verify the tag.'); + if (version_compare(PHP_VERSION, '7.1.0RC5') >= 0 && null !== $C) { + return self::decryptWithPHP71($K, $key_length, $IV, $C, $A, $T); + } elseif (class_exists('\Crypto\Cipher')) { + return self::decryptWithCryptoExtension($K, $key_length, $IV, $C, $A, $T, $tag_length); + } - return $P; + return self::decryptWithPHP($K, $key_length, $IV, $C, $A, $T, $tag_length); } /** @@ -110,25 +172,90 @@ public static function decrypt($K, $IV, $C = null, $A = null, $T) */ public static function decryptWithAppendedTag($K, $IV, $Ciphertext = null, $A = null, $tag_length = 128) { - $tag_length_in_bits = $tag_length/8; + $tag_length_in_bits = $tag_length / 8; $C = mb_substr($Ciphertext, 0, -$tag_length_in_bits, '8bit'); $T = mb_substr($Ciphertext, -$tag_length_in_bits, null, '8bit'); return self::decrypt($K, $IV, $C, $A, $T); } + /** + * @param string $K Key encryption key + * @param string $key_length Key length + * @param string $IV Initialization vector + * @param string|null $C Data to encrypt (null for authentication) + * @param string|null $A Additional Authentication Data + * @param string $T Tag + * + * @return string + */ + private static function decryptWithPHP71($K, $key_length, $IV, $C, $A, $T) + { + $mode = 'aes-'.($key_length).'-gcm'; + $P = openssl_decrypt(null === $C ? '' : $C, $mode, $K, OPENSSL_RAW_DATA, $IV, $T, $A); + Assertion::true(false !== $P, 'Unable to decrypt or to verify the tag.'); + + return $P; + } + + /** + * @param string $K Key encryption key + * @param string $key_length Key length + * @param string $IV Initialization vector + * @param string|null $C Data to encrypt (null for authentication) + * @param string|null $A Additional Authentication Data + * @param string $T Tag + * @param int $tag_length Tag length + * + * @return string + */ + private static function decryptWithPHP($K, $key_length, $IV, $C, $A, $T, $tag_length = 128) + { + list($J0, $v, $a_len_padding, $H) = self::common($K, $key_length, $IV, $A); + + $P = self::getGCTR($K, $key_length, self::getInc(32, $J0), $C); + + $u = self::calcVector($C); + $c_len_padding = self::addPadding($C); + + $S = self::getHash($H, $A.str_pad('', $v / 8, "\0").$C.str_pad('', $u / 8, "\0").$a_len_padding.$c_len_padding); + $T1 = self::getMSB($tag_length, self::getGCTR($K, $key_length, $J0, $S)); + Assertion::eq($T1, $T, 'Unable to decrypt or to verify the tag.'); + + return $P; + } + + /** + * @param string $K Key encryption key + * @param string $key_length Key length + * @param string $IV Initialization vector + * @param string|null $C Data to encrypt (null for authentication) + * @param string|null $A Additional Authentication Data + * @param string $T Tag + * @param int $tag_length Tag length + * + * @return string + */ + private static function decryptWithCryptoExtension($K, $key_length, $IV, $C, $A, $T, $tag_length = 128) + { + $cipher = \Crypto\Cipher::aes(\Crypto\Cipher::MODE_GCM, $key_length); + $cipher->setTag($T); + $cipher->setAAD($A); + $cipher->setTagLength($tag_length / 8); + + return $cipher->decrypt($C, $K, $IV); + } + /** * @param $K + * @param $key_length * @param $IV * @param $A * * @return array */ - private static function common($K, $IV, $A) + private static function common($K, $key_length, $IV, $A) { - $key_length = mb_strlen($K, '8bit') * 8; - Assertion::inArray($key_length, [128, 192, 256], 'Bad key encryption key length.'); - $H = openssl_encrypt(str_repeat("\0", 16), 'aes-'.($key_length).'-ecb', $K, OPENSSL_NO_PADDING | OPENSSL_RAW_DATA); //--- $iv_len = self::getLength($IV); @@ -310,12 +437,13 @@ private static function getHash($H, $X) /** * @param string $K + * @param int $key_length * @param string $ICB * @param string $X * * @return string */ - private static function getGCTR($K, $ICB, $X) + private static function getGCTR($K, $key_length, $ICB, $X) { if (empty($X)) { return ''; @@ -328,7 +456,6 @@ private static function getGCTR($K, $ICB, $X) for ($i = 2; $i <= $n; $i++) { $CB[$i] = self::getInc(32, $CB[$i - 1]); } - $key_length = strlen($K) * 8; $mode = 'aes-'.($key_length).'-ecb'; for ($i = 1; $i < $n; $i++) { $C = openssl_encrypt($CB[$i], $mode, $K, OPENSSL_NO_PADDING | OPENSSL_RAW_DATA); diff --git a/tests/Benchmark.php b/tests/Benchmark.php index e7929f2..79adc55 100644 --- a/tests/Benchmark.php +++ b/tests/Benchmark.php @@ -19,7 +19,7 @@ * * @param int $nb Number of encryption/decryption to perform */ -function runEncryptionBenchmark($nb = 100) +function runEncryptionBenchmark($nb = 1000) { Assertion::integer($nb, 'The argument must be an integer'); Assertion::greaterThan($nb, 1, 'The argument must be greater than 1'); @@ -33,13 +33,13 @@ function runEncryptionBenchmark($nb = 100) print_r('# AES-GCM ENCRYPTION BENCHMARK #'.PHP_EOL); print_r('################################'.PHP_EOL); - $time_start = microtime(true); + $time = -microtime(true); for ($i = 0; $i < $nb; $i++) { AESGCM::encrypt($K, $IV, $P, $A); } - $time_end = microtime(true); - $time = ($time_end - $time_start) / $nb * 1000; - printf('%f milliseconds/encryption (tested on %d encryptions)'.PHP_EOL, $time, $nb); + $time += microtime(true); + $ops = $nb / $time; + printf('%f OPS (tested on %d encryptions)'.PHP_EOL, $ops, $nb); print_r('################################'.PHP_EOL); } @@ -49,7 +49,7 @@ function runEncryptionBenchmark($nb = 100) * * @param int $nb Number of encryption/decryption to perform */ -function runDecryptionBenchmark($nb = 100) +function runDecryptionBenchmark($nb = 1000) { Assertion::integer($nb, 'The argument must be an integer'); Assertion::greaterThan($nb, 1, 'The argument must be greater than 1'); @@ -64,13 +64,13 @@ function runDecryptionBenchmark($nb = 100) print_r('# AES-GCM DECRYPTION BENCHMARK #'.PHP_EOL); print_r('################################'.PHP_EOL); - $time_start = microtime(true); + $time = -microtime(true); for ($i = 0; $i < $nb; $i++) { AESGCM::decrypt($K, $IV, $C, $A, $T); } - $time_end = microtime(true); - $time = ($time_end - $time_start) / $nb * 1000; - printf('%f milliseconds/decryption (tested on %d decryptions)'.PHP_EOL, $time, $nb); + $time += microtime(true); + $ops = $nb / $time; + printf('%f OPS (tested on %d encryptions)'.PHP_EOL, $ops, $nb); print_r('################################'.PHP_EOL); } diff --git a/tests/IEEE802Test.php b/tests/IEEE802Test.php index 8ca049e..6e798e0 100644 --- a/tests/IEEE802Test.php +++ b/tests/IEEE802Test.php @@ -24,15 +24,14 @@ public function testVectors($K, $P, $A, $IV, $expected_C, $expected_T) { list($C, $T) = AESGCM::encrypt($K, $IV, $P, $A); - $this->assertEquals($C, $expected_C); - $this->assertEquals($T, $expected_T); + $this->assertEquals($expected_C, $C); + $this->assertEquals($expected_T, $T); $computed_P = AESGCM::decrypt($K, $IV, $C, $A, $T); $this->assertEquals($P, $computed_P); - foreach([128, 120, 112, 104, 96] as $tag_length) { - + foreach ([128, 120, 112, 104, 96] as $tag_length) { $c = AESGCM::encryptAndAppendTag($K, $IV, $P, $A, $tag_length); $p = AESGCM::decryptWithAppendedTag($K, $IV, $c, $A, $tag_length); $this->assertEquals($P, $p); diff --git a/tests/NistTest.php b/tests/NistTest.php index 55e28ce..622487c 100644 --- a/tests/NistTest.php +++ b/tests/NistTest.php @@ -24,15 +24,14 @@ public function testVectors($K, $P, $A, $IV, $expected_C, $expected_T) { list($C, $T) = AESGCM::encrypt($K, $IV, $P, $A); - $this->assertEquals($C, $expected_C); - $this->assertEquals($T, $expected_T); + $this->assertEquals($expected_C, $C); + $this->assertEquals($expected_T, $T); $computed_P = AESGCM::decrypt($K, $IV, $C, $A, $T); $this->assertEquals($P, $computed_P); - foreach([128, 120, 112, 104, 96] as $tag_length) { - + foreach ([128, 120, 112, 104, 96] as $tag_length) { $c = AESGCM::encryptAndAppendTag($K, $IV, $P, $A, $tag_length); $p = AESGCM::decryptWithAppendedTag($K, $IV, $c, $A, $tag_length); $this->assertEquals($P, $p);