1
0
mirror of https://github.com/danog/phpseclib.git synced 2024-11-26 20:35:21 +01:00

Merge pull request #1403 from terrafrost/hmac-additions

add new HMAC algorithms
This commit is contained in:
terrafrost 2019-09-16 07:41:54 -05:00 committed by GitHub
commit 45d787a578
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 650 additions and 82 deletions

View File

@ -35,7 +35,10 @@ namespace phpseclib\Crypt;
use phpseclib\Math\BigInteger;
use phpseclib\Exception\UnsupportedAlgorithmException;
use phpseclib\Exception\InsufficientSetupException;
use phpseclib\Common\Functions\Strings;
use phpseclib\Crypt\AES;
use phpseclib\Math\PrimeField;
/**
* @package Hash
@ -101,6 +104,15 @@ class Hash
*/
private $key = false;
/**
* Nonce
*
* @see self::setNonce()
* @var string
* @access private
*/
private $nonce = false;
/**
* Hash Parameters
*
@ -140,6 +152,51 @@ class Hash
*/
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.
*
@ -163,6 +220,28 @@ class Hash
{
$this->key = $key;
$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);
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 'md5-96':
case 'sha1-96':
@ -358,7 +445,339 @@ 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);
$pad = max(32, $length + $pad % 32);
$m[$i] = str_pad($m[$i], $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);
$pad%= 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
* @param string $text
@ -366,6 +785,58 @@ class Hash
*/
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 (empty($this->key) || !is_string($this->key)) {
return substr(call_user_func($this->hash, $text, ...array_values($this->parameters)), 0, $this->length);

View File

@ -1464,14 +1464,14 @@ class SSH2
// we don't initialize any crypto-objects, yet - we do that, later. for now, we need the lengths to make the
// diffie-hellman key exchange as fast as possible
$decrypt = $this->array_intersect_first($s2c_encryption_algorithms, $this->encryption_algorithms_server_to_client);
$decrypt = self::array_intersect_first($s2c_encryption_algorithms, $this->encryption_algorithms_server_to_client);
$decryptKeyLength = $this->encryption_algorithm_to_key_size($decrypt);
if ($decryptKeyLength === null) {
$this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
throw new NoSupportedAlgorithmsException('No compatible server to client encryption algorithms found');
}
$encrypt = $this->array_intersect_first($c2s_encryption_algorithms, $this->encryption_algorithms_client_to_server);
$encrypt = self::array_intersect_first($c2s_encryption_algorithms, $this->encryption_algorithms_client_to_server);
$encryptKeyLength = $this->encryption_algorithm_to_key_size($encrypt);
if ($encryptKeyLength === null) {
$this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
@ -1479,7 +1479,7 @@ class SSH2
}
// through diffie-hellman key exchange a symmetric key is obtained
$this->kex_algorithm = $this->array_intersect_first($kex_algorithms, $this->kex_algorithms);
$this->kex_algorithm = self::array_intersect_first($kex_algorithms, $this->kex_algorithms);
if ($this->kex_algorithm === false) {
$this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
throw new NoSupportedAlgorithmsException('No compatible key exchange algorithms found');
@ -1623,7 +1623,7 @@ class SSH2
$this->session_id = $this->exchange_hash;
}
$server_host_key_algorithm = $this->array_intersect_first($server_host_key_algorithms, $this->server_host_key_algorithms);
$server_host_key_algorithm = self::array_intersect_first($server_host_key_algorithms, $this->server_host_key_algorithms);
if ($server_host_key_algorithm === false) {
$this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
throw new NoSupportedAlgorithmsException('No compatible server host key algorithms found');
@ -1768,39 +1768,19 @@ class SSH2
$this->decrypt->decrypt(str_repeat("\0", 1536));
}
$mac_algorithm = $this->array_intersect_first($c2s_mac_algorithms, $this->mac_algorithms_client_to_server);
$mac_algorithm = self::array_intersect_first($c2s_mac_algorithms, $this->mac_algorithms_client_to_server);
if ($mac_algorithm === false) {
$this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
throw new NoSupportedAlgorithmsException('No compatible client to server message authentication algorithms found');
}
if ($this->encrypt->usesNonce()) {
if (!$this->encrypt->usesNonce()) {
list($this->hmac_create, $createKeyLength) = self::mac_algorithm_to_hash_instance($mac_algorithm);
} else {
$this->hmac_create = new \stdClass;
$this->hmac_create->name = $mac_algorithm;
$mac_algorithm = 'none';
}
$createKeyLength = 0; // ie. $mac_algorithm == 'none'
switch ($mac_algorithm) {
case 'hmac-sha2-256':
$this->hmac_create = new Hash('sha256');
$createKeyLength = 32;
break;
case 'hmac-sha1':
$this->hmac_create = new Hash('sha1');
$createKeyLength = 20;
break;
case 'hmac-sha1-96':
$this->hmac_create = new Hash('sha1-96');
$createKeyLength = 20;
break;
case 'hmac-md5':
$this->hmac_create = new Hash('md5');
$createKeyLength = 16;
break;
case 'hmac-md5-96':
$this->hmac_create = new Hash('md5-96');
$createKeyLength = 16;
//$mac_algorithm = 'none';
$createKeyLength = 0;
}
if ($this->hmac_create instanceof Hash) {
@ -1810,47 +1790,24 @@ class SSH2
}
$this->hmac_create->setKey(substr($key, 0, $createKeyLength));
$this->hmac_create->name = $mac_algorithm;
$this->hmac_create->etm = preg_match('#-etm@openssh\.com$#', $mac_algorithm);
}
$mac_algorithm = $this->array_intersect_first($s2c_mac_algorithms, $this->mac_algorithms_server_to_client);
$mac_algorithm = self::array_intersect_first($s2c_mac_algorithms, $this->mac_algorithms_server_to_client);
if ($mac_algorithm === false) {
$this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
throw new NoSupportedAlgorithmsException('No compatible server to client message authentication algorithms found');
}
if ($this->decrypt->usesNonce()) {
if (!$this->decrypt->usesNonce()) {
list($this->hmac_check, $checkKeyLength) = self::mac_algorithm_to_hash_instance($mac_algorithm);
$this->hmac_size = $this->hmac_check->getLengthInBytes();
} else {
$this->hmac_check = new \stdClass;
$this->hmac_check->name = $mac_algorithm;
$mac_algorithm = 'none';
}
$checkKeyLength = 0;
$this->hmac_size = 0;
switch ($mac_algorithm) {
case 'hmac-sha2-256':
$this->hmac_check = new Hash('sha256');
$checkKeyLength = 32;
$this->hmac_size = 32;
break;
case 'hmac-sha1':
$this->hmac_check = new Hash('sha1');
$checkKeyLength = 20;
$this->hmac_size = 20;
break;
case 'hmac-sha1-96':
$this->hmac_check = new Hash('sha1-96');
$checkKeyLength = 20;
$this->hmac_size = 12;
break;
case 'hmac-md5':
$this->hmac_check = new Hash('md5');
$checkKeyLength = 16;
$this->hmac_size = 16;
break;
case 'hmac-md5-96':
$this->hmac_check = new Hash('md5-96');
$checkKeyLength = 16;
$this->hmac_size = 12;
//$mac_algorithm = 'none';
$checkKeyLength = 0;
$this->hmac_size = 0;
}
if ($this->hmac_check instanceof Hash) {
@ -1860,16 +1817,17 @@ class SSH2
}
$this->hmac_check->setKey(substr($key, 0, $checkKeyLength));
$this->hmac_check->name = $mac_algorithm;
$this->hmac_check->etm = preg_match('#-etm@openssh\.com$#', $mac_algorithm);
}
$compression_algorithm = $this->array_intersect_first($s2c_compression_algorithms, $this->compression_algorithms_server_to_client);
$compression_algorithm = self::array_intersect_first($s2c_compression_algorithms, $this->compression_algorithms_server_to_client);
if ($compression_algorithm === false) {
$this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
throw new NoSupportedAlgorithmsException('No compatible server to client compression algorithms found');
}
$this->decompress = $compression_algorithm == 'zlib';
$compression_algorithm = $this->array_intersect_first($c2s_compression_algorithms, $this->compression_algorithms_client_to_server);
$compression_algorithm = self::array_intersect_first($c2s_compression_algorithms, $this->compression_algorithms_client_to_server);
if ($compression_algorithm === false) {
$this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
throw new NoSupportedAlgorithmsException('No compatible client to server compression algorithms found');
@ -1928,10 +1886,10 @@ class SSH2
/**
* Maps an encryption algorithm name to an instance of a subclass of
* \phpseclib\Crypt\Base.
* \phpseclib\Crypt\Common\SymmetricKey.
*
* @param string $algorithm Name of the encryption algorithm
* @return mixed Instance of \phpseclib\Crypt\Base or null for unknown
* @return mixed Instance of \phpseclib\Crypt\Common\SymmetricKey or null for unknown
* @access private
*/
private static function encryption_algorithm_to_crypt_instance($algorithm)
@ -1975,6 +1933,41 @@ class SSH2
return null;
}
/**
* Maps an encryption algorithm name to an instance of a subclass of
* \phpseclib\Crypt\Hash.
*
* @param string $algorithm Name of the encryption algorithm
* @return mixed Instance of \phpseclib\Crypt\Hash or null for unknown
* @access private
*/
private static function mac_algorithm_to_hash_instance($algorithm)
{
switch ($algorithm) {
case 'umac-64@openssh.com':
case 'umac-64-etm@openssh.com':
return [new Hash('umac-64'), 16];
case 'umac-128@openssh.com':
case 'umac-128-etm@openssh.com':
return [new Hash('umac-128'), 16];
case 'hmac-sha2-512':
case 'hmac-sha2-512-etm@openssh.com':
return [new Hash('sha512'), 64];
case 'hmac-sha2-256':
case 'hmac-sha2-256-etm@openssh.com':
return [new Hash('sha256'), 32];
case 'hmac-sha1':
case 'hmac-sha1-etm@openssh.com':
return [new Hash('sha1'), 20];
case 'hmac-sha1-96':
return [new Hash('sha1-96'), 20];
case 'hmac-md5':
return [new Hash('md5'), 16];
case 'hmac-md5-96':
return [new Hash('md5-96'), 16];
}
}
/*
* Tests whether or not proposed algorithm has a potential for issues
*
@ -3198,7 +3191,19 @@ class SSH2
$remaining_length = 0;
break;
default:
$raw = $this->decrypt->decrypt($raw);
if (!$this->hmac_check instanceof Hash || !$this->hmac_check->etm) {
$raw = $this->decrypt->decrypt($raw);
break;
}
extract(unpack('Npacket_length', $temp = Strings::shift($raw, 4)));
/**
* @var integer $packet_length
*/
$raw.= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4);
$stop = microtime(true);
$encrypted = $temp . $raw;
$raw = $temp . $this->decrypt->decrypt($raw);
$remaining_length = 0;
}
}
@ -3232,8 +3237,20 @@ class SSH2
if ($hmac === false || strlen($hmac) != $this->hmac_size) {
$this->bitmap = 0;
throw new \RuntimeException('Error reading socket');
} elseif ($hmac != $this->hmac_check->hash(pack('NNCa*', $this->get_seq_no, $packet_length, $padding_length, $payload . $padding))) {
throw new \RuntimeException('Invalid HMAC');
}
$reconstructed = !$this->hmac_check->etm ?
pack('NCa*', $packet_length, $padding_length, $payload . $padding) :
$encrypted;
if (($this->hmac_check->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') {
$this->hmac_check->setNonce("\0\0\0\0" . pack('N', $this->get_seq_no));
if ($hmac != $this->hmac_check->hash($reconstructed)) {
throw new \RuntimeException('Invalid UMAC');
}
} else {
if ($hmac != $this->hmac_check->hash(pack('Na*', $this->get_seq_no, $reconstructed))) {
throw new \RuntimeException('Invalid HMAC');
}
}
}
@ -3271,10 +3288,11 @@ class SSH2
$adjustLength = false;
if ($this->decrypt) {
switch ($this->decrypt->name) {
case 'aes128-gcm@openssh.com':
case 'aes256-gcm@openssh.com':
case 'chacha20-poly1305@openssh.com':
switch (true) {
case $this->decrypt->name == 'aes128-gcm@openssh.com':
case $this->decrypt->name == 'aes256-gcm@openssh.com':
case $this->decrypt->name == 'chacha20-poly1305@openssh.com':
case $this->hmac_check instanceof Hash && $this->hmac_check->etm:
$remaining_length+= $this->decrypt_block_size - 4;
$adjustLength = true;
}
@ -3787,17 +3805,27 @@ class SSH2
$packet_length+= (($this->encrypt_block_size - 1) * $packet_length) % $this->encrypt_block_size;
// subtracting strlen($data) is obvious - subtracting 5 is necessary because of packet_length and padding_length
$padding_length = $packet_length - strlen($data) - 5;
if ($this->encrypt && $this->encrypt->usesNonce()) {
$padding_length+= 4;
$packet_length+= 4;
switch (true) {
case $this->encrypt && $this->encrypt->usesNonce():
case $this->hmac_create instanceof Hash && $this->hmac_create->etm:
$padding_length+= 4;
$packet_length+= 4;
}
$padding = Random::string($padding_length);
// we subtract 4 from packet_length because the packet_length field isn't supposed to include itself
$packet = pack('NCa*', $packet_length - 4, $padding_length, $data . $padding);
$hmac = $this->hmac_create instanceof Hash ? $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet)) : '';
$this->send_seq_no++;
$hmac = '';
if ($this->hmac_create instanceof Hash && !$this->hmac_create->etm) {
if (($this->hmac_create->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') {
$this->hmac_create->setNonce("\0\0\0\0" . pack('N', $this->send_seq_no));
$hmac = $this->hmac_create->hash($packet);
} else {
$hmac = $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet));
}
}
if ($this->encrypt) {
switch ($this->encrypt->name) {
@ -3812,7 +3840,7 @@ class SSH2
$packet = $temp . $this->encrypt->encrypt(substr($packet, 4));
break;
case 'chacha20-poly1305@openssh.com':
$nonce = pack('N2', 0, $this->send_seq_no - 1);
$nonce = pack('N2', 0, $this->send_seq_no);
$this->encrypt->setNonce($nonce);
$this->lengthEncrypt->setNonce($nonce);
@ -3831,10 +3859,23 @@ class SSH2
$packet = $length . $this->encrypt->encrypt(substr($packet, 4));
break;
default:
$packet = $this->encrypt->encrypt($packet);
$packet = $this->hmac_create instanceof Hash && $this->hmac_create->etm ?
($packet & "\xFF\xFF\xFF\xFF") . $this->encrypt->encrypt(substr($packet, 4)) :
$this->encrypt->encrypt($packet);
}
}
if ($this->hmac_create instanceof Hash && $this->hmac_create->etm) {
if (($this->hmac_create->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') {
$this->hmac_create->setNonce("\0\0\0\0" . pack('N', $this->send_seq_no));
$hmac = $this->hmac_create->hash($packet);
} else {
$hmac = $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet));
}
}
$this->send_seq_no++;
$packet.= $this->encrypt && $this->encrypt->usesNonce() ? $this->encrypt->getTag() : $hmac;
$start = microtime(true);
@ -4155,7 +4196,7 @@ class SSH2
* @return mixed False if intersection is empty, else intersected value.
* @access private
*/
private function array_intersect_first($array1, $array2)
private static function array_intersect_first($array1, $array2)
{
foreach ($array1 as $value) {
if (in_array($value, $array2)) {
@ -4401,8 +4442,19 @@ class SSH2
public static function getSupportedMACAlgorithms()
{
return [
'hmac-sha2-256-etm@openssh.com',
'hmac-sha2-512-etm@openssh.com',
'umac-64-etm@openssh.com',
'umac-128-etm@openssh.com',
'hmac-sha1-etm@openssh.com',
// from <http://www.ietf.org/rfc/rfc6668.txt>:
'hmac-sha2-256',// RECOMMENDED HMAC-SHA256 (digest length = key length = 32)
'hmac-sha2-512',// OPTIONAL HMAC-SHA512 (digest length = key length = 64)
// from <https://tools.ietf.org/html/draft-miller-secsh-umac-01>:
'umac-64@openssh.com',
'umac-128@openssh.com',
'hmac-sha1-96', // RECOMMENDED first 96 bits of HMAC-SHA1 (digest length = 12, key length = 20)
'hmac-sha1', // REQUIRED HMAC-SHA1 (digest length = key length = 20)

View File

@ -419,4 +419,49 @@ class Unit_Crypt_HashTest extends PhpseclibTestCase
['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);
}
}