From 7f2a55d4aeb96f468b28db8a4357d6b6b5315ff4 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Tue, 16 Apr 2024 19:31:40 -0400 Subject: [PATCH 1/6] Implement bit-sliced AES block cipher This is the first step towards implementing AEGIS-128L and AEGIS-256 in pure PHP, with the eventual hope of polyfilling those functions ahead of their landing in PHP 8.4's ext-sodium. While we *may* be able to use a table look-up version of AES for the actual bulk data processing for AEGIS, we do _at least_ want a bitsliced implementation of the AES round function for the Init() step. This implementation is based heavily on the work of Thomas Pornin's BearSSL project. See https://www.bearssl.org/constanttime.html#aes --- src/Core/AES.php | 430 +++++++++++++++++++++++++++++++++++ src/Core/AES/Block.php | 327 ++++++++++++++++++++++++++ src/Core/AES/Expanded.php | 10 + src/Core/AES/KeySchedule.php | 78 +++++++ src/Core/Util.php | 2 + tests/unit/AESTest.php | 374 ++++++++++++++++++++++++++++++ 6 files changed, 1221 insertions(+) create mode 100644 src/Core/AES.php create mode 100644 src/Core/AES/Block.php create mode 100644 src/Core/AES/Expanded.php create mode 100644 src/Core/AES/KeySchedule.php create mode 100644 tests/unit/AESTest.php diff --git a/src/Core/AES.php b/src/Core/AES.php new file mode 100644 index 00000000..13c01d81 --- /dev/null +++ b/src/Core/AES.php @@ -0,0 +1,430 @@ +orthogonalize(); + self::sbox($q); + $q->orthogonalize(); + return $q[0] & self::U32_MAX; + } + + /** + * Calculate the key schedule from a given random key + * + * @param string $key + * @return ParagonIE_Sodium_Core_AES_KeySchedule + * @throws SodiumException + */ + public static function keySchedule($key) + { + $key_len = self::strlen($key); + switch ($key_len) { + case 16: + $num_rounds = 10; + break; + case 24: + $num_rounds = 12; + break; + case 32: + $num_rounds = 14; + break; + default: + throw new SodiumException('Invalid key length: ' . $key_len); + } + $skey = array(); + $comp_skey = array(); + $nk = $key_len >> 2; + $nkf = ($num_rounds + 1) << 2; + $tmp = 0; + + for ($i = 0; $i < $nk; ++$i) { + $tmp = self::load_4(self::substr($key, $i << 2, 4)); + $skey[($i << 1)] = $tmp; + $skey[($i << 1) + 1] = $tmp; + } + + for ($i = $nk, $j = 0, $k = 0; $i < $nkf; ++$i) { + if ($j === 0) { + $tmp = (($tmp & 0xff) << 24) | ($tmp >> 8); + $tmp = (self::subWord($tmp) ^ self::$Rcon[$k]) & self::U32_MAX; + } elseif ($nk > 6 && $j === 4) { + $tmp = self::subWord($tmp); + } + $tmp ^= $skey[($i - $nk) << 1]; + $skey[($i << 1)] = $tmp & self::U32_MAX; + $skey[($i << 1) + 1] = $tmp & self::U32_MAX; + if (++$j === $nk) { + /** @psalm-suppress LoopInvalidation */ + $j = 0; + ++$k; + } + } + for ($i = 0; $i < $nkf; $i += 4) { + $q = ParagonIE_Sodium_Core_AES_Block::fromArray( + array_slice($skey, $i << 1, 8) + ); + $q->orthogonalize(); + // We have to overwrite $skey since we're not using C pointers like BearSSL did + for ($j = 0; $j < 8; ++$j) { + $skey[($i << 1) + $j] = $q[$j]; + } + } + for ($i = 0, $j = 0; $i < $nkf; ++$i, $j += 2) { + $comp_skey[$i] = ($skey[$j] & 0x55555555) + | ($skey[$j + 1] & 0xAAAAAAAA); + } + return new ParagonIE_Sodium_Core_AES_KeySchedule($comp_skey, $num_rounds); + } + + /** + * Mutates $q + * + * @param ParagonIE_Sodium_Core_AES_KeySchedule $skey + * @param ParagonIE_Sodium_Core_AES_Block $q + * @param int $offset + * @return void + */ + public static function addRoundKey( + ParagonIE_Sodium_Core_AES_Block $q, + ParagonIE_Sodium_Core_AES_KeySchedule $skey, + $offset = 0 + ) { + $block = $skey->getRoundKey($offset); + for ($j = 0; $j < 8; ++$j) { + $q[$j] = ($q[$j] ^ $block[$j]) & ParagonIE_Sodium_Core_Util::U32_MAX; + } + } + + /** + * This mainly exists for testing, as we need the round key features for AEGIS. + * + * @param string $message + * @param string $key + * @return string + * @throws SodiumException + */ + public static function decryptBlockECB($message, $key) + { + if (self::strlen($message) !== 16) { + throw new SodiumException('decryptBlockECB() expects a 16 byte message'); + } + $skey = self::keySchedule($key)->expand(); + $q = ParagonIE_Sodium_Core_AES_Block::init(); + $q[0] = self::load_4(self::substr($message, 0, 4)); + $q[2] = self::load_4(self::substr($message, 4, 4)); + $q[4] = self::load_4(self::substr($message, 8, 4)); + $q[6] = self::load_4(self::substr($message, 12, 4)); + + $q->orthogonalize(); + self::bitsliceDecryptBlock($skey, $q); + $q->orthogonalize(); + + return self::store32_le($q[0]) . + self::store32_le($q[2]) . + self::store32_le($q[4]) . + self::store32_le($q[6]); + } + + /** + * This mainly exists for testing, as we need the round key features for AEGIS. + * + * @param string $message + * @param string $key + * @return string + * @throws SodiumException + */ + public static function encryptBlockECB($message, $key) + { + if (self::strlen($message) !== 16) { + throw new SodiumException('encryptBlockECB() expects a 16 byte message'); + } + $comp_skey = self::keySchedule($key); + $skey = $comp_skey->expand(); + $q = ParagonIE_Sodium_Core_AES_Block::init(); + $q[0] = self::load_4(self::substr($message, 0, 4)); + $q[2] = self::load_4(self::substr($message, 4, 4)); + $q[4] = self::load_4(self::substr($message, 8, 4)); + $q[6] = self::load_4(self::substr($message, 12, 4)); + + $q->orthogonalize(); + self::bitsliceEncryptBlock($skey, $q); + $q->orthogonalize(); + + return self::store32_le($q[0]) . + self::store32_le($q[2]) . + self::store32_le($q[4]) . + self::store32_le($q[6]); + } + + /** + * Mutates $q + * + * @param ParagonIE_Sodium_Core_AES_Expanded $skey + * @param ParagonIE_Sodium_Core_AES_Block $q + * @return void + */ + public static function bitsliceEncryptBlock( + ParagonIE_Sodium_Core_AES_Expanded $skey, + ParagonIE_Sodium_Core_AES_Block $q + ) { + self::addRoundKey($q, $skey); + for ($u = 1; $u < $skey->getNumRounds(); ++$u) { + self::sbox($q); + $q->shiftRows(); + $q->mixColumns(); + self::addRoundKey($q, $skey, ($u << 3)); + } + self::sbox($q); + $q->shiftRows(); + self::addRoundKey($q, $skey, ($skey->getNumRounds() << 3)); + } + + /** + * @param ParagonIE_Sodium_Core_AES_Expanded $skey + * @param ParagonIE_Sodium_Core_AES_Block $q + * @return void + */ + public static function bitsliceDecryptBlock( + ParagonIE_Sodium_Core_AES_Expanded $skey, + ParagonIE_Sodium_Core_AES_Block $q + ) { + self::addRoundKey($q, $skey, ($skey->getNumRounds() << 3)); + for ($u = $skey->getNumRounds() - 1; $u > 0; --$u) { + $q->inverseShiftRows(); + self::invSbox($q); + self::addRoundKey($q, $skey, ($u << 3)); + $q->inverseMixColumns(); + } + $q->inverseShiftRows(); + self::invSbox($q); + self::addRoundKey($q, $skey, ($u << 3)); + } +} diff --git a/src/Core/AES/Block.php b/src/Core/AES/Block.php new file mode 100644 index 00000000..47ff4a69 --- /dev/null +++ b/src/Core/AES/Block.php @@ -0,0 +1,327 @@ + + */ + protected $values = array(); + + /** + * @var int + */ + protected $size; + + public function __construct($size = 8) + { + parent::__construct($size); + $this->size = $size; + $this->values = array_fill(0, $size, 0); + } + + public static function init() + { + return new self(8); + } + + /** + * @internal You should not use this directly from another application + * + * @param array $array + * @param bool $save_indexes + * @return self + */ + #[ReturnTypeWillChange] + public static function fromArray($array, $save_indexes = null) + { + $count = count($array); + if ($save_indexes) { + $keys = array_keys($array); + } else { + $keys = range(0, $count - 1); + } + $array = array_values($array); + /** @var array $keys */ + + $obj = new ParagonIE_Sodium_Core_AES_Block(); + if ($save_indexes) { + for ($i = 0; $i < $count; ++$i) { + $obj->offsetSet($keys[$i], $array[$i]); + } + } else { + for ($i = 0; $i < $count; ++$i) { + $obj->offsetSet($i, $array[$i]); + } + } + return $obj; + } + + + /** + * @internal You should not use this directly from another application + * + * @param int|null $offset + * @param int $value + * @return void + * @psalm-suppress MixedArrayOffset + */ + #[ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + if (!is_int($value)) { + throw new InvalidArgumentException('Expected an integer'); + } + if (is_null($offset)) { + $this->values[] = $value; + } else { + $this->values[$offset] = $value; + } + } + + /** + * @internal You should not use this directly from another application + * + * @param int $offset + * @return bool + * @psalm-suppress MixedArrayOffset + */ + #[ReturnTypeWillChange] + public function offsetExists($offset) + { + return isset($this->values[$offset]); + } + + /** + * @internal You should not use this directly from another application + * + * @param int $offset + * @return void + * @psalm-suppress MixedArrayOffset + */ + #[ReturnTypeWillChange] + public function offsetUnset($offset) + { + unset($this->values[$offset]); + } + + /** + * @internal You should not use this directly from another application + * + * @param int $offset + * @return int + * @psalm-suppress MixedArrayOffset + */ + #[ReturnTypeWillChange] + public function offsetGet($offset) + { + if (!isset($this->values[$offset])) { + $this->values[$offset] = 0; + } + return (int) ($this->values[$offset]); + } + + /** + * @internal You should not use this directly from another application + * + * @return array + */ + public function __debugInfo() + { + $out = array(); + foreach ($this->values as $v) { + $out[] = str_pad(dechex($v), 8, '0', STR_PAD_LEFT); + } + return array(implode(', ', $out)); + /* + return array(implode(', ', $this->values)); + */ + } + + /** + * @param int $cl low bit mask + * @param int $ch high bit mask + * @param int $s shift + * @param int $x index 1 + * @param int $y index 2 + * @return self + */ + public function swapN($cl, $ch, $s, $x, $y) + { + static $u32mask = ParagonIE_Sodium_Core_Util::U32_MAX; + $a = $this->values[$x] & $u32mask; + $b = $this->values[$y] & $u32mask; + // (x) = (a & cl) | ((b & cl) << (s)); + $this->values[$x] = ($a & $cl) | ((($b & $cl) << $s) & $u32mask); + // (y) = ((a & ch) >> (s)) | (b & ch); + $this->values[$y] = ((($a & $ch) & $u32mask) >> $s) | ($b & $ch); + return $this; + } + + /** + * @param int $x index 1 + * @param int $y index 2 + * @return self + */ + public function swap2($x, $y) + { + return $this->swapN(0x55555555, 0xAAAAAAAA, 1, $x, $y); + } + + /** + * @param int $x index 1 + * @param int $y index 2 + * @return self + */ + public function swap4($x, $y) + { + return $this->swapN(0x33333333, 0xCCCCCCCC, 2, $x, $y); + } + + /** + * @param int $x index 1 + * @param int $y index 2 + * @return self + */ + public function swap8($x, $y) + { + return $this->swapN(0x0F0F0F0F, 0xF0F0F0F0, 4, $x, $y); + } + + /** + * @return self + */ + public function orthogonalize() + { + return $this + ->swap2(0, 1) + ->swap2(2, 3) + ->swap2(4, 5) + ->swap2(6, 7) + + ->swap4(0, 2) + ->swap4(1, 3) + ->swap4(4, 6) + ->swap4(5, 7) + + ->swap8(0, 4) + ->swap8(1, 5) + ->swap8(2, 6) + ->swap8(3, 7); + } + + /** + * @return self + */ + public function shiftRows() + { + for ($i = 0; $i < 8; ++$i) { + $x = $this->values[$i] & ParagonIE_Sodium_Core_Util::U32_MAX; + $this->values[$i] = ( + ($x & 0x000000FF) + | (($x & 0x0000FC00) >> 2) | (($x & 0x00000300) << 6) + | (($x & 0x00F00000) >> 4) | (($x & 0x000F0000) << 4) + | (($x & 0xC0000000) >> 6) | (($x & 0x3F000000) << 2) + ) & ParagonIE_Sodium_Core_Util::U32_MAX; + } + return $this; + } + + /** + * @param int $x + * @return int + */ + public static function rotr16($x) + { + return (($x << 16) & ParagonIE_Sodium_Core_Util::U32_MAX) | ($x >> 16); + } + + /** + * @return self + */ + public function mixColumns() + { + $q0 = $this->values[0]; + $q1 = $this->values[1]; + $q2 = $this->values[2]; + $q3 = $this->values[3]; + $q4 = $this->values[4]; + $q5 = $this->values[5]; + $q6 = $this->values[6]; + $q7 = $this->values[7]; + $r0 = (($q0 >> 8) | ($q0 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r1 = (($q1 >> 8) | ($q1 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r2 = (($q2 >> 8) | ($q2 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r3 = (($q3 >> 8) | ($q3 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r4 = (($q4 >> 8) | ($q4 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r5 = (($q5 >> 8) | ($q5 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r6 = (($q6 >> 8) | ($q6 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r7 = (($q7 >> 8) | ($q7 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + + $this->values[0] = $q7 ^ $r7 ^ $r0 ^ self::rotr16($q0 ^ $r0); + $this->values[1] = $q0 ^ $r0 ^ $q7 ^ $r7 ^ $r1 ^ self::rotr16($q1 ^ $r1); + $this->values[2] = $q1 ^ $r1 ^ $r2 ^ self::rotr16($q2 ^ $r2); + $this->values[3] = $q2 ^ $r2 ^ $q7 ^ $r7 ^ $r3 ^ self::rotr16($q3 ^ $r3); + $this->values[4] = $q3 ^ $r3 ^ $q7 ^ $r7 ^ $r4 ^ self::rotr16($q4 ^ $r4); + $this->values[5] = $q4 ^ $r4 ^ $r5 ^ self::rotr16($q5 ^ $r5); + $this->values[6] = $q5 ^ $r5 ^ $r6 ^ self::rotr16($q6 ^ $r6); + $this->values[7] = $q6 ^ $r6 ^ $r7 ^ self::rotr16($q7 ^ $r7); + return $this; + } + + /** + * @return self + */ + public function inverseMixColumns() + { + $q0 = $this->values[0]; + $q1 = $this->values[1]; + $q2 = $this->values[2]; + $q3 = $this->values[3]; + $q4 = $this->values[4]; + $q5 = $this->values[5]; + $q6 = $this->values[6]; + $q7 = $this->values[7]; + $r0 = (($q0 >> 8) | ($q0 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r1 = (($q1 >> 8) | ($q1 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r2 = (($q2 >> 8) | ($q2 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r3 = (($q3 >> 8) | ($q3 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r4 = (($q4 >> 8) | ($q4 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r5 = (($q5 >> 8) | ($q5 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r6 = (($q6 >> 8) | ($q6 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $r7 = (($q7 >> 8) | ($q7 << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + + $this->values[0] = $q5 ^ $q6 ^ $q7 ^ $r0 ^ $r5 ^ $r7 ^ self::rotr16($q0 ^ $q5 ^ $q6 ^ $r0 ^ $r5); + $this->values[1] = $q0 ^ $q5 ^ $r0 ^ $r1 ^ $r5 ^ $r6 ^ $r7 ^ self::rotr16($q1 ^ $q5 ^ $q7 ^ $r1 ^ $r5 ^ $r6); + $this->values[2] = $q0 ^ $q1 ^ $q6 ^ $r1 ^ $r2 ^ $r6 ^ $r7 ^ self::rotr16($q0 ^ $q2 ^ $q6 ^ $r2 ^ $r6 ^ $r7); + $this->values[3] = $q0 ^ $q1 ^ $q2 ^ $q5 ^ $q6 ^ $r0 ^ $r2 ^ $r3 ^ $r5 ^ self::rotr16($q0 ^ $q1 ^ $q3 ^ $q5 ^ $q6 ^ $q7 ^ $r0 ^ $r3 ^ $r5 ^ $r7); + $this->values[4] = $q1 ^ $q2 ^ $q3 ^ $q5 ^ $r1 ^ $r3 ^ $r4 ^ $r5 ^ $r6 ^ $r7 ^ self::rotr16($q1 ^ $q2 ^ $q4 ^ $q5 ^ $q7 ^ $r1 ^ $r4 ^ $r5 ^ $r6); + $this->values[5] = $q2 ^ $q3 ^ $q4 ^ $q6 ^ $r2 ^ $r4 ^ $r5 ^ $r6 ^ $r7 ^ self::rotr16($q2 ^ $q3 ^ $q5 ^ $q6 ^ $r2 ^ $r5 ^ $r6 ^ $r7); + $this->values[6] = $q3 ^ $q4 ^ $q5 ^ $q7 ^ $r3 ^ $r5 ^ $r6 ^ $r7 ^ self::rotr16($q3 ^ $q4 ^ $q6 ^ $q7 ^ $r3 ^ $r6 ^ $r7); + $this->values[7] = $q4 ^ $q5 ^ $q6 ^ $r4 ^ $r6 ^ $r7 ^ self::rotr16($q4 ^ $q5 ^ $q7 ^ $r4 ^ $r7); + return $this; + } + + /** + * @return self + */ + public function inverseShiftRows() + { + for ($i = 0; $i < 8; ++$i) { + $x = $this->values[$i]; + $this->values[$i] = ParagonIE_Sodium_Core_Util::U32_MAX & ( + ($x & 0x000000FF) + | (($x & 0x00003F00) << 2) | (($x & 0x0000C000) >> 6) + | (($x & 0x000F0000) << 4) | (($x & 0x00F00000) >> 4) + | (($x & 0x03000000) << 6) | (($x & 0xFC000000) >> 2) + ); + } + return $this; + } +} diff --git a/src/Core/AES/Expanded.php b/src/Core/AES/Expanded.php new file mode 100644 index 00000000..e8b84924 --- /dev/null +++ b/src/Core/AES/Expanded.php @@ -0,0 +1,10 @@ + $skey -- has size 120 */ + protected $skey; + + /** @var bool $expanded */ + protected $expanded = false; + + /** @var int $numRounds */ + private $numRounds; + + /** + * @param array $skey + * @param int $numRounds + */ + public function __construct(array $skey, $numRounds = 10) + { + $this->skey = $skey; + $this->numRounds = $numRounds; + } + + /** + * Get a value at an arbitrary index. Mostly used for unit testing. + * + * @param int $i + * @return int + */ + public function get($i) + { + return $this->skey[$i]; + } + + /** + * @return int + */ + public function getNumRounds() + { + return $this->numRounds; + } + + /** + * @param int $offset + * @return ParagonIE_Sodium_Core_AES_Block + */ + public function getRoundKey($offset) + { + return ParagonIE_Sodium_Core_AES_Block::fromArray( + array_slice($this->skey, $offset, 8) + ); + } + + /** + * Return an expanded key schedule + * + * @return ParagonIE_Sodium_Core_AES_Expanded + */ + public function expand() + { + $exp = new ParagonIE_Sodium_Core_AES_Expanded( + array_fill(0, 120, 0), + $this->numRounds + ); + $n = ($exp->numRounds + 1) << 2; + for ($u = 0, $v = 0; $u < $n; ++$u, $v += 2) { + $x = $y = $this->skey[$u]; + $x &= 0x55555555; + $exp->skey[$v] = ($x | ($x << 1)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $y &= 0xAAAAAAAA; + $exp->skey[$v + 1] = ($y | ($y >> 1)) & ParagonIE_Sodium_Core_Util::U32_MAX; + } + return $exp; + } +} diff --git a/src/Core/Util.php b/src/Core/Util.php index 73e463f2..2903beff 100644 --- a/src/Core/Util.php +++ b/src/Core/Util.php @@ -9,6 +9,8 @@ */ abstract class ParagonIE_Sodium_Core_Util { + const U32_MAX = 0xFFFFFFFF; + /** * @param int $integer * @param int $size (16, 32, 64) diff --git a/tests/unit/AESTest.php b/tests/unit/AESTest.php new file mode 100644 index 00000000..b37c78d9 --- /dev/null +++ b/tests/unit/AESTest.php @@ -0,0 +1,374 @@ + $v) { + $return []= array($i, $v); + } + return $return; + } + + public function testSboxKnownGood() + { + $q = ParagonIE_Sodium_Core_AES_Block::fromArray(array( + 0x00010203, + 0x04050607, + 0x08090a0b, + 0x0c0d0e0f, + 0x89697676, // YELL + 0x79872083, // OW S + 0x85667765, // UBMA + 0x82737869, // RINE + )); + $q->orthogonalize(); + ParagonIE_Sodium_Core_AES::sbox($q); + $q->orthogonalize(); + $this->assertSame('637c777b', dechex($q[0])); + } + + /** + * @dataProvider sboxProvider + */ + public function testSBox($input, $expected) + { + $q = ParagonIE_Sodium_Core_AES_Block::init(); + for ($i = 0; $i < 8; ++$i) { + $q[$i] = ($input | ($input << 8) | ($input << 16) | ($input << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + } + + $q->orthogonalize(); + ParagonIE_Sodium_Core_AES::sbox($q); + $q->orthogonalize(); + + $this->assertSame(dechex($expected), dechex($q[0] & 0xff)); + $this->assertSame($expected, $q[0] & 0xff); + + $q2 = clone $q; + $q2->orthogonalize(); + ParagonIE_Sodium_Core_AES::invSbox($q2); + $q2->orthogonalize(); + for ($i = 0; $i < 8; ++$i) { + $x = ($input | ($input << 8) | ($input << 16) | ($input << 24)) & ParagonIE_Sodium_Core_Util::U32_MAX; + $this->assertSame($x, $q2[$i]); + } + } + + public function orthoProvider() + { + return array( + array( + array(0x03020100, 0x03020100, 0x07060504, 0x07060504, 0x0b0a0908, 0x0b0a0908, 0x0f0e0d0c, 0x0f0e0d0c), + array(0xff00ff00, 0xffff0000, 0xcccccccc, 0xf0f0f0f0, 0x00000000, 0x00000000, 0x00000000, 0x00000000) + ), + array( + array(0xfd74aad6, 0xfd74aad6, 0xfa72afd2, 0xfa72afd2, 0xf178a6da, 0xf178a6da, 0xfe76abd6, 0xfe76abd6), + array(0x3300cc00, 0xccccffff, 0xc3c33cc3, 0xcf30cf30, 0xffff00ff, 0xffffff00, 0xffff00ff, 0xff00ffff) + ) + ); + } + + /** + * @dataProvider orthoProvider + */ + public function testOrtho(array $input, array $expected) + { + $q = ParagonIE_Sodium_Core_AES_Block::fromArray($input); + $q->orthogonalize(); + for ($i = 0; $i < 8; ++$i) { + $this->assertSame($expected[$i], $q[$i], 'ortogonalize test'); + } + } + + /** + * @covers ParagonIE_Sodium_Core_AES::addRoundKey + */ + public function testAddRoundKey() + { + $q = ParagonIE_Sodium_Core_AES_Block::fromArray(array(1, 2, 3, 4, 5, 6, 7, 8)); + $schedule = ParagonIE_Sodium_Core_AES::keySchedule('sodiumcompat1.21'); + ParagonIE_Sodium_Core_AES::addRoundKey($q, $schedule); + $rk = $schedule->getRoundKey(0); + for ($i = 0; $i < 8; ++$i) { + $this->assertSame($rk[$i] ^ ($i + 1), $q[$i]); + } + } + + /** + * @covers ParagonIE_Sodium_Core_AES_Block::shiftRows + */ + public function testShiftRows() + { + $q = ParagonIE_Sodium_Core_AES_Block::fromArray(array( + 0x11111111, 0x22222222, 0x33333333, 0x44444444, + 0x01234567, 0xfedcba98, 0x00010203, 0xfffefdfc, + )); + $_q = clone $q; + $q->orthogonalize()->shiftRows()->orthogonalize(); + $this->assertSame(0x00233311, $q[0]); + + // Ensure the inverse operation is valid + $q->orthogonalize()->inverseShiftRows()->orthogonalize(); + for ($i = 0; $i < 8; ++$i) { + $this->assertSame($_q[$i], $q[$i]); + } + } + + public function testSubWord() + { + $this->assertSame(0xfe76abd7, ParagonIE_Sodium_Core_AES::subWord(0x0c0f0e0d)); + } + + public function testMixColumns() + { + $q = ParagonIE_Sodium_Core_AES_Block::fromArray(array( + 0xf8be2b17, 0xcaba63cb, 0x67b2a090, 0x8988c2d4, 0x1a70b1e8, 0xcabf96eb, 0x7ae7f79b, 0x615d60d8 + )); + $q->mixColumns(); + + $this->assertSame(0x3bf86cd5, $q[0]); + $this->assertSame(0x44181397, $q[1]); + $this->assertSame(0x83279cdd, $q[2]); + $this->assertSame(0xd076fa4b, $q[3]); + $this->assertSame(0xcd7ef575, $q[4]); + $this->assertSame(0x30dd5fba, $q[5]); + $this->assertSame(0xaa632f17, $q[6]); + $this->assertSame(0x0444f430, $q[7]); + + $q->inverseMixColumns(); + + $this->assertSame(0xf8be2b17, $q[0]); + $this->assertSame(0xcaba63cb, $q[1]); + $this->assertSame(0x67b2a090, $q[2]); + $this->assertSame(0x8988c2d4, $q[3]); + $this->assertSame(0x1a70b1e8, $q[4]); + $this->assertSame(0xcabf96eb, $q[5]); + $this->assertSame(0x7ae7f79b, $q[6]); + $this->assertSame(0x615d60d8, $q[7]); + } + + public function testKeySchedule() + { + $ks = ParagonIE_Sodium_Core_AES::keySchedule(sodium_hex2bin("000102030405060708090a0b0c0d0e0f")); + $expect = array( + 0xffaa5500, 0xe4e4e4e4, 0x00000000, 0x00000000, 0x9988eeaa, 0xcb619e61, 0xffffaa55, 0xff55aaff, + 0x87d73622, 0xc21f2cb5, 0xccccddbb, 0xccbb2266, 0x7ec45b0a, 0x3ff9a371, 0x3cc39327, 0x698d5f1e, + 0x4c6b4757, 0xf3cd75e5, 0xf33f2ff4, 0xe28313f9, 0xbcb7be44, 0x9a3c4ecb, 0x9aa6f69b, 0x9f2afa98, + 0xd9dbd9be, 0x87f31697, 0x87749b2d, 0x7908982d, 0x92386d8c, 0x7e30aed1, 0x811b8709, 0x18fd875c, + 0x85a7e383, 0x19a5dcc5, 0x7ff8d4a8, 0xf86681b9, 0x81de3580, 0xf874c6c1, 0x196791dd, 0x67b42a8d, + 0x7f39f17f, 0x671b94c0, 0x07e18539, 0xe124087c, 0xae3e1b15, 0x258d57b9, 0xfdc705c5, 0xf8ed6a99, + 0x4cf0d767, 0xed0ccc30, 0xf42551fb, 0xa7ed7a77, 0xf26bc1e0, 0xd7e69659, 0x2a21939c, 0xd2ccf905, + 0x4769bcd2, 0xaa6570e2, 0x5e402119, 0xf9ad5b6e + ); + for ($i = 0; $i < 44; ++$i) { + $this->assertSame( + sprintf('0x%08x', $expect[$i]), + sprintf('0x%08x', $ks->get($i)), + 'key schedule u = ' . $i + ); + $this->assertSame($expect[$i], $ks->get($i), 'key schedule u = ' . $i); + } + $sk_expect = array( + 0xff00ff00, 0xffff0000, 0xcccccccc, 0xf0f0f0f0, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x3300cc00, 0xccccffff, 0xc3c33cc3, 0xcf30cf30, 0xffff00ff, 0xffffff00, 0xffff00ff, 0xff00ffff, + 0x0fff3c00, 0xc3c33333, 0xc03f0c3f, 0xc30f3cf0, 0xccccff33, 0xccccccff, 0xcc3300cc, 0xccff3333, + 0xfcccf300, 0x3fc00f0f, 0x3ff303f3, 0x3ffcf330, 0x3cc3330f, 0x3cc3c333, 0xc30fff3c, 0x3ccc0f0f, + 0xccc3cfff, 0x0c3f0303, 0xf3cfffcf, 0xf3cc30f0, 0xf33f0ffc, 0xf33f3ff0, 0xc00333f3, 0xf3c303fc, + 0x3c3f3ccc, 0xfcf3ff00, 0x303cccc3, 0xcf3c0fcf, 0x300cfc33, 0xcff3f3cf, 0x3f00f030, 0xcf3fffcc, + 0xf3f3f33c, 0xcccfccff, 0x0ff33c3f, 0xc3f303c3, 0x0ffc330f, 0xc330cf3c, 0xf300300f, 0x3c0ccc3c, + 0x3030cf0c, 0xc33c3ccc, 0xfc300cf3, 0x3f30ffc0, 0x03330f03, 0xc00fc30c, 0x30ff0ffc, 0x0cfcc30c, + 0x0f0fc303, 0xc0f3f3c3, 0x330ffccf, 0x0cf0ccc0, 0xfff0fc00, 0x3ffcc0fc, 0xf0cc0333, 0xfc33c0fc, + 0x03fc3f00, 0xc0cf30c0, 0xf0fcccc3, 0xfc30c3c0, 0x33cf33ff, 0x0c33c0cc, 0xcf3c000f, 0x33f03fcc, + 0xff33f3ff, 0x3f3cf03f, 0xcf333cc0, 0x330fc0c0, 0x0fc30f33, 0x03f0c03c, 0xc30c00fc, 0xf0300c3c, + 0xb828aeed, 0x00007f2b, 0x00000006, 0x00000000, 0xb841b780, 0x00007f2b, 0x0000000d, 0x00000000, + 0x0f6832a0, 0x0000558a, 0x00000d68, 0x00000000, 0xb828c9e1, 0x00007f2b, 0xb841b780, 0x00007f2b, + 0xb841b780, 0x00007f2b, 0xb8417600, 0x00007f2b, 0x03c1dbd8, 0x00007ffd, 0x0e840083, 0x0000558a, + 0x0e87b7d8, 0x0000558a, 0xb858e040, 0x00007f2b, 0x77160b00, 0x852ca90b, 0x0e83a0e7, 0x0000558a + ); + $expanded = $ks->expand(); + for ($i = 0; $i < 44; ++$i) { + $this->assertSame( + sprintf('0x%08x', $sk_expect[$i]), + sprintf('0x%08x', $expanded->get($i)), + 'key schedule u = ' . $i + ); + $this->assertSame($expect[$i], $ks->get($i), 'key schedule u = ' . $i); + } + } + + public function testSkeyExpand() + { + // "000102030405060708090a0b0c0d0e0f" + $ks = new ParagonIE_Sodium_Core_AES_KeySchedule(array( + 0xffaa5500, 0xe4e4e4e4, 0x00000000, 0x00000000, 0x9988eeaa, 0xcb619e61, 0xffffaa55, 0xff55aaff, + 0x87d73622, 0xc21f2cb5, 0xccccddbb, 0xccbb2266, 0x7ec45b0a, 0x3ff9a371, 0x3cc39327, 0x698d5f1e, + 0x4c6b4757, 0xf3cd75e5, 0xf33f2ff4, 0xe28313f9, 0xbcb7be44, 0x9a3c4ecb, 0x9aa6f69b, 0x9f2afa98, + 0xd9dbd9be, 0x87f31697, 0x87749b2d, 0x7908982d, 0x92386d8c, 0x7e30aed1, 0x811b8709, 0x18fd875c, + 0x85a7e383, 0x19a5dcc5, 0x7ff8d4a8, 0xf86681b9, 0x81de3580, 0xf874c6c1, 0x196791dd, 0x67b42a8d, + 0x7f39f17f, 0x671b94c0, 0x07e18539, 0xe124087c + ), 10); + $exp = $ks->expand(); + $values = array( + 0xff00ff00, 0xffff0000, 0xcccccccc, 0xf0f0f0f0, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x3300cc00, 0xccccffff, 0xc3c33cc3, 0xcf30cf30, 0xffff00ff, 0xffffff00, 0xffff00ff, 0xff00ffff, + 0x0fff3c00, 0xc3c33333, 0xc03f0c3f, 0xc30f3cf0, 0xccccff33, 0xccccccff, 0xcc3300cc, 0xccff3333, + 0xfcccf300, 0x3fc00f0f, 0x3ff303f3, 0x3ffcf330, 0x3cc3330f, 0x3cc3c333, 0xc30fff3c, 0x3ccc0f0f, + 0xccc3cfff, 0x0c3f0303, 0xf3cfffcf, 0xf3cc30f0, 0xf33f0ffc, 0xf33f3ff0, 0xc00333f3, 0xf3c303fc, + 0x3c3f3ccc, 0xfcf3ff00, 0x303cccc3, 0xcf3c0fcf, 0x300cfc33, 0xcff3f3cf, 0x3f00f030, 0xcf3fffcc, + 0xf3f3f33c, 0xcccfccff, 0x0ff33c3f, 0xc3f303c3, 0x0ffc330f, 0xc330cf3c, 0xf300300f, 0x3c0ccc3c, + 0x3030cf0c, 0xc33c3ccc, 0xfc300cf3, 0x3f30ffc0, 0x03330f03, 0xc00fc30c, 0x30ff0ffc, 0x0cfcc30c, + 0x0f0fc303, 0xc0f3f3c3, 0x330ffccf, 0x0cf0ccc0, 0xfff0fc00, 0x3ffcc0fc, 0xf0cc0333, 0xfc33c0fc, + 0x03fc3f00, 0xc0cf30c0, 0xf0fcccc3, 0xfc30c3c0, 0x33cf33ff, 0x0c33c0cc, 0xcf3c000f, 0x33f03fcc, + 0xff33f3ff, 0x3f3cf03f, 0xcf333cc0, 0x330fc0c0, 0x0fc30f33, 0x03f0c03c, 0xc30c00fc, 0xf0300c3c + ); + for ($i = 0; $i < 88; ++$i) { + $this->assertSame($values[$i], $exp->get($i), 'skey - index ' . $i); + } + } + + /** + * @dataProvider aes128ecbProvider + * @covers ParagonIE_Sodium_Core_AES::encryptBlockECB + */ + public function testEncryptBlock128ECB($key_hex, $pt_hex, $ct_hex) + { + $key = ParagonIE_Sodium_Core_Util::hex2bin($key_hex); + + for ($i = 0; $i < strlen($pt_hex); $i += 32) { + $pt = ParagonIE_Sodium_Core_Util::hex2bin(substr($pt_hex, $i, 32)); + $ct = ParagonIE_Sodium_Core_Util::hex2bin(substr($ct_hex, $i, 32)); + $actual = ParagonIE_Sodium_Core_AES::encryptBlockECB($pt, $key); + $this->assertSame($actual, $ct, 'AES-128 test vector failed (encryption)'); + $decrypted = ParagonIE_Sodium_Core_AES::decryptBlockECB($ct, $key); + $this->assertSame($decrypted, $pt, 'AES-128 test vector failed (decryption)'); + } + } + /** + * @dataProvider aes192ecbProvider + * @covers ParagonIE_Sodium_Core_AES::encryptBlockECB + */ + public function testEncryptBlock192ECB($key_hex, $pt_hex, $ct_hex) + { + $key = ParagonIE_Sodium_Core_Util::hex2bin($key_hex); + + for ($i = 0; $i < strlen($pt_hex); $i += 32) { + $pt = ParagonIE_Sodium_Core_Util::hex2bin(substr($pt_hex, $i, 32)); + $ct = ParagonIE_Sodium_Core_Util::hex2bin(substr($ct_hex, $i, 32)); + $actual = ParagonIE_Sodium_Core_AES::encryptBlockECB($pt, $key); + $this->assertSame($actual, $ct, 'AES-192 test vector failed (encryption)'); + $decrypted = ParagonIE_Sodium_Core_AES::decryptBlockECB($ct, $key); + $this->assertSame($decrypted, $pt, 'AES-192 test vector failed (decryption)'); + } + } + + /** + * @dataProvider aes256ecbProvider + * @covers ParagonIE_Sodium_Core_AES::encryptBlockECB + */ + public function testEncryptBlock256ECB($key_hex, $pt_hex, $ct_hex) + { + $key = ParagonIE_Sodium_Core_Util::hex2bin($key_hex); + + for ($i = 0; $i < strlen($pt_hex); $i += 32) { + $pt = ParagonIE_Sodium_Core_Util::hex2bin(substr($pt_hex, $i, 32)); + $ct = ParagonIE_Sodium_Core_Util::hex2bin(substr($ct_hex, $i, 32)); + $actual = ParagonIE_Sodium_Core_AES::encryptBlockECB($pt, $key); + $this->assertSame($actual, $ct, 'AES-256 test vector failed (encryption)'); + $decrypted = ParagonIE_Sodium_Core_AES::decryptBlockECB($ct, $key); + $this->assertSame($decrypted, $pt, 'AES-256 test vector failed (decryption)'); + } + } +} From 67fadea78b6200d863d52e00d1bbe29419e6f642 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 17 Apr 2024 03:11:33 -0400 Subject: [PATCH 2/6] Implement AEGIS-128L and AEGIS-256 --- src/Core/AEGIS/State128L.php | 275 ++++++++++++++++++++++++ src/Core/AEGIS/State256.php | 232 +++++++++++++++++++++ src/Core/AEGIS128L.php | 119 +++++++++++ src/Core/AEGIS256.php | 118 +++++++++++ src/Core/AES.php | 34 +++ src/Core/Util.php | 22 ++ tests/unit/AEGISTest.php | 390 +++++++++++++++++++++++++++++++++++ tests/unit/AESTest.php | 12 ++ tests/unit/UtilTest.php | 17 ++ 9 files changed, 1219 insertions(+) create mode 100644 src/Core/AEGIS/State128L.php create mode 100644 src/Core/AEGIS/State256.php create mode 100644 src/Core/AEGIS128L.php create mode 100644 src/Core/AEGIS256.php create mode 100644 tests/unit/AEGISTest.php diff --git a/src/Core/AEGIS/State128L.php b/src/Core/AEGIS/State128L.php new file mode 100644 index 00000000..0bbf99d2 --- /dev/null +++ b/src/Core/AEGIS/State128L.php @@ -0,0 +1,275 @@ + $state */ + protected $state; + public function __construct() + { + $this->state = array_fill(0, 8, ''); + } + + /** + * @internal Only use this for unit tests! + * @return string[] + */ + public function getState() + { + return array_values($this->state); + } + + /** + * @param array $input + * @return self + * @throws SodiumException + * + * @internal Only for unit tests + */ + public static function initForUnitTests(array $input) + { + if (count($input) < 8) { + throw new SodiumException('invalid input'); + } + $state = new self(); + for ($i = 0; $i < 8; ++$i) { + $state->state[$i] = $input[$i]; + } + return $state; + } + + /** + * @param string $key + * @param string $nonce + * @return self + */ + public static function init($key, $nonce) + { + $state = new self(); + + // S0 = key ^ nonce + $state->state[0] = $key ^ $nonce; + // S1 = C1 + $state->state[1] = SODIUM_COMPAT_AEGIS_C1; + // S2 = C0 + $state->state[2] = SODIUM_COMPAT_AEGIS_C0; + // S3 = C1 + $state->state[3] = SODIUM_COMPAT_AEGIS_C1; + // S4 = key ^ nonce + $state->state[4] = $key ^ $nonce; + // S5 = key ^ C0 + $state->state[5] = $key ^ SODIUM_COMPAT_AEGIS_C0; + // S6 = key ^ C1 + $state->state[6] = $key ^ SODIUM_COMPAT_AEGIS_C1; + // S7 = key ^ C0 + $state->state[7] = $key ^ SODIUM_COMPAT_AEGIS_C0; + + // Repeat(10, Update(nonce, key)) + for ($i = 0; $i < 10; ++$i) { + $state->update($nonce, $key); + } + return $state; + } + + /** + * @param string $ai + * @return self + */ + public function absorb($ai) + { + if (ParagonIE_Sodium_Core_Util::strlen($ai) !== 32) { + throw new SodiumException('Input must be two AES blocks in size'); + } + $t0 = ParagonIE_Sodium_Core_Util::substr($ai, 0, 16); + $t1 = ParagonIE_Sodium_Core_Util::substr($ai, 16, 16); + return $this->update($t0, $t1); + } + + + /** + * @param string $ci + * @return string + * @throws SodiumException + */ + public function dec($ci) + { + if (ParagonIE_Sodium_Core_Util::strlen($ci) !== 32) { + throw new SodiumException('Input must be two AES blocks in size'); + } + + // z0 = S6 ^ S1 ^ (S2 & S3) + $z0 = $this->state[6] + ^ $this->state[1] + ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); + // z1 = S2 ^ S5 ^ (S6 & S7) + $z1 = $this->state[2] + ^ $this->state[5] + ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[6], $this->state[7]); + + // t0, t1 = Split(xi, 128) + $t0 = ParagonIE_Sodium_Core_Util::substr($ci, 0, 16); + $t1 = ParagonIE_Sodium_Core_Util::substr($ci, 16, 16); + + // out0 = t0 ^ z0 + // out1 = t1 ^ z1 + $out0 = $t0 ^ $z0; + $out1 = $t1 ^ $z1; + + // Update(out0, out1) + // xi = out0 || out1 + $this->update($out0, $out1); + return $out0 . $out1; + } + + /** + * @param string $cn + * @return string + */ + public function decPartial($cn) + { + $len = ParagonIE_Sodium_Core_Util::strlen($cn); + + // z0 = S6 ^ S1 ^ (S2 & S3) + $z0 = $this->state[6] + ^ $this->state[1] + ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); + // z1 = S2 ^ S5 ^ (S6 & S7) + $z1 = $this->state[2] + ^ $this->state[5] + ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[6], $this->state[7]); + + // t0, t1 = Split(ZeroPad(cn, 256), 128) + $cn = str_pad($cn, 32, "\0", STR_PAD_RIGHT); + $t0 = ParagonIE_Sodium_Core_Util::substr($cn, 0, 16); + $t1 = ParagonIE_Sodium_Core_Util::substr($cn, 16, 16); + // out0 = t0 ^ z0 + // out1 = t1 ^ z1 + $out0 = $t0 ^ $z0; + $out1 = $t1 ^ $z1; + + // xn = Truncate(out0 || out1, |cn|) + $xn = ParagonIE_Sodium_Core_Util::substr($out0 . $out1, 0, $len); + + // v0, v1 = Split(ZeroPad(xn, 256), 128) + $padded = str_pad($xn, 32, "\0", STR_PAD_RIGHT); + $v0 = ParagonIE_Sodium_Core_Util::substr($padded, 0, 16); + $v1 = ParagonIE_Sodium_Core_Util::substr($padded, 16, 16); + // Update(v0, v1) + $this->update($v0, $v1); + + // return xn + return $xn; + } + + /** + * @param string $xi + * @return string + * @throws SodiumException + */ + public function enc($xi) + { + if (ParagonIE_Sodium_Core_Util::strlen($xi) !== 32) { + throw new SodiumException('Input must be two AES blocks in size'); + } + + // z0 = S6 ^ S1 ^ (S2 & S3) + $z0 = $this->state[6] + ^ $this->state[1] + ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); + // z1 = S2 ^ S5 ^ (S6 & S7) + $z1 = $this->state[2] + ^ $this->state[5] + ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[6], $this->state[7]); + + // t0, t1 = Split(xi, 128) + $t0 = ParagonIE_Sodium_Core_Util::substr($xi, 0, 16); + $t1 = ParagonIE_Sodium_Core_Util::substr($xi, 16, 16); + + // out0 = t0 ^ z0 + // out1 = t1 ^ z1 + $out0 = $t0 ^ $z0; + $out1 = $t1 ^ $z1; + + // Update(t0, t1) + // ci = out0 || out1 + $this->update($t0, $t1); + + // return ci + return $out0 . $out1; + } + + /** + * @param int $ad_len_bits + * @param int $msg_len_bits + * @return string + */ + public function finalize($ad_len_bits, $msg_len_bits) + { + $encoded = ParagonIE_Sodium_Core_Util::store64_le($ad_len_bits) . + ParagonIE_Sodium_Core_Util::store64_le($msg_len_bits); + $t = $this->state[2] ^ $encoded; + for ($i = 0; $i < 7; ++$i) { + $this->update($t, $t); + } + return ($this->state[0] ^ $this->state[1] ^ $this->state[2] ^ $this->state[3]) . + ($this->state[4] ^ $this->state[5] ^ $this->state[6] ^ $this->state[7]); + } + + /** + * @param string $m0 + * @param string $m1 + * @return self + */ + public function update($m0, $m1) + { + /* + S'0 = AESRound(S7, S0 ^ M0) + S'1 = AESRound(S0, S1) + S'2 = AESRound(S1, S2) + S'3 = AESRound(S2, S3) + S'4 = AESRound(S3, S4 ^ M1) + S'5 = AESRound(S4, S5) + S'6 = AESRound(S5, S6) + S'7 = AESRound(S6, S7) + */ + $s_0 = ParagonIE_Sodium_Core_AES::aesRound( + $this->state[7], + $this->state[0] ^ $m0 + ); + $s_1 = ParagonIE_Sodium_Core_AES::aesRound($this->state[0], $this->state[1]); + $s_2 = ParagonIE_Sodium_Core_AES::aesRound($this->state[1], $this->state[2]); + $s_3 = ParagonIE_Sodium_Core_AES::aesRound($this->state[2], $this->state[3]); + $s_4 = ParagonIE_Sodium_Core_AES::aesRound( + $this->state[3], + $this->state[4] ^ $m1 + ); + $s_5 = ParagonIE_Sodium_Core_AES::aesRound($this->state[4], $this->state[5]); + $s_6 = ParagonIE_Sodium_Core_AES::aesRound($this->state[5], $this->state[6]); + $s_7 = ParagonIE_Sodium_Core_AES::aesRound($this->state[6], $this->state[7]); + + /* + S0 = S'0 + S1 = S'1 + S2 = S'2 + S3 = S'3 + S4 = S'4 + S5 = S'5 + S6 = S'6 + S7 = S'7 + */ + $this->state[0] = $s_0; + $this->state[1] = $s_1; + $this->state[2] = $s_2; + $this->state[3] = $s_3; + $this->state[4] = $s_4; + $this->state[5] = $s_5; + $this->state[6] = $s_6; + $this->state[7] = $s_7; + return $this; + } +} \ No newline at end of file diff --git a/src/Core/AEGIS/State256.php b/src/Core/AEGIS/State256.php new file mode 100644 index 00000000..7c11cf73 --- /dev/null +++ b/src/Core/AEGIS/State256.php @@ -0,0 +1,232 @@ + $state */ + protected $state; + public function __construct() + { + $this->state = array_fill(0, 6, ''); + } + + /** + * @internal Only use this for unit tests! + * @return string[] + */ + public function getState() + { + return array_values($this->state); + } + + /** + * @param array $input + * @return self + * @throws SodiumException + * + * @internal Only for unit tests + */ + public static function initForUnitTests(array $input) + { + if (count($input) < 6) { + throw new SodiumException('invalid input'); + } + $state = new self(); + for ($i = 0; $i < 6; ++$i) { + $state->state[$i] = $input[$i]; + } + return $state; + } + + /** + * @param string $key + * @param string $nonce + * @return self + */ + public static function init($key, $nonce) + { + $state = new self(); + $k0 = ParagonIE_Sodium_Core_Util::substr($key, 0, 16); + $k1 = ParagonIE_Sodium_Core_Util::substr($key, 16, 16); + $n0 = ParagonIE_Sodium_Core_Util::substr($nonce, 0, 16); + $n1 = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 16); + + // S0 = k0 ^ n0 + // S1 = k1 ^ n1 + // S2 = C1 + // S3 = C0 + // S4 = k0 ^ C0 + // S5 = k1 ^ C1 + $k0_n0 = $k0 ^ $n0; + $k1_n1 = $k1 ^ $n1; + $state->state[0] = $k0_n0; + $state->state[1] = $k1_n1; + $state->state[2] = SODIUM_COMPAT_AEGIS_C1; + $state->state[3] = SODIUM_COMPAT_AEGIS_C0; + $state->state[4] = $k0 ^ SODIUM_COMPAT_AEGIS_C0; + $state->state[5] = $k1 ^ SODIUM_COMPAT_AEGIS_C1; + + // Repeat(4, + // Update(k0) + // Update(k1) + // Update(k0 ^ n0) + // Update(k1 ^ n1) + // ) + for ($i = 0; $i < 4; ++$i) { + $state->update($k0); + $state->update($k1); + $state->update($k0 ^ $n0); + $state->update($k1 ^ $n1); + } + return $state; + } + + /** + * @param string $ai + * @return self + * @throws SodiumException + */ + public function absorb($ai) + { + if (ParagonIE_Sodium_Core_Util::strlen($ai) !== 16) { + throw new SodiumException('Input must be an AES block in size'); + } + return $this->update($ai); + } + + /** + * @param string $ci + * @return string + * @throws SodiumException + */ + public function dec($ci) + { + if (ParagonIE_Sodium_Core_Util::strlen($ci) !== 16) { + throw new SodiumException('Input must be an AES block in size'); + } + // z = S1 ^ S4 ^ S5 ^ (S2 & S3) + $z = $this->state[1] + ^ $this->state[4] + ^ $this->state[5] + ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); + $xi = $ci ^ $z; + $this->update($xi); + return $xi; + } + + /** + * @param string $cn + * @return string + */ + public function decPartial($cn) + { + $len = ParagonIE_Sodium_Core_Util::strlen($cn); + // z = S1 ^ S4 ^ S5 ^ (S2 & S3) + $z = $this->state[1] + ^ $this->state[4] + ^ $this->state[5] + ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); + + // t = ZeroPad(cn, 128) + $t = str_pad($cn, 16, "\0", STR_PAD_RIGHT); + + // out = t ^ z + $out = $t ^ $z; + + // xn = Truncate(out, |cn|) + $xn = ParagonIE_Sodium_Core_Util::substr($out, 0, $len); + + // v = ZeroPad(xn, 128) + $v = str_pad($xn, 16, "\0", STR_PAD_RIGHT); + // Update(v) + $this->update($v); + + // return xn + return $xn; + } + + /** + * @param string $xi + * @return string + * @throws SodiumException + */ + public function enc($xi) + { + if (ParagonIE_Sodium_Core_Util::strlen($xi) !== 16) { + throw new SodiumException('Input must be an AES block in size'); + } + // z = S1 ^ S4 ^ S5 ^ (S2 & S3) + $z = $this->state[1] + ^ $this->state[4] + ^ $this->state[5] + ^ ParagonIE_Sodium_Core_Util::andStrings($this->state[2], $this->state[3]); + $this->update($xi); + return $xi ^ $z; + } + + /** + * @param int $ad_len_bits + * @param int $msg_len_bits + * @return string + */ + public function finalize($ad_len_bits, $msg_len_bits) + { + $encoded = ParagonIE_Sodium_Core_Util::store64_le($ad_len_bits) . + ParagonIE_Sodium_Core_Util::store64_le($msg_len_bits); + $t = $this->state[3] ^ $encoded; + + for ($i = 0; $i < 7; ++$i) { + $this->update($t); + } + + return ($this->state[0] ^ $this->state[1] ^ $this->state[2]) . + ($this->state[3] ^ $this->state[4] ^ $this->state[5]); + } + + /** + * @param string $m + * @return self + */ + public function update($m) + { + /* + S'0 = AESRound(S5, S0 ^ M) + S'1 = AESRound(S0, S1) + S'2 = AESRound(S1, S2) + S'3 = AESRound(S2, S3) + S'4 = AESRound(S3, S4) + S'5 = AESRound(S4, S5) + */ + $s_0 = ParagonIE_Sodium_Core_AES::aesRound( + $this->state[5], + $this->state[0] ^ $m + ); + $s_1 = ParagonIE_Sodium_Core_AES::aesRound($this->state[0], $this->state[1]); + $s_2 = ParagonIE_Sodium_Core_AES::aesRound($this->state[1], $this->state[2]); + $s_3 = ParagonIE_Sodium_Core_AES::aesRound($this->state[2], $this->state[3]); + $s_4 = ParagonIE_Sodium_Core_AES::aesRound($this->state[3], $this->state[4]); + $s_5 = ParagonIE_Sodium_Core_AES::aesRound($this->state[4], $this->state[5]); + + /* + S0 = S'0 + S1 = S'1 + S2 = S'2 + S3 = S'3 + S4 = S'4 + S5 = S'5 + */ + $this->state[0] = $s_0; + $this->state[1] = $s_1; + $this->state[2] = $s_2; + $this->state[3] = $s_3; + $this->state[4] = $s_4; + $this->state[5] = $s_5; + return $this; + } +} diff --git a/src/Core/AEGIS128L.php b/src/Core/AEGIS128L.php new file mode 100644 index 00000000..ad1e85d3 --- /dev/null +++ b/src/Core/AEGIS128L.php @@ -0,0 +1,119 @@ +> 5; + for ($i = 0; $i < $ad_blocks; ++$i) { + $ai = self::substr($ad, $i << 5, 32); + if (self::strlen($ai) < 32) { + $ai = str_pad($ai, 32, "\0", STR_PAD_RIGHT); + } + $state->absorb($ai); + } + + $msg = ''; + $cn = self::strlen($ct) & 31; + $ct_blocks = self::strlen($ct) >> 5; + for ($i = 0; $i < $ct_blocks; ++$i) { + $msg .= $state->dec(self::substr($ct, $i << 5, 32)); + } + if ($cn) { + $start = $ct_blocks << 5; + $msg .= $state->decPartial(self::substr($ct, $start, $cn)); + } + $expected_tag = $state->finalize( + self::strlen($ad) << 3, + self::strlen($msg) << 3 + ); + if (!self::hashEquals($expected_tag, $tag)) { + try { + // The RFC says to erase msg, so we shall try: + ParagonIE_Sodium_Compat::memzero($msg); + } catch (SodiumException $ex) { + // Do nothing if we cannot memzero + } + throw new SodiumException('verification failed'); + } + return $msg; + } + + /** + * @param string $msg + * @param string $ad + * @param string $key + * @param string $nonce + * @return array + * + * @throws SodiumException + */ + public static function encrypt($msg, $ad, $key, $nonce) + { + $state = self::init($key, $nonce); + // ad_blocks = Split(ZeroPad(ad, 256), 256) + // for ai in ad_blocks: + // Absorb(ai) + $ad_len = self::strlen($ad); + $msg_len = self::strlen($msg); + $ad_blocks = ($ad_len + 31) >> 5; + for ($i = 0; $i < $ad_blocks; ++$i) { + $ai = self::substr($ad, $i << 5, 32); + if (self::strlen($ai) < 32) { + $ai = str_pad($ai, 32, "\0", STR_PAD_RIGHT); + } + $state->absorb($ai); + } + + // msg_blocks = Split(ZeroPad(msg, 256), 256) + // for xi in msg_blocks: + // ct = ct || Enc(xi) + $ct = ''; + $msg_blocks = ($msg_len + 31) >> 5; + for ($i = 0; $i < $msg_blocks; ++$i) { + $xi = self::substr($msg, $i << 5, 32); + if (self::strlen($xi) < 32) { + $xi = str_pad($xi, 32, "\0", STR_PAD_RIGHT); + } + $ct .= $state->enc($xi); + } + // tag = Finalize(|ad|, |msg|) + // ct = Truncate(ct, |msg|) + $tag = $state->finalize( + $ad_len << 3, + $msg_len << 3 + ); + // return ct and tag + return array( + self::substr($ct, 0, $msg_len), + $tag + ); + } + + /** + * @param string $key + * @param string $nonce + * @return ParagonIE_Sodium_Core_AEGIS_State128L + */ + public static function init($key, $nonce) + { + return ParagonIE_Sodium_Core_AEGIS_State128L::init($key, $nonce); + } +} diff --git a/src/Core/AEGIS256.php b/src/Core/AEGIS256.php new file mode 100644 index 00000000..605bbcaf --- /dev/null +++ b/src/Core/AEGIS256.php @@ -0,0 +1,118 @@ +> 4; + // for ai in ad_blocks: + // Absorb(ai) + for ($i = 0; $i < $ad_blocks; ++$i) { + $ai = self::substr($ad, $i << 4, 16); + if (self::strlen($ai) < 16) { + $ai = str_pad($ai, 16, "\0", STR_PAD_RIGHT); + } + $state->absorb($ai); + } + + $msg = ''; + $cn = self::strlen($ct) & 15; + $ct_blocks = self::strlen($ct) >> 4; + // ct_blocks = Split(ZeroPad(ct, 128), 128) + // cn = Tail(ct, |ct| mod 128) + for ($i = 0; $i < $ct_blocks; ++$i) { + $msg .= $state->dec(self::substr($ct, $i << 4, 16)); + } + // if cn is not empty: + // msg = msg || DecPartial(cn) + if ($cn) { + $start = $ct_blocks << 4; + $msg .= $state->decPartial(self::substr($ct, $start, $cn)); + } + $expected_tag = $state->finalize( + self::strlen($ad) << 3, + self::strlen($msg) << 3 + ); + if (!self::hashEquals($expected_tag, $tag)) { + try { + // The RFC says to erase msg, so we shall try: + ParagonIE_Sodium_Compat::memzero($msg); + } catch (SodiumException $ex) { + // Do nothing if we cannot memzero + } + throw new SodiumException('verification failed'); + } + return $msg; + } + + /** + * @param string $msg + * @param string $ad + * @param string $key + * @param string $nonce + * @return array + * @throws SodiumException + */ + public static function encrypt($msg, $ad, $key, $nonce) + { + $state = self::init($key, $nonce); + $ad_len = self::strlen($ad); + $msg_len = self::strlen($msg); + $ad_blocks = ($ad_len + 15) >> 4; + for ($i = 0; $i < $ad_blocks; ++$i) { + $ai = self::substr($ad, $i << 4, 16); + if (self::strlen($ai) < 16) { + $ai = str_pad($ai, 16, "\0", STR_PAD_RIGHT); + } + $state->absorb($ai); + } + + $ct = ''; + $msg_blocks = ($msg_len + 15) >> 4; + for ($i = 0; $i < $msg_blocks; ++$i) { + $xi = self::substr($msg, $i << 4, 16); + if (self::strlen($xi) < 16) { + $xi = str_pad($xi, 16, "\0", STR_PAD_RIGHT); + } + $ct .= $state->enc($xi); + } + $tag = $state->finalize( + $ad_len << 3, + $msg_len << 3 + ); + return array( + self::substr($ct, 0, $msg_len), + $tag + ); + + } + + /** + * @param string $key + * @param string $nonce + * @return ParagonIE_Sodium_Core_AEGIS_State256 + */ + public static function init($key, $nonce) + { + return ParagonIE_Sodium_Core_AEGIS_State256::init($key, $nonce); + } +} diff --git a/src/Core/AES.php b/src/Core/AES.php index 13c01d81..a7b75499 100644 --- a/src/Core/AES.php +++ b/src/Core/AES.php @@ -407,6 +407,40 @@ public static function bitsliceEncryptBlock( self::addRoundKey($q, $skey, ($skey->getNumRounds() << 3)); } + /** + * @param string $x + * @param string $y + * @return string + */ + public static function aesRound($x, $y) + { + $q = ParagonIE_Sodium_Core_AES_Block::init(); + $q[0] = self::load_4(self::substr($x, 0, 4)); + $q[2] = self::load_4(self::substr($x, 4, 4)); + $q[4] = self::load_4(self::substr($x, 8, 4)); + $q[6] = self::load_4(self::substr($x, 12, 4)); + + $rk = ParagonIE_Sodium_Core_AES_Block::init(); + $rk[0] = $rk[1] = self::load_4(self::substr($y, 0, 4)); + $rk[2] = $rk[3] = self::load_4(self::substr($y, 4, 4)); + $rk[4] = $rk[5] = self::load_4(self::substr($y, 8, 4)); + $rk[6] = $rk[7] = self::load_4(self::substr($y, 12, 4)); + + $q->orthogonalize(); + self::sbox($q); + $q->shiftRows(); + $q->mixColumns(); + $q->orthogonalize(); + // add round key without key schedule: + for ($i = 0; $i < 8; ++$i) { + $q[$i] ^= $rk[$i]; + } + return self::store32_le($q[0]) . + self::store32_le($q[2]) . + self::store32_le($q[4]) . + self::store32_le($q[6]); + } + /** * @param ParagonIE_Sodium_Core_AES_Expanded $skey * @param ParagonIE_Sodium_Core_AES_Block $q diff --git a/src/Core/Util.php b/src/Core/Util.php index 2903beff..e5d96dcd 100644 --- a/src/Core/Util.php +++ b/src/Core/Util.php @@ -35,6 +35,28 @@ public static function abs($integer, $size = 0) ); } + /** + * @param string $a + * @param string $b + * @return string + * @throws SodiumException + */ + public static function andStrings($a, $b) + { + /* Type checks: */ + if (!is_string($a)) { + throw new TypeError('Argument 1 must be a string'); + } + if (!is_string($b)) { + throw new TypeError('Argument 2 must be a string'); + } + $len = self::strlen($a); + if (self::strlen($b) !== $len) { + throw new SodiumException('Both strings must be of equal length to combine with bitwise AND'); + } + return $a & $b; + } + /** * Convert a binary string into a hexadecimal string without cache-timing * leaks diff --git a/tests/unit/AEGISTest.php b/tests/unit/AEGISTest.php new file mode 100644 index 00000000..2af92d03 --- /dev/null +++ b/tests/unit/AEGISTest.php @@ -0,0 +1,390 @@ +update($m0, $m1); + $s = $state->getState(); + $expected = array( + ParagonIE_Sodium_Core_Util::hex2bin('596ab773e4433ca0127c73f60536769d'), + ParagonIE_Sodium_Core_Util::hex2bin('790394041a3d26ab697bde865014652d'), + ParagonIE_Sodium_Core_Util::hex2bin('38cf49e4b65248acd533041b64dd0611'), + ParagonIE_Sodium_Core_Util::hex2bin('16d8e58748f437bfff1797f780337cee'), + ParagonIE_Sodium_Core_Util::hex2bin('69761320f7dd738b281cc9f335ac2f5a'), + ParagonIE_Sodium_Core_Util::hex2bin('a21746bb193a569e331e1aa985d0d729'), + ParagonIE_Sodium_Core_Util::hex2bin('09d714e6fcf9177a8ed1cde7e3d259a6'), + ParagonIE_Sodium_Core_Util::hex2bin('61279ba73167f0ab76f0a11bf203bdff') + ); + $this->assertSame($s, $expected); + } + + public function testAegis256lUpdate() + { + $state = ParagonIE_Sodium_Core_AEGIS_State256::initForUnitTests(array( + ParagonIE_Sodium_Core_Util::hex2bin('1fa1207ed76c86f2c4bb40e8b395b43e'), + ParagonIE_Sodium_Core_Util::hex2bin('b44c375e6c1e1978db64bcd12e9e332f'), + ParagonIE_Sodium_Core_Util::hex2bin('0dab84bfa9f0226432ff630f233d4e5b'), + ParagonIE_Sodium_Core_Util::hex2bin('d7ef65c9b93e8ee60c75161407b066e7'), + ParagonIE_Sodium_Core_Util::hex2bin('a760bb3da073fbd92bdc24734b1f56fb'), + ParagonIE_Sodium_Core_Util::hex2bin('a828a18d6a964497ac6e7e53c5f55c73') + )); + $m = ParagonIE_Sodium_Core_Util::hex2bin('b165617ed04ab738afb2612c6d18a1ec'); + $state->update($m); + $s = $state->getState(); + $expected = array( + ParagonIE_Sodium_Core_Util::hex2bin('e6bc643bae82dfa3d991b1b323839dcd'), + ParagonIE_Sodium_Core_Util::hex2bin('648578232ba0f2f0a3677f617dc052c3'), + ParagonIE_Sodium_Core_Util::hex2bin('ea788e0e572044a46059212dd007a789'), + ParagonIE_Sodium_Core_Util::hex2bin('2f1498ae19b80da13fba698f088a8590'), + ParagonIE_Sodium_Core_Util::hex2bin('a54c2ee95e8c2a2c3dae2ec743ae6b86'), + ParagonIE_Sodium_Core_Util::hex2bin('a3240fceb68e32d5d114df1b5363ab67') + ); + $this->assertSame($s, $expected); + } + + /** + * @return array[] + * + * name, key, nonce, tag, ciphertext, plaintext, aad, expect_fail? + */ + public function aegis128lVectors() + { + return array( + array( + 'AEGIS-128L test vector 1', + '10010000000000000000000000000000', + '10000200000000000000000000000000', + '25835bfbb21632176cf03840687cb968cace4617af1bd0f7d064c639a5c79ee4', + 'c1c0e58bd913006feba00f4b3cc3594e', + '00000000000000000000000000000000', + '', + false + ), + array( + 'AEGIS-128L test vector 2', + '10010000000000000000000000000000', + '10000200000000000000000000000000', + '1360dc9db8ae42455f6e5b6a9d488ea4f2184c4e12120249335c4ee84bafe25d', + '', + '', + '', + false + ), + array( + 'AEGIS-128L test vector 3', + '10010000000000000000000000000000', + '10000200000000000000000000000000', + '022cb796fe7e0ae1197525ff67e309484cfbab6528ddef89f17d74ef8ecd82b3', + '79d94593d8c2119d7e8fd9b8fc77845c5c077a05b2528b6ac54b563aed8efe84', + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', + '0001020304050607', + false + ), + array( + 'AEGIS-128L test vector 4', + '10010000000000000000000000000000', + '10000200000000000000000000000000', + '86f1b80bfb463aba711d15405d094baf4a55a15dbfec81a76f35ed0b9c8b04ac', + '79d94593d8c2119d7e8fd9b8fc77', + '000102030405060708090a0b0c0d', + '0001020304050607', + false + ), + array( + 'AEGIS-128L test vector 5', + '10010000000000000000000000000000', + '10000200000000000000000000000000', + 'b91e2947a33da8bee89b6794e647baf0fc835ff574aca3fc27c33be0db2aff98', + 'b31052ad1cca4e291abcf2df3502e6bdb1bfd6db36798be3607b1f94d34478aa7ede7f7a990fec10', + '101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637', + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223242526272829', + false + ), + array( + 'AEGIS-128L test vector 6', + '10000200000000000000000000000000', + '10010000000000000000000000000000', + '86f1b80bfb463aba711d15405d094baf4a55a15dbfec81a76f35ed0b9c8b04ac', + '79d94593d8c2119d7e8fd9b8fc77', + '', + '0001020304050607', + true + ), + array( + 'AEGIS-128L test vector 7', + '10010000000000000000000000000000', + '10000200000000000000000000000000', + '86f1b80bfb463aba711d15405d094baf4a55a15dbfec81a76f35ed0b9c8b04ac', + '79d94593d8c2119d7e8fd9b8fc78', + '', + '0001020304050607', + true + ), + array( + 'AEGIS-128L test vector 8', + '10010000000000000000000000000000', + '10000200000000000000000000000000', + '86f1b80bfb463aba711d15405d094baf4a55a15dbfec81a76f35ed0b9c8b04ac', + '79d94593d8c2119d7e8fd9b8fc77', + '', + '0001020304050608', + true + ), + array( + 'AEGIS-128L test vector 9', + '10010000000000000000000000000000', + '10000200000000000000000000000000', + '86f1b80bfb463aba711d15405d094baf4a55a15dbfec81a76f35ed0b9c8b04ad', + '79d94593d8c2119d7e8fd9b8fc77', + '', + '0001020304050607', + true + ), + ); + } + + /** + * @return array[] + * + * name, key, nonce, tag, ciphertext, plaintext, aad, expect_fail? + */ + public function aegis256Vectors() + { + return array( + array( + 'AEGIS-256 test vector 1', + '1001000000000000000000000000000000000000000000000000000000000000', + '1000020000000000000000000000000000000000000000000000000000000000', + '1181a1d18091082bf0266f66297d167d2e68b845f61a3b0527d31fc7b7b89f13', + '754fc3d8c973246dcc6d741412a4b236', + '00000000000000000000000000000000', + '', + false + ), + array( + 'AEGIS-256 test vector 2', + '1001000000000000000000000000000000000000000000000000000000000000', + '1000020000000000000000000000000000000000000000000000000000000000', + '6a348c930adbd654896e1666aad67de989ea75ebaa2b82fb588977b1ffec864a', + '', + '', + '', + false + ), + array( + 'AEGIS-256 test vector 3', + '1001000000000000000000000000000000000000000000000000000000000000', + '1000020000000000000000000000000000000000000000000000000000000000', + 'b7d28d0c3c0ebd409fd22b44160503073a547412da0854bfb9723020dab8da1a', + 'f373079ed84b2709faee373584585d60accd191db310ef5d8b11833df9dec711', + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', + '0001020304050607', + false + ), + array( + 'AEGIS-256 test vector 4', + '1001000000000000000000000000000000000000000000000000000000000000', + '1000020000000000000000000000000000000000000000000000000000000000', + '8c1cc703c81281bee3f6d9966e14948b4a175b2efbdc31e61a98b4465235c2d9', + 'f373079ed84b2709faee37358458', + '000102030405060708090a0b0c0d', + '0001020304050607', + false + ), + array( + 'AEGIS-256 test vector 5', + '1001000000000000000000000000000000000000000000000000000000000000', + '1000020000000000000000000000000000000000000000000000000000000000', + 'a3aca270c006094d71c20e6910b5161c0826df233d08919a566ec2c05990f734', + '57754a7d09963e7c787583a2e7b859bb24fa1e04d49fd550b2511a358e3bca252a9b1b8b30cc4a67', + '101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637', + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223242526272829', + false + ), + array( + 'AEGIS-256 test vector 6', + '1000020000000000000000000000000000000000000000000000000000000000', + '1001000000000000000000000000000000000000000000000000000000000000', + '8c1cc703c81281bee3f6d9966e14948b4a175b2efbdc31e61a98b4465235c2d9', + 'f373079ed84b2709faee37358458', + '', + '0001020304050607', + true + ), + array( + 'AEGIS-256 test vector 7', + '1001000000000000000000000000000000000000000000000000000000000000', + '1000020000000000000000000000000000000000000000000000000000000000', + '8c1cc703c81281bee3f6d9966e14948b4a175b2efbdc31e61a98b4465235c2d9', + 'f373079ed84b2709faee37358459', + '', + '0001020304050607', + true + ), + array( + 'AEGIS-256 test vector 8', + '1001000000000000000000000000000000000000000000000000000000000000', + '1000020000000000000000000000000000000000000000000000000000000000', + '8c1cc703c81281bee3f6d9966e14948b4a175b2efbdc31e61a98b4465235c2d9', + 'f373079ed84b2709faee37358458', + '', + '0001020304050608', + true + ), + array( + 'AEGIS-256 test vector 9', + '1001000000000000000000000000000000000000000000000000000000000000', + '1000020000000000000000000000000000000000000000000000000000000000', + '8c1cc703c81281bee3f6d9966e14948b4a175b2efbdc31e61a98b4465235c2da', + 'f373079ed84b2709faee37358458', + '', + '0001020304050607', + true + ) + ); + } + + /** + * @dataProvider aegis128lVectors + * @param string $key_hex + * @param string $nonce_hex + * @param string $expected_tag_hex + * @param string $expected_ct_hex + * @param string $msg_hex + * @param string $ad_hex + * @param bool $expect_fail + * @return void + * @throws SodiumException + */ + public function testAegis128lVectors( + $name, + $key_hex, + $nonce_hex, + $expected_tag_hex, + $expected_ct_hex, + $msg_hex = '', + $ad_hex = '', + $expect_fail = false + ) { + $key = ParagonIE_Sodium_Core_Util::hex2bin($key_hex); + $nonce = ParagonIE_Sodium_Core_Util::hex2bin($nonce_hex); + $expTag = ParagonIE_Sodium_Core_Util::hex2bin($expected_tag_hex); + $expCt = ParagonIE_Sodium_Core_Util::hex2bin($expected_ct_hex); + $ad = ParagonIE_Sodium_Core_Util::hex2bin($ad_hex); + if ($expect_fail) { + $failed = false; + try { + ParagonIE_Sodium_Core_AEGIS128L::decrypt($expCt, $expTag, $ad, $key, $nonce); + } catch (SodiumException $ex) { + $failed = true; + } + $this->assertTrue($failed, 'Expected decryption to fail but it did not'); + return; + } + $msg = ParagonIE_Sodium_Core_Util::hex2bin($msg_hex); + list($ct, $tag) = ParagonIE_Sodium_Core_AEGIS128L::encrypt($msg, $ad, $key, $nonce); + + $this->assertSame( + ParagonIE_Sodium_Core_Util::bin2hex($expCt), + ParagonIE_Sodium_Core_Util::bin2hex($ct), + $name + ); + $this->assertSame( + ParagonIE_Sodium_Core_Util::bin2hex($expTag), + ParagonIE_Sodium_Core_Util::bin2hex($tag), + $name + ); + $this->assertSame($expCt, $ct, $name); + $this->assertSame($expTag, $tag, $name); + $got_pt = ParagonIE_Sodium_Core_AEGIS128L::decrypt($expCt, $expTag, $ad, $key, $nonce); + $this->assertSame( + ParagonIE_Sodium_Core_Util::bin2hex($got_pt), + $msg_hex, + $name + ); + $this->assertSame($got_pt, $msg, $name); + } + + /** + * @dataProvider aegis256Vectors + * @param string $key_hex + * @param string $nonce_hex + * @param string $expected_tag_hex + * @param string $expected_ct_hex + * @param string $msg_hex + * @param string $ad_hex + * @param bool $expect_fail + * @return void + * @throws SodiumException + */ + public function testAegis256Vectors( + $name, + $key_hex, + $nonce_hex, + $expected_tag_hex, + $expected_ct_hex, + $msg_hex = '', + $ad_hex = '', + $expect_fail = false + ) { + $key = ParagonIE_Sodium_Core_Util::hex2bin($key_hex); + $nonce = ParagonIE_Sodium_Core_Util::hex2bin($nonce_hex); + $expTag = ParagonIE_Sodium_Core_Util::hex2bin($expected_tag_hex); + $expCt = ParagonIE_Sodium_Core_Util::hex2bin($expected_ct_hex); + $ad = ParagonIE_Sodium_Core_Util::hex2bin($ad_hex); + if ($expect_fail) { + $failed = false; + try { + ParagonIE_Sodium_Core_AEGIS256::decrypt($expCt, $expTag, $ad, $key, $nonce); + } catch (SodiumException $ex) { + $failed = true; + } + $this->assertTrue($failed, 'Expected decryption to fail but it did not'); + return; + } + $msg = ParagonIE_Sodium_Core_Util::hex2bin($msg_hex); + list($ct, $tag) = ParagonIE_Sodium_Core_AEGIS256::encrypt($msg, $ad, $key, $nonce); + + $this->assertSame( + ParagonIE_Sodium_Core_Util::bin2hex($expCt), + ParagonIE_Sodium_Core_Util::bin2hex($ct), + $name + ); + $this->assertSame( + ParagonIE_Sodium_Core_Util::bin2hex($expTag), + ParagonIE_Sodium_Core_Util::bin2hex($tag), + $name + ); + $this->assertSame($expCt, $ct, $name); + $this->assertSame($expTag, $tag, $name); + $got_pt = ParagonIE_Sodium_Core_AEGIS256::decrypt($expCt, $expTag, $ad, $key, $nonce); + $this->assertSame( + ParagonIE_Sodium_Core_Util::bin2hex($got_pt), + $msg_hex, + $name + ); + $this->assertSame($got_pt, $msg, $name); + } + +} \ No newline at end of file diff --git a/tests/unit/AESTest.php b/tests/unit/AESTest.php index b37c78d9..37fd027b 100644 --- a/tests/unit/AESTest.php +++ b/tests/unit/AESTest.php @@ -319,6 +319,18 @@ public function testSkeyExpand() } } + public function testAesRound() + { + $in = ParagonIE_Sodium_Core_Util::hex2bin('000102030405060708090a0b0c0d0e0f'); + $rk = ParagonIE_Sodium_Core_Util::hex2bin('101112131415161718191a1b1c1d1e1f'); + $this->assertSame( + '7a7b4e5638782546a8c0477a3b813f43', + ParagonIE_Sodium_Core_Util::bin2hex( + ParagonIE_Sodium_Core_AES::aesRound($in, $rk) + ) + ); + } + /** * @dataProvider aes128ecbProvider * @covers ParagonIE_Sodium_Core_AES::encryptBlockECB diff --git a/tests/unit/UtilTest.php b/tests/unit/UtilTest.php index 845e3010..2155833e 100644 --- a/tests/unit/UtilTest.php +++ b/tests/unit/UtilTest.php @@ -10,6 +10,23 @@ public function before() ParagonIE_Sodium_Compat::$disableFallbackForUnitTests = true; } + public function testAndString() + { + $x = "\x01\x02\x03\x04"; + $y = "\xff\xff\xff\xff"; + $z = "\xcc\x8a\xcc\x00"; + + $this->assertSame($x, ParagonIE_Sodium_Core_Util::andStrings($x, $y)); + $this->assertSame( + ParagonIE_Sodium_Core_Util::bin2hex($z), + ParagonIE_Sodium_Core_Util::bin2hex(ParagonIE_Sodium_Core_Util::andStrings($y, $z)) + ); + $this->assertSame( + '00020000', + ParagonIE_Sodium_Core_Util::bin2hex(ParagonIE_Sodium_Core_Util::andStrings($x, $z)) + ); + } + public function testAbs() { $this->assertEquals(0, ParagonIE_Sodium_Core_Util::abs(0)); From a7b337de892329fa3de20bb5f5ce8939aefd0647 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 17 Apr 2024 03:44:40 -0400 Subject: [PATCH 3/6] Expose AEGIS through public API and polyfill --- lib/php72compat.php | 98 ++++++++++++++++- src/Compat.php | 220 +++++++++++++++++++++++++++++++++++++++ tests/unit/AEGISTest.php | 23 +++- 3 files changed, 336 insertions(+), 5 deletions(-) diff --git a/lib/php72compat.php b/lib/php72compat.php index e949dbdc..ee81d4e7 100644 --- a/lib/php72compat.php +++ b/lib/php72compat.php @@ -14,14 +14,22 @@ 'BASE64_VARIANT_ORIGINAL_NO_PADDING', 'BASE64_VARIANT_URLSAFE', 'BASE64_VARIANT_URLSAFE_NO_PADDING', - 'CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES', - 'CRYPTO_AEAD_CHACHA20POLY1305_NSECBYTES', - 'CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES', - 'CRYPTO_AEAD_CHACHA20POLY1305_ABYTES', + 'CRYPTO_AEAD_AESGIS128L_KEYBYTES', + 'CRYPTO_AEAD_AESGIS128L_NSECBYTES', + 'CRYPTO_AEAD_AESGIS128L_NPUBBYTES', + 'CRYPTO_AEAD_AESGIS128L_ABYTES', + 'CRYPTO_AEAD_AESGIS256_KEYBYTES', + 'CRYPTO_AEAD_AESGIS256_NSECBYTES', + 'CRYPTO_AEAD_AESGIS256_NPUBBYTES', + 'CRYPTO_AEAD_AESGIS256_ABYTES', 'CRYPTO_AEAD_AES256GCM_KEYBYTES', 'CRYPTO_AEAD_AES256GCM_NSECBYTES', 'CRYPTO_AEAD_AES256GCM_NPUBBYTES', 'CRYPTO_AEAD_AES256GCM_ABYTES', + 'CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES', + 'CRYPTO_AEAD_CHACHA20POLY1305_NSECBYTES', + 'CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES', + 'CRYPTO_AEAD_CHACHA20POLY1305_ABYTES', 'CRYPTO_AEAD_CHACHA20POLY1305_IETF_KEYBYTES', 'CRYPTO_AEAD_CHACHA20POLY1305_IETF_NSECBYTES', 'CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES', @@ -176,6 +184,88 @@ function sodium_compare($string1, $string2) return ParagonIE_Sodium_Compat::compare($string1, $string2); } } +if (!is_callable('sodium_crypto_aead_aegis128l_decrypt')) { + /** + * @see ParagonIE_Sodium_Compat::crypto_aead_aegis128l_decrypt() + * @param string $ciphertext + * @param string $additional_data + * @param string $nonce + * @param string $key + * @return string + * @throws SodiumException + */ + function sodium_crypto_aead_aegis128l_decrypt($ciphertext, $additional_data, $nonce, $key) + { + return ParagonIE_Sodium_Compat::crypto_aead_aegis128l_decrypt( + $ciphertext, + $additional_data, + $nonce, + $key + ); + } +} +if (!is_callable('sodium_crypto_aead_aegis128l_encrypt')) { + /** + * @see ParagonIE_Sodium_Compat::crypto_aead_aegis128l_encrypt() + * @param string $message + * @param string $additional_data + * @param string $nonce + * @param string $key + * @return string + * @throws SodiumException + * @throws TypeError + */ + function sodium_crypto_aead_aegis128l_encrypt($message, $additional_data, $nonce, $key) + { + return ParagonIE_Sodium_Compat::crypto_aead_aegis128l_encrypt( + $message, + $additional_data, + $nonce, + $key + ); + } +} +if (!is_callable('sodium_crypto_aead_aegis256_decrypt')) { + /** + * @see ParagonIE_Sodium_Compat::crypto_aead_aegis256_encrypt() + * @param string $ciphertext + * @param string $additional_data + * @param string $nonce + * @param string $key + * @return string + * @throws SodiumException + */ + function sodium_crypto_aead_aegis256_decrypt($ciphertext, $additional_data, $nonce, $key) + { + return ParagonIE_Sodium_Compat::crypto_aead_aegis256_decrypt( + $ciphertext, + $additional_data, + $nonce, + $key + ); + } +} +if (!is_callable('sodium_crypto_aead_aegis256_encrypt')) { + /** + * @see ParagonIE_Sodium_Compat::crypto_aead_aegis256_encrypt() + * @param string $message + * @param string $additional_data + * @param string $nonce + * @param string $key + * @return string + * @throws SodiumException + * @throws TypeError + */ + function sodium_crypto_aead_aegis256_encrypt($message, $additional_data, $nonce, $key) + { + return ParagonIE_Sodium_Compat::crypto_aead_aegis256_encrypt( + $message, + $additional_data, + $nonce, + $key + ); + } +} if (!is_callable('sodium_crypto_aead_aes256gcm_decrypt')) { /** * @see ParagonIE_Sodium_Compat::crypto_aead_aes256gcm_decrypt() diff --git a/src/Compat.php b/src/Compat.php index 3afe97c0..a675aa57 100644 --- a/src/Compat.php +++ b/src/Compat.php @@ -59,6 +59,14 @@ class ParagonIE_Sodium_Compat const CRYPTO_AEAD_AES256GCM_NSECBYTES = 0; const CRYPTO_AEAD_AES256GCM_NPUBBYTES = 12; const CRYPTO_AEAD_AES256GCM_ABYTES = 16; + const CRYPTO_AEAD_AEGIS128L_KEYBYTES = 16; + const CRYPTO_AEAD_AEGIS128L_NSECBYTES = 0; + const CRYPTO_AEAD_AEGIS128L_NPUBBYTES = 16; + const CRYPTO_AEAD_AEGIS128L_ABYTES = 32; + const CRYPTO_AEAD_AEGIS256_KEYBYTES = 32; + const CRYPTO_AEAD_AEGIS256_NSECBYTES = 0; + const CRYPTO_AEAD_AEGIS256_NPUBBYTES = 32; + const CRYPTO_AEAD_AEGIS256_ABYTES = 32; const CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES = 32; const CRYPTO_AEAD_CHACHA20POLY1305_NSECBYTES = 0; const CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES = 8; @@ -299,6 +307,218 @@ public static function compare($left, $right) return ParagonIE_Sodium_Core_Util::compare($left, $right); } + /** + * Authenticated Encryption with Associated Data: Decryption + * + * Algorithm: + * AEGIS-128L + * + * @param string $ciphertext Encrypted message (with MAC appended) + * @param string $assocData Authenticated Associated Data (unencrypted) + * @param string $nonce Number to be used only Once; must be 32 bytes + * @param string $key Encryption key + * + * @return string The original plaintext message + * @throws SodiumException + * @throws TypeError + * @psalm-suppress MixedArgument + * @psalm-suppress MixedInferredReturnType + * @psalm-suppress MixedReturnStatement + */ + public static function crypto_aead_aegis128l_decrypt( + $ciphertext = '', + $assocData = '', + $nonce = '', + $key = '' + ) { + ParagonIE_Sodium_Core_Util::declareScalarType($ciphertext, 'string', 1); + ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2); + ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3); + ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4); + + /* Input validation: */ + if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_AEGIS128L_NPUBBYTES) { + throw new SodiumException('Nonce must be CRYPTO_AEAD_AEGIS_128L_NPUBBYTES long'); + } + if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_AEGIS128L_KEYBYTES) { + throw new SodiumException('Key must be CRYPTO_AEAD_AEGIS128L_KEYBYTES long'); + } + $ct_length = ParagonIE_Sodium_Core_Util::strlen($ciphertext); + if ($ct_length < self::CRYPTO_AEAD_AEGIS128L_ABYTES) { + throw new SodiumException('Message must be at least CRYPTO_AEAD_AEGIS128L_ABYTES long'); + } + + $ct = ParagonIE_Sodium_Core_Util::substr( + $ciphertext, + 0, + $ct_length - self::CRYPTO_AEAD_AEGIS128L_ABYTES + ); + $tag = ParagonIE_Sodium_Core_Util::substr( + $ciphertext, + $ct_length - self::CRYPTO_AEAD_AEGIS128L_ABYTES, + self::CRYPTO_AEAD_AEGIS128L_ABYTES + ); + return ParagonIE_Sodium_Core_AEGIS128L::decrypt($ct, $tag, $assocData, $key, $nonce); + } + + /** + * Authenticated Encryption with Associated Data: Encryption + * + * Algorithm: + * AEGIS-128L + * + * @param string $plaintext Message to be encrypted + * @param string $assocData Authenticated Associated Data (unencrypted) + * @param string $nonce Number to be used only Once; must be 32 bytes + * @param string $key Encryption key + * + * @return string Ciphertext with 32-byte authentication tag appended + * @throws SodiumException + * @throws TypeError + * @psalm-suppress MixedArgument + */ + public static function crypto_aead_aegis128l_encrypt( + $plaintext = '', + $assocData = '', + $nonce = '', + $key = '' + ) { + ParagonIE_Sodium_Core_Util::declareScalarType($plaintext, 'string', 1); + ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2); + ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3); + ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4); + + /* Input validation: */ + if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_AEGIS128L_NPUBBYTES) { + throw new SodiumException('Nonce must be CRYPTO_AEAD_AEGIS128L_KEYBYTES long'); + } + if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_AEGIS128L_KEYBYTES) { + throw new SodiumException('Key must be CRYPTO_AEAD_AEGIS128L_KEYBYTES long'); + } + + list($ct, $tag) = ParagonIE_Sodium_Core_AEGIS128L::encrypt($plaintext, $assocData, $key, $nonce); + return $ct . $tag; + } + + /** + * Return a secure random key for use with the AEGIS-128L + * symmetric AEAD interface. + * + * @return string + * @throws Exception + * @throws Error + */ + public static function crypto_aead_aegis128l_keygen() + { + return random_bytes(self::CRYPTO_AEAD_AEGIS128L_KEYBYTES); + } + + /** + * Authenticated Encryption with Associated Data: Decryption + * + * Algorithm: + * AEGIS-256 + * + * @param string $ciphertext Encrypted message (with MAC appended) + * @param string $assocData Authenticated Associated Data (unencrypted) + * @param string $nonce Number to be used only Once; must be 32 bytes + * @param string $key Encryption key + * + * @return string The original plaintext message + * @throws SodiumException + * @throws TypeError + * @psalm-suppress MixedArgument + * @psalm-suppress MixedInferredReturnType + * @psalm-suppress MixedReturnStatement + */ + public static function crypto_aead_aegis256_decrypt( + $ciphertext = '', + $assocData = '', + $nonce = '', + $key = '' + ) { + ParagonIE_Sodium_Core_Util::declareScalarType($ciphertext, 'string', 1); + ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2); + ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3); + ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4); + + /* Input validation: */ + if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_AEGIS256_NPUBBYTES) { + throw new SodiumException('Nonce must be CRYPTO_AEAD_AEGIS256_NPUBBYTES long'); + } + if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_AEGIS256_KEYBYTES) { + throw new SodiumException('Key must be CRYPTO_AEAD_AEGIS256_KEYBYTES long'); + } + $ct_length = ParagonIE_Sodium_Core_Util::strlen($ciphertext); + if ($ct_length < self::CRYPTO_AEAD_AEGIS256_ABYTES) { + throw new SodiumException('Message must be at least CRYPTO_AEAD_AEGIS256_ABYTES long'); + } + + $ct = ParagonIE_Sodium_Core_Util::substr( + $ciphertext, + 0, + $ct_length - self::CRYPTO_AEAD_AEGIS256_ABYTES + ); + $tag = ParagonIE_Sodium_Core_Util::substr( + $ciphertext, + $ct_length - self::CRYPTO_AEAD_AEGIS256_ABYTES, + self::CRYPTO_AEAD_AEGIS256_ABYTES + ); + return ParagonIE_Sodium_Core_AEGIS256::decrypt($ct, $tag, $assocData, $key, $nonce); + } + + /** + * Authenticated Encryption with Associated Data: Encryption + * + * Algorithm: + * AEGIS-256 + * + * @param string $plaintext Message to be encrypted + * @param string $assocData Authenticated Associated Data (unencrypted) + * @param string $nonce Number to be used only Once; must be 32 bytes + * @param string $key Encryption key + * + * @return string Ciphertext with 32-byte authentication tag appended + * @throws SodiumException + * @throws TypeError + * @psalm-suppress MixedArgument + */ + public static function crypto_aead_aegis256_encrypt( + $plaintext = '', + $assocData = '', + $nonce = '', + $key = '' + ) { + ParagonIE_Sodium_Core_Util::declareScalarType($plaintext, 'string', 1); + ParagonIE_Sodium_Core_Util::declareScalarType($assocData, 'string', 2); + ParagonIE_Sodium_Core_Util::declareScalarType($nonce, 'string', 3); + ParagonIE_Sodium_Core_Util::declareScalarType($key, 'string', 4); + + /* Input validation: */ + if (ParagonIE_Sodium_Core_Util::strlen($nonce) !== self::CRYPTO_AEAD_AEGIS256_NPUBBYTES) { + throw new SodiumException('Nonce must be CRYPTO_AEAD_AEGIS128L_KEYBYTES long'); + } + if (ParagonIE_Sodium_Core_Util::strlen($key) !== self::CRYPTO_AEAD_AEGIS256_KEYBYTES) { + throw new SodiumException('Key must be CRYPTO_AEAD_AEGIS128L_KEYBYTES long'); + } + + list($ct, $tag) = ParagonIE_Sodium_Core_AEGIS256::encrypt($plaintext, $assocData, $key, $nonce); + return $ct . $tag; + } + + /** + * Return a secure random key for use with the AEGIS-256 + * symmetric AEAD interface. + * + * @return string + * @throws Exception + * @throws Error + */ + public static function crypto_aead_aegis256_keygen() + { + return random_bytes(self::CRYPTO_AEAD_AEGIS256_KEYBYTES); + } + /** * Is AES-256-GCM even available to use? * diff --git a/tests/unit/AEGISTest.php b/tests/unit/AEGISTest.php index 2af92d03..c403ca3a 100644 --- a/tests/unit/AEGISTest.php +++ b/tests/unit/AEGISTest.php @@ -387,4 +387,25 @@ public function testAegis256Vectors( $this->assertSame($got_pt, $msg, $name); } -} \ No newline at end of file + public function testPublicAegis128l() + { + $msg = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::randombytes_uniform(999) + 1); + $nonce = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::CRYPTO_AEAD_AEGIS128L_NPUBBYTES); + $ad = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::randombytes_uniform(999) + 1); + $key = ParagonIE_Sodium_Compat::crypto_aead_aegis128l_keygen(); + $ciphertext = ParagonIE_Sodium_Compat::crypto_aead_aegis128l_encrypt($msg, $ad, $nonce, $key); + $msg2 = ParagonIE_Sodium_Compat::crypto_aead_aegis128l_decrypt($ciphertext, $ad, $nonce, $key); + $this->assertSame($msg, $msg2); + } + + public function testPublicAegis256() + { + $msg = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::randombytes_uniform(999) + 1); + $nonce = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::CRYPTO_AEAD_AEGIS256_NPUBBYTES); + $ad = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::randombytes_uniform(999) + 1); + $key = ParagonIE_Sodium_Compat::crypto_aead_aegis256_keygen(); + $ciphertext = ParagonIE_Sodium_Compat::crypto_aead_aegis256_encrypt($msg, $ad, $nonce, $key); + $msg2 = ParagonIE_Sodium_Compat::crypto_aead_aegis256_decrypt($ciphertext, $ad, $nonce, $key); + $this->assertSame($msg, $msg2); + } +} From f6c4161da2aa56015d39ea5705fb3c3b80fbb363 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 17 Apr 2024 03:50:14 -0400 Subject: [PATCH 4/6] Add compatibility tests for PHP 8.4+ --- .github/workflows/ci.yml | 2 +- tests/compat/PHP84Test.php | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/compat/PHP84Test.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca55fbb9..2b7e6858 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout diff --git a/tests/compat/PHP84Test.php b/tests/compat/PHP84Test.php new file mode 100644 index 00000000..c990e096 --- /dev/null +++ b/tests/compat/PHP84Test.php @@ -0,0 +1,45 @@ +markTestSkipped('PHP < 8.4.0; skipping PHP 8.4 compatibility test suite.'); + } + ParagonIE_Sodium_Compat::$disableFallbackForUnitTests = true; + } + + public function testAegis128l() + { + $msg = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::randombytes_uniform(999) + 1); + $nonce = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::CRYPTO_AEAD_AEGIS128L_NPUBBYTES); + $ad = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::randombytes_uniform(999) + 1); + $key = ParagonIE_Sodium_Compat::crypto_aead_aegis128l_keygen(); + $ciphertext = ParagonIE_Sodium_Compat::crypto_aead_aegis128l_encrypt($msg, $ad, $nonce, $key); + $c2 = sodium_crypto_aead_aegis128l_encrypt($msg, $ad, $nonce, $key); + $this->assertSame($ciphertext, $c2); + $msg2 = ParagonIE_Sodium_Compat::crypto_aead_aegis128l_decrypt($ciphertext, $ad, $nonce, $key); + $msg2b = sodium_crypto_aead_aegis128l_decrypt($c2, $ad, $nonce, $key); + $this->assertSame($msg, $msg2); + $this->assertSame($msg, $msg2b); + } + + public function testAegis256() + { + $msg = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::randombytes_uniform(999) + 1); + $nonce = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::CRYPTO_AEAD_AEGIS256_NPUBBYTES); + $ad = ParagonIE_Sodium_Compat::randombytes_buf(ParagonIE_Sodium_Compat::randombytes_uniform(999) + 1); + $key = ParagonIE_Sodium_Compat::crypto_aead_aegis256_keygen(); + $ciphertext = ParagonIE_Sodium_Compat::crypto_aead_aegis256_encrypt($msg, $ad, $nonce, $key); + $c2 = sodium_crypto_aead_aegis256_encrypt($msg, $ad, $nonce, $key); + $this->assertSame($ciphertext, $c2); + $msg2 = ParagonIE_Sodium_Compat::crypto_aead_aegis256_decrypt($ciphertext, $ad, $nonce, $key); + $msg2b = sodium_crypto_aead_aegis256_decrypt($c2, $ad, $nonce, $key); + $this->assertSame($msg, $msg2); + $this->assertSame($msg, $msg2b); + } +} \ No newline at end of file From bb272126bfb5e6dee19b8847c12efedb26b57bd2 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 17 Apr 2024 03:52:59 -0400 Subject: [PATCH 5/6] Don't error on PHP 8.4 before it's even out --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b7e6858..765f1923 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,8 @@ jobs: operating-system: ['ubuntu-latest'] php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + continue-on-error: ${{ matrix.php-versions == '8.4' }} + steps: - name: Checkout uses: actions/checkout@v3 From 38709764b5447f2e92412f1c6cc0550753714a4c Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 17 Apr 2024 03:55:38 -0400 Subject: [PATCH 6/6] Temporarily disable PHP 8.4 testing I don't feel like troubleshooting GHA tonight --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 765f1923..5798531e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,9 +38,8 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] - - continue-on-error: ${{ matrix.php-versions == '8.4' }} + php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + # php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout