Skip to content

Commit

Permalink
Add libCrypto and PHP7.1 AES GCM support (#4)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
Spomky authored Nov 22, 2016
1 parent 6fd0c0f commit b655bef
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 57 deletions.
34 changes: 25 additions & 9 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -116,6 +119,9 @@ However, if the tag is appended at the end of the ciphertext and if it is not 12
```php
<?php

// The values $K, $IV, $C, $A hereafter have the same meaning as above
$TL = 96; // In this example the tag length will be 96 bits

$P = AESGCM::decryptWithAppendedTag($K, $IV, $C, $A, $TL);
```

Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,19 @@
"require": {
"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"
}
}
}
181 changes: 154 additions & 27 deletions src/AESGCM.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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
Expand All @@ -69,29 +134,26 @@ 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.');

$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);
}

/**
Expand All @@ -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);

Expand Down Expand Up @@ -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 '';
Expand All @@ -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);
Expand Down
Loading

0 comments on commit b655bef

Please sign in to comment.