diff --git a/phpseclib/Crypt/Hash.php b/phpseclib/Crypt/Hash.php index 9bba923c..41615317 100644 --- a/phpseclib/Crypt/Hash.php +++ b/phpseclib/Crypt/Hash.php @@ -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); diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 054778e5..120b796d 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -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 : 'hmac-sha2-256',// RECOMMENDED HMAC-SHA256 (digest length = key length = 32) + 'hmac-sha2-512',// OPTIONAL HMAC-SHA512 (digest length = key length = 64) + + // from : + '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) diff --git a/tests/Unit/Crypt/HashTest.php b/tests/Unit/Crypt/HashTest.php index 087a4c65..9ab52f3b 100644 --- a/tests/Unit/Crypt/HashTest.php +++ b/tests/Unit/Crypt/HashTest.php @@ -419,4 +419,49 @@ class Unit_Crypt_HashTest extends PhpseclibTestCase ['sha512', 64], ]; } + + public function UMACs() + { + return [ + ['', 'umac-32', '113145FB', "umac-32 and message of "], + ['', 'umac-64', '6E155FAD26900BE1', "umac-64 and message of "], + ['', 'umac-96', '32FEDB100C79AD58F07FF764', "umac-96 and message of "], + ['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); + } }