Hash: add support for UMACs

This commit is contained in:
terrafrost 2019-09-06 07:36:45 -05:00
parent eddbdcc66c
commit 8608463d61
2 changed files with 515 additions and 1 deletions

View File

@ -35,7 +35,10 @@ namespace phpseclib\Crypt;
use phpseclib\Math\BigInteger; use phpseclib\Math\BigInteger;
use phpseclib\Exception\UnsupportedAlgorithmException; use phpseclib\Exception\UnsupportedAlgorithmException;
use phpseclib\Exception\InsufficientSetupException;
use phpseclib\Common\Functions\Strings; use phpseclib\Common\Functions\Strings;
use phpseclib\Crypt\AES;
use phpseclib\Math\PrimeField;
/** /**
* @package Hash * @package Hash
@ -101,6 +104,15 @@ class Hash
*/ */
private $key = false; private $key = false;
/**
* Nonce
*
* @see self::setNonce()
* @var string
* @access private
*/
private $nonce = false;
/** /**
* Hash Parameters * Hash Parameters
* *
@ -140,6 +152,51 @@ class Hash
*/ */
private $ipad; private $ipad;
/**
* Recompute AES Key
*
* Used only for umac
*
* @see self::hash()
* @var boolean
* @access private
*/
private $recomputeAESKey;
/**
* umac cipher object
*
* @see self::hash()
* @var \phpseclib\Crypt\AES
* @access private
*/
private $c;
/**
* umac pad
*
* @see self::hash()
* @var string
* @access private
*/
private $pad;
/**#@+
* UMAC variables
*
* @var PrimeField
*/
private static $factory36;
private static $factory64;
private static $factory128;
private static $offset64;
private static $offset128;
private static $marker64;
private static $marker128;
private static $maxwordrange64;
private static $maxwordrange128;
/**#@-*/
/** /**
* Default Constructor. * Default Constructor.
* *
@ -163,6 +220,28 @@ class Hash
{ {
$this->key = $key; $this->key = $key;
$this->computeKey(); $this->computeKey();
$this->recomputeAESKey = true;
}
/**
* Sets the nonce for UMACs
*
* Keys can be of any length.
*
* @access public
* @param string $nonce
*/
public function setNonce($nonce = false)
{
switch (true) {
case !is_string($nonce):
case strlen($nonce) > 0 && strlen($nonce) <= 16:
$this->recomputeAESKey = true;
$this->nonce = $nonce;
return;
}
throw new \LengthException('The nonce length must be between 1 and 16 bytes, inclusive');
} }
/** /**
@ -217,6 +296,14 @@ class Hash
{ {
$this->hashParam = $hash = strtolower($hash); $this->hashParam = $hash = strtolower($hash);
switch ($hash) { switch ($hash) {
case 'umac-32':
case 'umac-64':
case 'umac-96':
case 'umac-128':
$this->blockSize = 128;
$this->length = abs(substr($hash, -3)) >> 3;
$this->hash = 'umac';
return;
case 'md2-96': case 'md2-96':
case 'md5-96': case 'md5-96':
case 'sha1-96': case 'sha1-96':
@ -358,7 +445,337 @@ class Hash
} }
/** /**
* Compute the HMAC. * KDF: Key-Derivation Function
*
* The key-derivation function generates pseudorandom bits used to key the hash functions.
*
* @param int $index a non-negative integer less than 2^64
* @param int $numbytes a non-negative integer less than 2^64
* @return string string of length numbytes bytes
*/
private function kdf($index, $numbytes)
{
$this->c->setIV(pack('N4', 0, $index, 0, 1));
return $this->c->encrypt(str_repeat("\0", $numbytes));
}
/**
* PDF Algorithm
*
* @return string string of length taglen bytes.
*/
private function pdf()
{
$k = $this->key;
$nonce = $this->nonce;
$taglen = $this->length;
//
// Extract and zero low bit(s) of Nonce if needed
//
if ($taglen <= 8) {
$last = strlen($nonce) - 1;
$mask = $taglen == 4 ? "\3" : "\1";
$index = $nonce[$last] & $mask;
$nonce[$last] = $nonce[$last] ^ $index;
}
//
// Make Nonce BLOCKLEN bytes by appending zeroes if needed
//
$nonce = str_pad($nonce, 16, "\0");
//
// Generate subkey, encipher and extract indexed substring
//
$kp = $this->kdf(0, 16);
$c = new AES('ctr');
$c->disablePadding();
$c->setKey($kp);
$c->setIV($nonce);
$t = $c->encrypt("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
// we could use ord() but per https://paragonie.com/blog/2016/06/constant-time-encoding-boring-cryptography-rfc-4648-and-you
// unpack() doesn't leak timing info
return $taglen <= 8 ?
substr($t, unpack('C', $index)[1] * $taglen, $taglen) :
substr($t, 0, $taglen);
}
/**
* UHASH Algorithm
*
* @param string $m string of length less than 2^67 bits.
* @param int $taglen the integer 4, 8, 12 or 16.
* @return string string of length taglen bytes.
*/
private function uhash($m, $taglen)
{
//
// One internal iteration per 4 bytes of output
//
$iters = $taglen >> 2;
//
// Define total key needed for all iterations using KDF.
// L1Key reuses most key material between iterations.
//
//$L1Key = $this->kdf(1, 1024 + ($iters - 1) * 16);
$L1Key = $this->kdf(1, (1024 + ($iters - 1)) * 16);
$L2Key = $this->kdf(2, $iters * 24);
$L3Key1 = $this->kdf(3, $iters * 64);
$L3Key2 = $this->kdf(4, $iters * 4);
//
// For each iteration, extract key and do three-layer hash.
// If bytelength(M) <= 1024, then skip L2-HASH.
//
$y = '';
for ($i = 0; $i < $iters; $i++) {
$L1Key_i = substr($L1Key, $i * 16, 1024);
$L2Key_i = substr($L2Key, $i * 24, 24);
$L3Key1_i = substr($L3Key1, $i * 64, 64);
$L3Key2_i = substr($L3Key2, $i * 4, 4);
$a = self::L1Hash($L1Key_i, $m);
$b = strlen($m) <= 1024 ? "\0\0\0\0\0\0\0\0$a" : self::L2Hash($L2Key_i, $a);
$c = self::L3Hash($L3Key1_i, $L3Key2_i, $b);
$y.= $c;
}
return $y;
}
/**
* L1-HASH Algorithm
*
* The first-layer hash breaks the message into 1024-byte chunks and
* hashes each with a function called NH. Concatenating the results
* forms a string, which is up to 128 times shorter than the original.
*
* @param string $k string of length 1024 bytes.
* @param string $m string of length less than 2^67 bits.
* @return string string of length (8 * ceil(bitlength(M)/8192)) bytes.
*/
private static function L1Hash($k, $m)
{
//
// Break M into 1024 byte chunks (final chunk may be shorter)
//
$m = str_split($m, 1024);
//
// For each chunk, except the last: endian-adjust, NH hash
// and add bit-length. Use results to build Y.
//
$length = new BigInteger(1024 * 8);
$y = '';
for ($i = 0; $i < count($m) - 1; $i++) {
$m[$i] = pack('N*', ...unpack('V*', $m[$i])); // ENDIAN-SWAP
$y.= static::nh($k, $m[$i], $length);
}
//
// For the last chunk: pad to 32-byte boundary, endian-adjust,
// NH hash and add bit-length. Concatenate the result to Y.
//
$length = strlen($m[$i]);
$pad = 32 - ($length % 32);
$m[$i] = str_pad($m[$i], $length + $pad, "\0"); // zeropad
$m[$i] = pack('N*', ...unpack('V*', $m[$i])); // ENDIAN-SWAP
$y.= static::nh($k, $m[$i], new BigInteger($length * 8));
return $y;
}
/**
* NH Algorithm
*
* @param string $k string of length 1024 bytes.
* @param string $m string with length divisible by 32 bytes.
* @return string string of length 8 bytes.
*/
private static function nh($k, $m, $length)
{
$toUInt32 = function($x) {
$x = new BigInteger($x, 256);
$x->setPrecision(32);
return $x;
};
//
// Break M and K into 4-byte chunks
//
//$t = strlen($m) >> 2;
$m = str_split($m, 4);
$t = count($m);
$k = str_split($k, 4);
$k = array_pad(array_slice($k, 0, $t), $t, 0);
$m = array_map($toUInt32, $m);
$k = array_map($toUInt32, $k);
//
// Perform NH hash on the chunks, pairing words for multiplication
// which are 4 apart to accommodate vector-parallelism.
//
$y = new BigInteger;
$y->setPrecision(64);
$i = 0;
while ($i < $t) {
$temp = $m[$i]->add($k[$i]);
$temp->setPrecision(64);
$temp = $temp->multiply($m[$i + 4]->add($k[$i + 4]));
$y = $y->add($temp);
$temp = $m[$i + 1]->add($k[$i + 1]);
$temp->setPrecision(64);
$temp = $temp->multiply($m[$i + 5]->add($k[$i + 5]));
$y = $y->add($temp);
$temp = $m[$i + 2]->add($k[$i + 2]);
$temp->setPrecision(64);
$temp = $temp->multiply($m[$i + 6]->add($k[$i + 6]));
$y = $y->add($temp);
$temp = $m[$i + 3]->add($k[$i + 3]);
$temp->setPrecision(64);
$temp = $temp->multiply($m[$i + 7]->add($k[$i + 7]));
$y = $y->add($temp);
$i+= 8;
}
return $y->add($length)->toBytes();
}
/**
* L2-HASH: Second-Layer Hash
*
* The second-layer rehashes the L1-HASH output using a polynomial hash
* called POLY. If the L1-HASH output is long, then POLY is called once
* on a prefix of the L1-HASH output and called using different settings
* on the remainder. (This two-step hashing of the L1-HASH output is
* needed only if the message length is greater than 16 megabytes.)
* Careful implementation of POLY is necessary to avoid a possible
* timing attack (see Section 6.6 for more information).
*
* @param string $k string of length 24 bytes.
* @param string $m string of length less than 2^64 bytes.
* @return string string of length 16 bytes.
*/
private static function L2Hash($k, $m)
{
//
// Extract keys and restrict to special key-sets
//
$k64 = $k & "\x01\xFF\xFF\xFF\x01\xFF\xFF\xFF";
$k64 = new BigInteger($k64, 256);
$k128 = substr($k, 8) & "\x01\xFF\xFF\xFF\x01\xFF\xFF\xFF\x01\xFF\xFF\xFF\x01\xFF\xFF\xFF";
$k128 = new BigInteger($k128, 256);
//
// If M is no more than 2^17 bytes, hash under 64-bit prime,
// otherwise, hash first 2^17 bytes under 64-bit prime and
// remainder under 128-bit prime.
//
if (strlen($m) <= 0x20000) { // 2^14 64-bit words
$y = self::poly(64, self::$maxwordrange64, $k64, $m);
} else {
$m_1 = substr($m, 0, 0x20000); // 1 << 17
$m_2 = substr($m, 0x20000) . "\x80";
$length = strlen($m_2);
$pad = 16 - ($length % 16);
$m_2 = str_pad($m_2, $length + $pad, "\0"); // zeropad
$y = self::poly(64, self::$maxwordrange64, $k64, $m_1);
$y = str_pad($y, 16, "\0", STR_PAD_LEFT);
$y = self::poly(128, self::$maxwordrange128, $k128, $y . $m_2);
}
return str_pad($y, 16, "\0", STR_PAD_LEFT);
}
/**
* POLY Algorithm
*
* @param int $wordbits the integer 64 or 128.
* @param BigInteger $maxwordrange positive integer less than 2^wordbits.
* @param BigInteger $k integer in the range 0 ... prime(wordbits) - 1.
* @param string $m string with length divisible by (wordbits / 8) bytes.
* @return integer in the range 0 ... prime(wordbits) - 1.
*/
private static function poly($wordbits, $maxwordrange, $k, $m)
{
//
// Define constants used for fixing out-of-range words
//
$wordbytes = $wordbits >> 3;
if ($wordbits == 128) {
$factory = self::$factory128;
$offset = self::$offset128;
$marker = self::$marker128;
} else {
$factory = self::$factory64;
$offset = self::$offset64;
$marker = self::$marker64;
}
$k = $factory->newInteger($k);
//
// Break M into chunks of length wordbytes bytes
//
$m_i = str_split($m, $wordbytes);
//
// Each input word m is compared with maxwordrange. If not smaller
// then 'marker' and (m - offset), both in range, are hashed.
//
$y = $factory->newInteger(new BigInteger(1));
foreach ($m_i as $m) {
$m = $factory->newInteger(new BigInteger($m, 256));
if ($m->compare($maxwordrange) >= 0) {
$y = $k->multiply($y)->add($marker);
$y = $k->multiply($y)->add($m->subtract($offset));
} else {
$y = $k->multiply($y)->add($m);
}
}
return $y->toBytes();
}
/**
* L3-HASH: Third-Layer Hash
*
* The output from L2-HASH is 16 bytes long. This final hash function
* hashes the 16-byte string to a fixed length of 4 bytes.
*
* @param string $k1 string of length 64 bytes.
* @param string $k2 string of length 4 bytes.
* @param string $m string of length 16 bytes.
* @return string string of length 4 bytes.
*/
private static function L3Hash($k1, $k2, $m)
{
$factory = self::$factory36;
$y = $factory->newInteger(new BigInteger());
for ($i = 0; $i < 8; $i++) {
$m_i = $factory->newInteger(new BigInteger(substr($m, 2 * $i, 2), 256));
$k_i = $factory->newInteger(new BigInteger(substr($k1, 8 * $i, 8), 256));
$y = $y->add($m_i->multiply($k_i));
}
$y = str_pad(substr($y->toBytes(), -4), 4, "\0", STR_PAD_LEFT);
$y = $y ^ $k2;
return $y;
}
/**
* Compute the Hash / HMAC / UMAC.
* *
* @access public * @access public
* @param string $text * @param string $text
@ -366,6 +783,58 @@ class Hash
*/ */
public function hash($text) public function hash($text)
{ {
if ($this->hash == 'umac') {
if ($this->recomputeAESKey) {
if (!is_string($this->nonce)) {
throw new InsufficientSetupException('No nonce has been set');
}
if (!is_string($this->key)) {
throw new InsufficientSetupException('No key has been set');
}
if (strlen($this->key) != 16) {
throw new \LengthException('Key must be 16 bytes long');
}
if (!isset(self::$maxwordrange64)) {
$one = new BigInteger(1);
$prime36 = new BigInteger("\x00\x00\x00\x0F\xFF\xFF\xFF\xFB", 256);
self::$factory36 = new PrimeField($prime36);
$prime64 = new BigInteger("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC5", 256);
self::$factory64 = new PrimeField($prime64);
$prime128 = new BigInteger("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x61", 256);
self::$factory128 = new PrimeField($prime128);
self::$offset64 = new BigInteger("\1\0\0\0\0\0\0\0\0", 256);
self::$offset64 = self::$factory64->newInteger(self::$offset64->subtract($prime64));
self::$offset128 = new BigInteger("\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 256);
self::$offset128 = self::$factory128->newInteger(self::$offset128->subtract($prime128));
self::$marker64 = self::$factory64->newInteger($prime64->subtract($one));
self::$marker128 = self::$factory128->newInteger($prime128->subtract($one));
$maxwordrange64 = $one->bitwise_leftShift(64)->subtract($one->bitwise_leftShift(32));
self::$maxwordrange64 = self::$factory64->newInteger($maxwordrange64);
$maxwordrange128 = $one->bitwise_leftShift(128)->subtract($one->bitwise_leftShift(96));
self::$maxwordrange128 = self::$factory128->newInteger($maxwordrange128);
}
$this->c = new AES('ctr');
$this->c->disablePadding();
$this->c->setKey($this->key);
$this->pad = $this->pdf();
$this->recomputeAESKey = false;
}
$hashedmessage = $this->uhash($text, $this->length);
return $hashedmessage ^ $this->pad;
}
if (is_array($this->hash)) { if (is_array($this->hash)) {
if (empty($this->key) || !is_string($this->key)) { if (empty($this->key) || !is_string($this->key)) {
return substr(call_user_func($this->hash, $text, ...array_values($this->parameters)), 0, $this->length); return substr(call_user_func($this->hash, $text, ...array_values($this->parameters)), 0, $this->length);

View File

@ -419,4 +419,49 @@ class Unit_Crypt_HashTest extends PhpseclibTestCase
['sha512', 64], ['sha512', 64],
]; ];
} }
public function UMACs()
{
return [
['', 'umac-32', '113145FB', "umac-32 and message of <empty>"],
['', 'umac-64', '6E155FAD26900BE1', "umac-64 and message of <empty>"],
['', 'umac-96', '32FEDB100C79AD58F07FF764', "umac-96 and message of <empty>"],
['aaa', 'umac-32', '3B91D102', "umac-32 and message of 'a' * 3"],
['aaa', 'umac-64', '44B5CB542F220104', "umac-64 and message of 'a' * 3"],
['aaa', 'umac-96', '185E4FE905CBA7BD85E4C2DC', "umac-96 and message of 'a' * 3"],
[str_repeat('a', 1 << 10), 'umac-32', '599B350B', "umac-32 and message of 'a' * 2^10"],
[str_repeat('a', 1 << 10), 'umac-64', '26BF2F5D60118BD9', "umac-64 and message of 'a' * 2^10"],
[str_repeat('a', 1 << 10), 'umac-96', '7A54ABE04AF82D60FB298C3C', "umac-96 and message of 'a' * 2^10"],
[str_repeat('a', 1 << 15), 'umac-32', '58DCF532', "umac-32 and message of 'a' * 2^15"],
[str_repeat('a', 1 << 15), 'umac-64', '27F8EF643B0D118D', "umac-64 and message of 'a' * 2^15"],
[str_repeat('a', 1 << 15), 'umac-96', '7B136BD911E4B734286EF2BE', "umac-96 and message of 'a' * 2^15"],
//[str_repeat('a', 1 << 20), 'umac-32', 'DB6364D1', "umac-32 and message of 'a' * 2^20"],
//[str_repeat('a', 1 << 20), 'umac-64', 'A4477E87E9F55853', "umac-64 and message of 'a' * 2^20"],
//[str_repeat('a', 1 << 20), 'umac-96', 'F8ACFA3AC31CFEEA047F7B11', "umac-96 and message of 'a' * 2^20"],
//[str_repeat('a', 1 << 25), 'umac-32', '5109A660', "umac-32 and message of 'a' * 2^25"],
//[str_repeat('a', 1 << 25), 'umac-64', '2E2DBC36860A0A5F', "umac-64 and message of 'a' * 2^25"],
//[str_repeat('a', 1 << 25), 'umac-96', '72C6388BACE3ACE6FBF062D9', "umac-96 and message of 'a' * 2^25"],
['abc', 'umac-32', 'ABF3A3A0', "umac-32 and message of 'abc' * 1"],
['abc', 'umac-64', 'D4D7B9F6BD4FBFCF', "umac-64 and message of 'abc' * 1"],
['abc', 'umac-96', '883C3D4B97A61976FFCF2323', "umac-96 and message of 'abc' * 1"],
[str_repeat('abc', 500), 'umac-32', 'ABEB3C8B', "umac-32 and message of 'abc' * 500"],
[str_repeat('abc', 500), 'umac-64', 'D4CF26DDEFD5C01A', "umac-64 and message of 'abc' * 500"],
[str_repeat('abc', 500), 'umac-96', '8824A260C53C66A36C9260A6', "umac-96 and message of 'abc' * 500"],
];
}
/**
* @dataProvider UMACs
*/
public function testUMACs($message, $algo, $tag, $error)
{
$k = 'abcdefghijklmnop'; // A 16-byte UMAC key
$n = 'bcdefghi'; // An 8-byte nonce
$hash = new Hash($algo);
$hash->setNonce($n);
$hash->setKey($k);
$this->assertSame($hash->hash($message), pack('H*', $tag), $error);
}
} }