From fbcf0a8f46461aad061045c8c5b004ccb6a937d5 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 6 Jun 2018 13:57:31 -0400 Subject: [PATCH] Include AES-256-CTR polyfill. Type safety. --- .gitignore | 2 ++ composer.json | 4 +-- psalm.xml | 1 + src/SeedSpring.php | 89 ++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..363611e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea +/composer.lock diff --git a/composer.json b/composer.json index 073b225..b8612c1 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,10 @@ "require": { "php": "^5.6.0|^7.0.0", "paragonie/constant_time_encoding": "^1|^2", - "paragonie/random_compat": "^1|^2" + "paragonie/random_compat": ">=1" }, "require-dev": { "phpunit/phpunit": "5.*", - "vimeo/psalm": "^0|^1" + "vimeo/psalm": "^1" } } diff --git a/psalm.xml b/psalm.xml index b3f25ca..9b94c70 100644 --- a/psalm.xml +++ b/psalm.xml @@ -2,6 +2,7 @@ diff --git a/src/SeedSpring.php b/src/SeedSpring.php index eabee0f..9bba135 100644 --- a/src/SeedSpring.php +++ b/src/SeedSpring.php @@ -20,6 +20,11 @@ final class SeedSpring */ protected $counter; + /** + * @var bool + */ + protected $usePolyfill; + /** * SeedSpring constructor. * @@ -33,6 +38,11 @@ public function __construct($seed = '', $counter = 0) } $this->seed('set', $seed); $this->counter = 0; + $this->usePolyfill = !\in_array( + 'aes-256-ctr', + \openssl_get_cipher_methods(), + true + ); } /** @@ -51,6 +61,10 @@ private function seed($action = 'get', $data = '') $seed[$hash] = $data; return ''; } elseif ($action === 'get') { + /** + * @var array $seed + * @var string $return + */ return (string) $seed[$hash]; } throw new \Error('Unknown action'); @@ -84,6 +98,13 @@ public function seek($position, $seektype = self::SEEK_SET) */ public function getBytes($numBytes) { + if ($this->usePolyfill) { + return self::aes256ctr( + \str_repeat("\0", $numBytes), + $this->seed('get'), + $this->getNonce($numBytes) + ); + } return (string) \openssl_encrypt( \str_repeat("\0", $numBytes), 'aes-128-ctr', @@ -127,6 +148,12 @@ public function getInt($min, $max) * $bytes => the number of random bytes we need * $mask => an integer bitmask (for use with the &) operator * so we can minimize the number of discards + * + * @var int $valueShift + * @var int $mask + * @var int $bytes + * @var int $bits + * @var int $attempts */ $attempts = $bits = $bytes = $mask = $valueShift = 0; @@ -134,7 +161,7 @@ public function getInt($min, $max) * At this point, $range is a positive number greater than 0. It might * overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to * a float and we will lose some precision. - * @var int $range + * @var int|float $range */ $range = $max - $min; @@ -154,6 +181,7 @@ public function getInt($min, $max) * @ref http://3v4l.org/XX9r5 (64-bit) */ $bytes = PHP_INT_SIZE; + /** @var int $mask */ $mask = ~0; } else { /** @@ -190,11 +218,6 @@ public function getInt($min, $max) * Let's grab the necessary number of random bytes */ $randomByteString = $this->getBytes($bytes); - if ($randomByteString === false) { - throw new \Exception( - 'Random number generator failure' - ); - } /** * Let's turn $randomByteString into an integer @@ -246,4 +269,58 @@ protected function getNonce($increment = 0) } return \str_pad($nonce, 16, "\0", STR_PAD_LEFT); } + + /** + * Userland polyfill for AES-256-CTR, using AES-256-ECB + * + * @param string $plaintext + * @param string $key + * @param string $nonce + * @return string + */ + public static function aes256ctr($plaintext, $key, $nonce) + { + if (empty($plaintext)) { + return ''; + } + $length = Binary::safeStrlen($plaintext); + /** @var int $numBlocks */ + $numBlocks = (($length - 1) >> 4) + 1; + $stream = ''; + for ($i = 0; $i < $numBlocks; ++$i) { + $stream .= $nonce; + $nonce = self::ctrNonceIncrease($nonce); + } + /** @var string $xor */ + $xor = \openssl_encrypt( + $stream, + 'aes-256-ecb', + $key, + OPENSSL_RAW_DATA + ); + return (string) ( + $plaintext ^ Binary::safeSubstr($xor, 0, $length) + ); + } + + /** + * Increase a counter nonce, starting with the LSB (big-endian) + * + * @param string $nonce + * @return string + */ + public static function ctrNonceIncrease($nonce) + { + /** @var array $pieces */ + $pieces = \unpack('C*', $nonce); + $c = 0; + ++$pieces[16]; + for ($i = 16; $i > 0; --$i) { + $pieces[$i] += $c; + $c = $pieces[$i] >> 8; + $pieces[$i] &= 0xff; + } + \array_unshift($pieces, \str_repeat('C', 16)); + return (string) \call_user_func_array('pack', $pieces); + } }