v2: Updates

* Simplifies & beautifies everything
* Introduces a new Class system.
* Errors are defaulted to AWS's handler.
* New function names & more efficient handling.
* Should fix a majority of the errors.

Please read the README for more!
This commit is contained in:
Devang Srivastava 2020-09-28 15:32:51 +05:30
commit e6d7753dc8
1095 changed files with 45088 additions and 2911 deletions

View file

@ -0,0 +1,228 @@
<?php
namespace Aws\Crypto\Polyfill;
use Aws\Exception\CryptoPolyfillException;
use InvalidArgumentException;
use RangeException;
/**
* Class AesGcm
*
* This provides a polyfill for AES-GCM encryption/decryption, with caveats:
*
* 1. Only 96-bit nonces are supported.
* 2. Only 128-bit authentication tags are supported. (i.e. non-truncated)
*
* Supports AES key sizes of 128-bit, 192-bit, and 256-bit.
*
* @package Aws\Crypto\Polyfill
*/
class AesGcm
{
use NeedsTrait;
/** @var Key $aesKey */
private $aesKey;
/** @var int $keySize */
private $keySize;
/** @var int $blockSize */
protected $blockSize = 8192;
/**
* AesGcm constructor.
*
* @param Key $aesKey
* @param int $keySize
* @param int $blockSize
*
* @throws CryptoPolyfillException
* @throws InvalidArgumentException
* @throws RangeException
*/
public function __construct(Key $aesKey, $keySize = 256, $blockSize = 8192)
{
/* Preconditions: */
self::needs(
\in_array($keySize, [128, 192, 256], true),
"Key size must be 128, 192, or 256 bits; {$keySize} given",
InvalidArgumentException::class
);
self::needs(
\is_int($blockSize) && $blockSize > 0 && $blockSize <= PHP_INT_MAX,
'Block size must be a positive integer.',
RangeException::class
);
self::needs(
$aesKey->length() << 3 === $keySize,
'Incorrect key size; expected ' . $keySize . ' bits, got ' . ($aesKey->length() << 3) . ' bits.'
);
$this->aesKey = $aesKey;
$this->keySize = $keySize;
}
/**
* Encryption interface for AES-GCM
*
* @param string $plaintext Message to be encrypted
* @param string $nonce Number to be used ONCE
* @param Key $key AES Key
* @param string $aad Additional authenticated data
* @param string &$tag Reference to variable to hold tag
* @param int $keySize Key size (bits)
* @param int $blockSize Block size (bytes) -- How much memory to buffer
* @return string
* @throws InvalidArgumentException
*/
public static function encrypt(
$plaintext,
$nonce,
Key $key,
$aad,
&$tag,
$keySize = 256,
$blockSize = 8192
) {
self::needs(
self::strlen($nonce) === 12,
'Nonce must be exactly 12 bytes',
InvalidArgumentException::class
);
$encryptor = new AesGcm($key, $keySize, $blockSize);
list($aadLength, $gmac) = $encryptor->gmacInit($nonce, $aad);
$ciphertext = \openssl_encrypt(
$plaintext,
"aes-{$encryptor->keySize}-ctr",
$key->get(),
OPENSSL_NO_PADDING | OPENSSL_RAW_DATA,
$nonce . "\x00\x00\x00\x02"
);
/* Calculate auth tag in a streaming fashion to minimize memory usage: */
$ciphertextLength = self::strlen($ciphertext);
for ($i = 0; $i < $ciphertextLength; $i += $encryptor->blockSize) {
$cBlock = new ByteArray(self::substr($ciphertext, $i, $encryptor->blockSize));
$gmac->update($cBlock);
}
$tag = $gmac->finish($aadLength, $ciphertextLength)->toString();
return $ciphertext;
}
/**
* Decryption interface for AES-GCM
*
* @param string $ciphertext Ciphertext to decrypt
* @param string $nonce Number to be used ONCE
* @param Key $key AES key
* @param string $aad Additional authenticated data
* @param string $tag Authentication tag
* @param int $keySize Key size (bits)
* @param int $blockSize Block size (bytes) -- How much memory to buffer
* @return string Plaintext
*
* @throws CryptoPolyfillException
* @throws InvalidArgumentException
*/
public static function decrypt(
$ciphertext,
$nonce,
Key $key,
$aad,
&$tag,
$keySize = 256,
$blockSize = 8192
) {
/* Precondition: */
self::needs(
self::strlen($nonce) === 12,
'Nonce must be exactly 12 bytes',
InvalidArgumentException::class
);
$encryptor = new AesGcm($key, $keySize, $blockSize);
list($aadLength, $gmac) = $encryptor->gmacInit($nonce, $aad);
/* Calculate auth tag in a streaming fashion to minimize memory usage: */
$ciphertextLength = self::strlen($ciphertext);
for ($i = 0; $i < $ciphertextLength; $i += $encryptor->blockSize) {
$cBlock = new ByteArray(self::substr($ciphertext, $i, $encryptor->blockSize));
$gmac->update($cBlock);
}
/* Validate auth tag in constant-time: */
$calc = $gmac->finish($aadLength, $ciphertextLength);
$expected = new ByteArray($tag);
self::needs($calc->equals($expected), 'Invalid authentication tag');
/* Return plaintext if auth tag check succeeded: */
return \openssl_decrypt(
$ciphertext,
"aes-{$encryptor->keySize}-ctr",
$key->get(),
OPENSSL_NO_PADDING | OPENSSL_RAW_DATA,
$nonce . "\x00\x00\x00\x02"
);
}
/**
* Initialize a Gmac object with the nonce and this object's key.
*
* @param string $nonce Must be exactly 12 bytes long.
* @param string|null $aad
* @return array
*/
protected function gmacInit($nonce, $aad = null)
{
$gmac = new Gmac(
$this->aesKey,
$nonce . "\x00\x00\x00\x01",
$this->keySize
);
$aadBlock = new ByteArray($aad);
$aadLength = $aadBlock->count();
$gmac->update($aadBlock);
$gmac->flush();
return [$aadLength, $gmac];
}
/**
* Calculate the length of a string.
*
* Uses the appropriate PHP function without being brittle to
* mbstring.func_overload.
*
* @param string $string
* @return int
*/
protected static function strlen($string)
{
if (\is_callable('\\mb_strlen')) {
return (int) \mb_strlen($string, '8bit');
}
return (int) \strlen($string);
}
/**
* Return a substring of the provided string.
*
* Uses the appropriate PHP function without being brittle to
* mbstring.func_overload.
*
* @param string $string
* @param int $offset
* @param int|null $length
* @return string
*/
protected static function substr($string, $offset = 0, $length = null)
{
if (\is_callable('\\mb_substr')) {
return \mb_substr($string, $offset, $length, '8bit');
} elseif (!\is_null($length)) {
return \substr($string, $offset, $length);
}
return \substr($string, $offset);
}
}

View file

@ -0,0 +1,258 @@
<?php
namespace Aws\Crypto\Polyfill;
/**
* Class ByteArray
* @package Aws\Crypto\Polyfill
*/
class ByteArray extends \SplFixedArray
{
use NeedsTrait;
/**
* ByteArray constructor.
*
* @param int|string|int[] $size
* If you pass in an integer, it creates a ByteArray of that size.
* If you pass in a string or array, it converts it to an array of
* integers between 0 and 255.
* @throws \InvalidArgumentException
*/
public function __construct($size = 0)
{
$arr = null;
// Integer? This behaves just like SplFixedArray.
if (\is_array($size)) {
// Array? We need to pass the count to parent::__construct() then populate
$arr = $size;
$size = \count($arr);
} elseif (\is_string($size)) {
// We need to avoid mbstring.func_overload
if (\is_callable('\\mb_str_split')) {
$tmp = \mb_str_split($size, 1, '8bit');
} else {
$tmp = \str_split($size, 1);
}
// Let's convert each character to an 8-bit integer and store in $arr
$arr = [];
if (!empty($tmp)) {
foreach ($tmp as $t) {
if (strlen($t) < 1) {
continue;
}
$arr []= \unpack('C', $t)[1] & 0xff;
}
}
$size = \count($arr);
} elseif ($size instanceof ByteArray) {
$arr = $size->toArray();
$size = $size->count();
} elseif (!\is_int($size)) {
throw new \InvalidArgumentException(
'Argument must be an integer, string, or array of integers.'
);
}
parent::__construct($size);
if (!empty($arr)) {
// Populate this object with values from constructor argument
foreach ($arr as $i => $v) {
$this->offsetSet($i, $v);
}
} else {
// Initialize to zero.
for ($i = 0; $i < $size; ++$i) {
$this->offsetSet($i, 0);
}
}
}
/**
* Encode an integer into a byte array. 32-bit (unsigned), big endian byte order.
*
* @param int $num
* @return self
*/
public static function enc32be($num)
{
return new ByteArray(\pack('N', $num));
}
/**
* @param ByteArray $other
* @return bool
*/
public function equals(ByteArray $other)
{
if ($this->count() !== $other->count()) {
return false;
}
$d = 0;
for ($i = $this->count() - 1; $i >= 0; --$i) {
$d |= $this[$i] ^ $other[$i];
}
return $d === 0;
}
/**
* @param ByteArray $array
* @return ByteArray
*/
public function exclusiveOr(ByteArray $array)
{
self::needs(
$this->count() === $array->count(),
'Both ByteArrays must be equal size for exclusiveOr()'
);
$out = clone $this;
for ($i = 0; $i < $this->count(); ++$i) {
$out[$i] = $array[$i] ^ $out[$i];
}
return $out;
}
/**
* Returns a new ByteArray incremented by 1 (big endian byte order).
*
* @param int $increase
* @return self
*/
public function getIncremented($increase = 1)
{
$clone = clone $this;
$index = $clone->count();
while ($index > 0) {
--$index;
$tmp = ($clone[$index] + $increase) & PHP_INT_MAX;
$clone[$index] = $tmp & 0xff;
$increase = $tmp >> 8;
}
return $clone;
}
/**
* Sets a value. See SplFixedArray for more.
*
* @param int $index
* @param int $newval
* @return void
*/
public function offsetSet($index, $newval)
{
parent::offsetSet($index, $newval & 0xff);
}
/**
* Return a copy of this ByteArray, bitshifted to the right by 1.
* Used in Gmac.
*
* @return self
*/
public function rshift()
{
$out = clone $this;
for ($j = $this->count() - 1; $j > 0; --$j) {
$out[$j] = (($out[$j - 1] & 1) << 7) | ($out[$j] >> 1);
}
$out[0] >>= 1;
return $out;
}
/**
* Constant-time conditional select. This is meant to read like a ternary operator.
*
* $z = ByteArray::select(1, $x, $y); // $z is equal to $x
* $z = ByteArray::select(0, $x, $y); // $z is equal to $y
*
* @param int $select
* @param ByteArray $left
* @param ByteArray $right
* @return ByteArray
*/
public static function select($select, ByteArray $left, ByteArray $right)
{
self::needs(
$left->count() === $right->count(),
'Both ByteArrays must be equal size for select()'
);
$rightLength = $right->count();
$out = clone $right;
$mask = (-($select & 1)) & 0xff;
for ($i = 0; $i < $rightLength; $i++) {
$out[$i] = $out[$i] ^ (($left[$i] ^ $right[$i]) & $mask);
}
return $out;
}
/**
* Overwrite values of this ByteArray based on a separate ByteArray, with
* a given starting offset and length.
*
* See JavaScript's Uint8Array.set() for more information.
*
* @param ByteArray $input
* @param int $offset
* @param int|null $length
* @return self
*/
public function set(ByteArray $input, $offset = 0, $length = null)
{
self::needs(
is_int($offset) && $offset >= 0,
'Offset must be a positive integer or zero'
);
if (is_null($length)) {
$length = $input->count();
}
$i = 0; $j = $offset;
while ($i < $length && $j < $this->count()) {
$this[$j] = $input[$i];
++$i;
++$j;
}
return $this;
}
/**
* Returns a slice of this ByteArray.
*
* @param int $start
* @param null $length
* @return self
*/
public function slice($start = 0, $length = null)
{
return new ByteArray(\array_slice($this->toArray(), $start, $length));
}
/**
* Mutates the current state and sets all values to zero.
*
* @return void
*/
public function zeroize()
{
for ($i = $this->count() - 1; $i >= 0; --$i) {
$this->offsetSet($i, 0);
}
}
/**
* Converts the ByteArray to a raw binary string.
*
* @return string
*/
public function toString()
{
$count = $this->count();
if ($count === 0) {
return '';
}
$args = $this->toArray();
\array_unshift($args, \str_repeat('C', $count));
// constant-time, PHP <5.6 equivalent to pack('C*', ...$args);
return \call_user_func_array('\\pack', $args);
}
}

View file

@ -0,0 +1,223 @@
<?php
namespace Aws\Crypto\Polyfill;
/**
* Class Gmac
*
* @package Aws\Crypto\Polyfill
*/
class Gmac
{
use NeedsTrait;
const BLOCK_SIZE = 16;
/** @var ByteArray $buf */
protected $buf;
/** @var int $bufLength */
protected $bufLength = 0;
/** @var ByteArray $h */
protected $h;
/** @var ByteArray $hf */
protected $hf;
/** @var Key $key */
protected $key;
/** @var ByteArray $x */
protected $x;
/**
* Gmac constructor.
*
* @param Key $aesKey
* @param string $nonce
* @param int $keySize
*/
public function __construct(Key $aesKey, $nonce, $keySize = 256)
{
$this->buf = new ByteArray(16);
$this->h = new ByteArray(
\openssl_encrypt(
\str_repeat("\0", 16),
"aes-{$keySize}-ecb",
$aesKey->get(),
OPENSSL_RAW_DATA | OPENSSL_NO_PADDING
)
);
$this->key = $aesKey;
$this->x = new ByteArray(16);
$this->hf = new ByteArray(
\openssl_encrypt(
$nonce,
"aes-{$keySize}-ecb",
$aesKey->get(),
OPENSSL_RAW_DATA | OPENSSL_NO_PADDING
)
);
}
/**
* Update the object with some data.
*
* This method mutates this Gmac object.
*
* @param ByteArray $blocks
* @return self
*/
public function update(ByteArray $blocks)
{
if (($blocks->count() + $this->bufLength) < self::BLOCK_SIZE) {
// Write to internal buffer until we reach enough to write.
$this->buf->set($blocks, $this->bufLength);
$this->bufLength += $blocks->count();
return $this;
}
// Process internal buffer first.
if ($this->bufLength > 0) {
// 0 <= state.buf_len < BLOCK_SIZE is an invariant
$tmp = new ByteArray(self::BLOCK_SIZE);
$tmp->set($this->buf->slice(0, $this->bufLength));
$remainingBlockLength = self::BLOCK_SIZE - $this->bufLength;
$tmp->set($blocks->slice(0, $remainingBlockLength), $this->bufLength);
$blocks = $blocks->slice($remainingBlockLength);
$this->bufLength = 0;
$this->x = $this->blockMultiply($this->x->exclusiveOr($tmp), $this->h);
}
// Process full blocks.
$numBlocks = $blocks->count() >> 4;
for ($i = 0; $i < $numBlocks; ++$i) {
$tmp = $blocks->slice($i << 4, self::BLOCK_SIZE);
$this->x = $this->blockMultiply($this->x->exclusiveOr($tmp), $this->h);
}
$last = $numBlocks << 4;
// Zero-fill buffer
for ($i = 0; $i < 16; ++$i) {
$this->buf[$i] = 0;
}
// Feed leftover into buffer.
if ($last < $blocks->count()) {
$tmp = $blocks->slice($last);
$this->buf->set($tmp);
$this->bufLength += ($blocks->count() - $last);
}
return $this;
}
/**
* Finish processing the authentication tag.
*
* This method mutates this Gmac object (effectively resetting it).
*
* @param int $aadLength
* @param int $ciphertextLength
* @return ByteArray
*/
public function finish($aadLength, $ciphertextLength)
{
$lengthBlock = new ByteArray(16);
$state = $this->flush();
// AES-GCM expects bit lengths, not byte lengths.
$lengthBlock->set(ByteArray::enc32be($aadLength >> 29), 0);
$lengthBlock->set(ByteArray::enc32be($aadLength << 3), 4);
$lengthBlock->set(ByteArray::enc32be($ciphertextLength >> 29), 8);
$lengthBlock->set(ByteArray::enc32be($ciphertextLength << 3), 12);
$state->update($lengthBlock);
$output = $state->x->exclusiveOr($state->hf);
// Zeroize the internal values as a best-effort.
$state->buf->zeroize();
$state->x->zeroize();
$state->h->zeroize();
$state->hf->zeroize();
return $output;
}
/**
* Get a specific bit from the provided array, at the given index.
*
* [01234567], 8+[01234567], 16+[01234567], ...
*
* @param ByteArray $x
* @param int $i
* @return int
*/
protected function bit(ByteArray $x, $i)
{
$byte = $i >> 3;
return ($x[$byte] >> ((7 - $i) & 7)) & 1;
}
/**
* Galois Field Multiplication
*
* This function is the critical path that must be constant-time in order to
* avoid timing side-channels against AES-GCM.
*
* The contents of each are always calculated, regardless of the branching
* condition, to prevent another kind of timing leak.
*
* @param ByteArray $x
* @param ByteArray $y
* @return ByteArray
*/
protected function blockMultiply(ByteArray $x, ByteArray $y)
{
static $fieldPolynomial = null;
if (!$fieldPolynomial) {
$fieldPolynomial = new ByteArray([
0xe1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]);
}
self::needs($x->count() === 16, 'Argument 1 must be a ByteArray of exactly 16 bytes');
self::needs($y->count() === 16, 'Argument 2 must be a ByteArray of exactly 16 bytes');
$v = clone $y;
$z = new ByteArray(16);
for ($i = 0; $i < 128; ++$i) {
// if ($b) $z = $z->exclusiveOr($v);
$b = $this->bit($x, $i);
$z = ByteArray::select(
$b,
$z->exclusiveOr($v),
$z
);
// if ($b) $v = $v->exclusiveOr($fieldPolynomial);
$b = $v[15] & 1;
$v = $v->rshift();
$v = ByteArray::select(
$b,
$v->exclusiveOr($fieldPolynomial),
$v
);
}
return $z;
}
/**
* Finish processing any leftover bytes in the internal buffer.
*
* @return self
*/
public function flush()
{
if ($this->bufLength !== 0) {
$this->x = $this->blockMultiply(
$this->x->exclusiveOr($this->buf),
$this->h
);
$this->bufLength = 0;
}
return $this;
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Aws\Crypto\Polyfill;
/**
* Class Key
*
* Wraps a string to keep it hidden from stack traces.
*
* @package Aws\Crypto\Polyfill
*/
class Key
{
/**
* @var string $internalString
*/
private $internalString;
/**
* Hide contents of
*
* @return array
*/
public function __debugInfo()
{
return [];
}
/**
* Key constructor.
* @param string $str
*/
public function __construct($str)
{
$this->internalString = $str;
}
/**
* Defense in depth:
*
* PHP 7.2 includes the Sodium cryptography library, which (among other things)
* exposes a function called sodium_memzero() that we can use to zero-fill strings
* to minimize the risk of sensitive cryptographic materials persisting in memory.
*
* If this function is not available, we XOR the string in-place with itself as a
* best-effort attempt.
*/
public function __destruct()
{
if (extension_loaded('sodium') && function_exists('sodium_memzero')) {
try {
\sodium_memzero($this->internalString);
} catch (\SodiumException $ex) {
// This is a best effort, but does not provide the same guarantees as sodium_memzero():
$this->internalString ^= $this->internalString;
}
}
}
/**
* @return string
*/
public function get()
{
return $this->internalString;
}
/**
* @return int
*/
public function length()
{
if (\is_callable('\\mb_strlen')) {
return (int) \mb_strlen($this->internalString, '8bit');
}
return (int) \strlen($this->internalString);
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Aws\Crypto\Polyfill;
use Aws\Exception\CryptoPolyfillException;
/**
* Trait NeedsTrait
* @package Aws\Crypto\Polyfill
*/
trait NeedsTrait
{
/**
* Preconditions, postconditions, and loop invariants are very
* useful for safe programing. They also document the specifications.
* This function is to help simplify the semantic burden of parsing
* these constructions.
*
* Instead of constructions like
* if (!(GOOD CONDITION)) {
* throw new \Exception('condition not true');
* }
*
* you can write:
* needs(GOOD CONDITION, 'condition not true');
* @param $condition
* @param $errorMessage
* @param null $exceptionClass
*/
public static function needs($condition, $errorMessage, $exceptionClass = null)
{
if (!$condition) {
if (!$exceptionClass) {
$exceptionClass = CryptoPolyfillException::class;
}
throw new $exceptionClass($errorMessage);
}
}
}