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)'); + } + } +}