From 35832fe2a15876558c9d759e864d6e195370daec Mon Sep 17 00:00:00 2001 From: terrafrost Date: Sun, 16 Dec 2012 02:20:16 -0600 Subject: [PATCH] Refactor crypt_random (renaming it to crypt_random_string) ...and update all the calls to it accordingly --- phpseclib/Crypt/RSA.php | 40 ++---- phpseclib/Crypt/Random.php | 222 +++++++++++++++++++++++----------- phpseclib/Math/BigInteger.php | 61 ++++++---- phpseclib/Net/SSH1.php | 22 ++-- phpseclib/Net/SSH2.php | 16 +-- 5 files changed, 214 insertions(+), 147 deletions(-) diff --git a/phpseclib/Crypt/RSA.php b/phpseclib/Crypt/RSA.php index cd3d2bae..5fe34bd4 100644 --- a/phpseclib/Crypt/RSA.php +++ b/phpseclib/Crypt/RSA.php @@ -79,11 +79,11 @@ if (!class_exists('Math_BigInteger')) { /** * Include Crypt_Random */ -// the class_exists() will only be called if the crypt_random function hasn't been defined and +// the class_exists() will only be called if the crypt_random_string function hasn't been defined and // will trigger a call to __autoload() if you're wanting to auto-load classes // call function_exists() a second time to stop the require_once from being called outside // of the auto loader -if (!function_exists('crypt_random') && !class_exists('Crypt_Random') && !function_exists('crypt_random')) { +if (!function_exists('crypt_random_string') && !class_exists('Crypt_Random') && !function_exists('crypt_random_string')) { require_once('Crypt/Random.php'); } @@ -560,7 +560,6 @@ class Crypt_RSA { extract($this->_generateMinMax($temp)); $generator = new Math_BigInteger(); - $generator->setRandomGenerator('crypt_random'); $n = $this->one->copy(); if (!empty($partial)) { @@ -740,7 +739,7 @@ class Crypt_RSA { $source.= pack('Na*', strlen($private), $private); $hashkey = 'putty-private-key-file-mac-key'; } else { - $private.= $this->_random(16 - (strlen($private) & 15)); + $private.= crypt_random_string(16 - (strlen($private) & 15)); $source.= pack('Na*', strlen($private), $private); if (!class_exists('Crypt_AES')) { require_once('Crypt/AES.php'); @@ -800,7 +799,7 @@ class Crypt_RSA { $RSAPrivateKey = pack('Ca*a*', CRYPT_RSA_ASN1_SEQUENCE, $this->_encodeLength(strlen($RSAPrivateKey)), $RSAPrivateKey); if (!empty($this->password) || is_string($this->password)) { - $iv = $this->_random(8); + $iv = crypt_random_string(8); $symkey = pack('H*', md5($this->password . $iv)); // symkey is short for symmetric key $symkey.= substr(pack('H*', md5($symkey . $this->password . $iv)), 0, 8); if (!class_exists('Crypt_TripleDES')) { @@ -1733,23 +1732,6 @@ class Crypt_RSA { $this->sLen = $sLen; } - /** - * Generates a random string x bytes long - * - * @access public - * @param Integer $bytes - * @param optional Integer $nonzero - * @return String - */ - function _random($bytes, $nonzero = false) - { - $temp = ''; - for ($i = 0; $i < $bytes; $i++) { - $temp.= chr(crypt_random($nonzero, 255)); - } - return $temp; - } - /** * Integer-to-Octet-String primitive * @@ -1832,7 +1814,6 @@ class Crypt_RSA { } $one = new Math_BigInteger(1); - $one->setRandomGenerator('crypt_random'); $r = $one->random($one, $smallest->subtract($one)); @@ -2040,7 +2021,7 @@ class Crypt_RSA { $lHash = $this->hash->hash($l); $ps = str_repeat(chr(0), $this->k - $mLen - 2 * $this->hLen - 2); $db = $lHash . $ps . chr(1) . $m; - $seed = $this->_random($this->hLen); + $seed = crypt_random_string($this->hLen); $dbMask = $this->_mgf1($seed, $this->k - $this->hLen - 1); $maskedDB = $db ^ $dbMask; $seedMask = $this->_mgf1($maskedDB, $this->hLen); @@ -2154,8 +2135,13 @@ class Crypt_RSA { } // EME-PKCS1-v1_5 encoding - - $ps = $this->_random($this->k - $mLen - 3, true); + $psLen = $this->k - $mLen - 3; + $ps = ''; + while (strlen($ps) != $psLen) { + $temp = crypt_random_string($psLen - strlen($ps)); + $temp = str_replace("\x00", '', $temp); + $ps.= $temp; + } $em = chr(0) . chr(2) . $ps . chr(0) . $m; // RSA encryption @@ -2251,7 +2237,7 @@ class Crypt_RSA { return false; } - $salt = $this->_random($sLen); + $salt = crypt_random_string($sLen); $m2 = "\0\0\0\0\0\0\0\0" . $mHash . $salt; $h = $this->hash->hash($m2); $ps = str_repeat(chr(0), $emLen - $sLen - $this->hLen - 2); diff --git a/phpseclib/Crypt/Random.php b/phpseclib/Crypt/Random.php index 3e14edd9..27be6b4f 100644 --- a/phpseclib/Crypt/Random.php +++ b/phpseclib/Crypt/Random.php @@ -11,7 +11,7 @@ * * * @@ -43,78 +43,129 @@ */ /** - * Generate a random value. + * Generate a random string. * - * On 32-bit machines, the largest distance that can exist between $min and $max is 2**31. - * If $min and $max are farther apart than that then the last ($max - range) numbers. + * Although microoptimizations are generally discouraged as they impair readability this function is ripe with + * microoptimizations because this function has the potential of being called a huge number of times. + * eg. for RSA key generation. * - * Depending on how this is being used, it may be worth while to write a replacement. For example, - * a PHP-based web app that stores its data in an SQL database can collect more entropy than this function - * can. - * - * @param optional Integer $min - * @param optional Integer $max - * @return Integer + * @param Integer $length + * @return String * @access public */ -function crypt_random($min = 0, $max = 0x7FFFFFFF) -{ - if ($min == $max) { - return $min; - } - - if (function_exists('openssl_random_pseudo_bytes')) { - // openssl_random_pseudo_bytes() is slow on windows per the following: - // http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php - if ((PHP_OS & "\xDF\xDF\xDF") !== 'WIN') { // PHP_OS & "\xDF\xDF\xDF" == strtoupper(substr(PHP_OS, 0, 3)), but a lot faster - extract(unpack('Nrandom', openssl_random_pseudo_bytes(4))); - - return abs($random) % ($max - $min) + $min; +function crypt_random_string($length) { + // PHP_OS & "\xDF\xDF\xDF" == strtoupper(substr(PHP_OS, 0, 3)), but a lot faster + if ((PHP_OS & "\xDF\xDF\xDF") === 'WIN') { + // method 1. prior to PHP 5.3 this would call rand() on windows hence the function_exists('class_alias') call. + // ie. class_alias is a function that was introduced in PHP 5.3 + if (function_exists('mcrypt_create_iv') && function_exists('class_alias')) { + return mcrypt_create_iv($length); + } + // method 2. openssl_random_pseudo_bytes was introduced in PHP 5.3.0 but prior to PHP 5.3.4 there was, + // to quote , "possible blocking behavior". as of 5.3.4 + // openssl_random_pseudo_bytes and mcrypt_create_iv do the exact same thing on Windows. ie. they both + // call php_win32_get_random_bytes(): + // + // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/openssl/openssl.c#L5008 + // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1392 + // + // php_win32_get_random_bytes() is defined thusly: + // + // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/win32/winutil.c#L80 + // + // we're calling it, all the same, in the off chance that the mcrypt extension is not available + if (function_exists('openssl_random_pseudo_bytes') && version_compare(PHP_VERSION, '5.3.4', '>=')) { + return openssl_random_pseudo_bytes($length); + } + } else { + // method 1. the fastest + if (function_exists('openssl_random_pseudo_bytes')) { + return openssl_random_pseudo_bytes($length); + } + // method 2 + static $fp = true; + if ($fp === true) { + // warning's will be output unles the error suppression operator is used. errors such as + // "open_basedir restriction in effect", "Permission denied", "No such file or directory", etc. + $fp = @fopen('/dev/urandom', 'rb'); + } + if ($fp !== true && $fp !== false) { // surprisingly faster than !is_bool() or is_resource() + return fread($urandom, $length); + } + // method 3. pretty much does the same thing as method 2 per the following url: + // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1391 + // surprisingly slower than method 2. maybe that's because mcrypt_create_iv does a bunch of error checking that we're + // not doing. regardless, this'll only be called if this PHP script couldn't open /dev/urandom due to open_basedir + // restrictions or some such + if (function_exists('mcrypt_create_iv')) { + return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); } } + // at this point we have no choice but to use a pure-PHP CSPRNG - // see http://en.wikipedia.org/wiki//dev/random - static $urandom = true; - if ($urandom === true) { - // Warning's will be output unles the error suppression operator is used. Errors such as - // "open_basedir restriction in effect", "Permission denied", "No such file or directory", etc. - $urandom = @fopen('/dev/urandom', 'rb'); - } - if (!is_bool($urandom)) { - extract(unpack('Nrandom', fread($urandom, 4))); - - // say $min = 0 and $max = 3. if we didn't do abs() then we could have stuff like this: - // -4 % 3 + 0 = -1, even though -1 < $min - return abs($random) % ($max - $min) + $min; - } - - /* Prior to PHP 4.2.0, mt_srand() had to be called before mt_rand() could be called. - Prior to PHP 5.2.6, mt_rand()'s automatic seeding was subpar, as elaborated here: - - http://www.suspekt.org/2008/08/17/mt_srand-and-not-so-random-numbers/ - - The seeding routine is pretty much ripped from PHP's own internal GENERATE_SEED() macro: - - http://svn.php.net/viewvc/php/php-src/tags/php_5_3_2/ext/standard/php_rand.h?view=markup */ - if (version_compare(PHP_VERSION, '5.2.5', '<=')) { - static $seeded; - if (!isset($seeded)) { - $seeded = true; - mt_srand(fmod(time() * getmypid(), 0x7FFFFFFF) ^ fmod(1000000 * lcg_value(), 0x7FFFFFFF)); + // cascade entropy across multiple PHP instances by fixing the session and collecting all + // environmental variables, including the previous session data and the current session + // data + static $crypto = false, $v; + if ($crypto === false) { + // save old session data + $old_session_id = session_id(); + $old_use_cookies = ini_get('session.use_cookies'); + $old_session_cache_limiter = session_cache_limiter(); + if (isset($_SESSION)) { + $_OLD_SESSION = $_SESSION; } - } - - static $crypto; - - // The CSPRNG's Yarrow and Fortuna periodically reseed. This function can be reseeded by hitting F5 - // in the browser and reloading the page. - - if (!isset($crypto)) { - $key = $iv = ''; - for ($i = 0; $i < 8; $i++) { - $key.= pack('n', mt_rand(0, 0xFFFF)); - $iv .= pack('n', mt_rand(0, 0xFFFF)); + if ($old_session_id != '') { + session_write_close(); } + + session_id(1); + ini_set('session.use_cookies', 0); + session_cache_limiter(''); + session_start(); + + $v = $seed = $_SESSION['seed'] = pack('H*', sha1( + serialize($_SERVER) . + serialize($_POST) . + serialize($_GET) . + serialize($_COOKIE) . + serialize($_GLOBAL) . + serialize($_SESSION) . + serialize($_OLD_SESSION) + )); + if (!isset($_SESSION['count'])) { + $_SESSION['count'] = 0; + } + $_SESSION['count']++; + + session_write_close(); + + // restore old session data + if ($old_session_id != '') { + session_id($old_session_id); + session_start(); + ini_set('session.use_cookies', $old_use_cookies); + session_cache_limiter($old_session_cache_limiter); + } else { + if (isset($_OLD_SESSION)) { + $_SESSION = $_OLD_SESSION; + unset($_OLD_SESSION); + } else { + unset($_SESSION); + } + } + + // in SSH2 a shared secret and an exchange hash are generated through the key exchange process. + // the IV client to server is the hash of that "nonce" with the letter A and for the encryption key it's the letter C. + // if the hash doesn't produce enough a key or an IV that's long enough concat successive hashes of the + // original hash and the current hash. we'll be emulating that. for more info see the following URL: + // + // http://tools.ietf.org/html/rfc4253#section-7.2 + // + // see the is_string($crypto) part for an example of how to expand the keys + $key = pack('H*', sha1($seed . 'A')); + $iv = pack('H*', sha1($seed . 'C')); + switch (true) { case class_exists('Crypt_AES'): $crypto = new Crypt_AES(CRYPT_AES_MODE_CTR); @@ -129,14 +180,47 @@ function crypt_random($min = 0, $max = 0x7FFFFFFF) $crypto = new Crypt_RC4(); break; default: - extract(unpack('Nrandom', pack('H*', sha1(mt_rand(0, 0x7FFFFFFF))))); - return abs($random) % ($max - $min) + $min; + $crypto = $seed; + return crypt_random_string($length); } + $crypto->setKey($key); $crypto->setIV($iv); $crypto->enableContinuousBuffer(); } - extract(unpack('Nrandom', $crypto->encrypt("\0\0\0\0"))); - return abs($random) % ($max - $min) + $min; -} + if (is_string($crypto)) { + // the following is based off of ANSI X9.31: + // + // http://csrc.nist.gov/groups/STM/cavp/documents/rng/931rngext.pdf + // + // OpenSSL uses that same standard for it's random numbers: + // + // http://www.opensource.apple.com/source/OpenSSL/OpenSSL-38/openssl/fips-1.0/rand/fips_rand.c + // (do a search for "ANS X9.31 A.2.4") + // + // ANSI X9.31 recommends ciphers be used and phpseclib does use them if they're available (see + // later on in the code) but if they're not we'll use sha1 + $result = ''; + while (strlen($result) < $length) { // each loop adds 20 bytes + // microtime() isn't packed as "densely" as it could be but then neither is that the idea. + // the idea is simply to ensure that each "block" has a unique element to it. + $i = pack('H*', sha1(microtime())); + $r = pack('H*', sha1($i ^ $v)); + $v = pack('H*', sha1($r ^ $i)); + $result.= $r; + } + return substr($result, 0, $length); + } + + //return $crypto->encrypt(str_repeat("\0", $length)); + + $result = ''; + while (strlen($result) < $length) { + $i = $crypto->encrypt(microtime()); + $r = $crypto->encrypt($i ^ $v); + $v = $crypto->encrypt($r ^ $i); + $result.= $r; + } + return substr($result, 0, $length); +} \ No newline at end of file diff --git a/phpseclib/Math/BigInteger.php b/phpseclib/Math/BigInteger.php index 925e3f78..04bcdf40 100644 --- a/phpseclib/Math/BigInteger.php +++ b/phpseclib/Math/BigInteger.php @@ -2995,20 +2995,13 @@ class Math_BigInteger { /** * Set random number generator function * - * $generator should be the name of a random generating function whose first parameter is the minimum - * value and whose second parameter is the maximum value. If this function needs to be seeded, it should - * be seeded prior to calling Math_BigInteger::random() or Math_BigInteger::randomPrime() + * This function is deprecated. * - * If the random generating function is not explicitly set, it'll be assumed to be mt_rand(). - * - * @see random() - * @see randomPrime() - * @param optional String $generator + * @param String $generator * @access public */ function setRandomGenerator($generator) { - $this->generator = $generator; } /** @@ -3045,27 +3038,43 @@ class Math_BigInteger { $max = $max->subtract($min); $max = ltrim($max->toBytes(), chr(0)); $size = strlen($max) - 1; - $random = ''; - $bytes = $size & 1; - for ($i = 0; $i < $bytes; ++$i) { - $random.= chr($generator(0, 255)); - } - - $blocks = $size >> 1; - for ($i = 0; $i < $blocks; ++$i) { - // mt_rand(-2147483648, 0x7FFFFFFF) always produces -2147483648 on some systems - $random.= pack('n', $generator(0, 0xFFFF)); - } - - $temp = new Math_BigInteger($random, 256); - if ($temp->compare(new Math_BigInteger(substr($max, 1), 256)) > 0) { - $random = chr($generator(0, ord($max[0]) - 1)) . $random; + $crypt_random = function_exists('crypt_random_string') || (!class_exists('Crypt_Random') && function_exists('crypt_random_string')); + if ($crypt_random) { + $random = crypt_random_string($size); } else { - $random = chr($generator(0, ord($max[0]) )) . $random; + $random = ''; + + if ($size & 1) { + $random.= chr(mt_rand(0, 255)); + } + + $blocks = $size >> 1; + for ($i = 0; $i < $blocks; ++$i) { + // mt_rand(-2147483648, 0x7FFFFFFF) always produces -2147483648 on some systems + $random.= pack('n', mt_rand(0, 0xFFFF)); + } } - $random = new Math_BigInteger($random, 256); + $fragment = new Math_BigInteger($random, 256); + $leading = $fragment->compare(new Math_BigInteger(substr($max, 1), 256)) > 0 ? + ord($max[0]) - 1 : ord($max[0]); + + if (!$crypt_random) { + $msb = chr(mt_rand(0, $leading)); + } else { + $cutoff = floor(0xFF / $leading) * $leading; + while (true) { + $msb = ord(crypt_random_string(1)); + if ($msb <= $cutoff) { + $msb%= $leading; + break; + } + } + $msb = chr($msb); + } + + $random = new Math_BigInteger($msb . $random, 256); return $this->_normalize($random->add($min)); } diff --git a/phpseclib/Net/SSH1.php b/phpseclib/Net/SSH1.php index 6b1f504e..50add201 100644 --- a/phpseclib/Net/SSH1.php +++ b/phpseclib/Net/SSH1.php @@ -104,11 +104,11 @@ if (!class_exists('Crypt_RC4')) { /** * Include Crypt_Random */ -// the class_exists() will only be called if the crypt_random function hasn't been defined and +// the class_exists() will only be called if the crypt_random_string function hasn't been defined and // will trigger a call to __autoload() if you're wanting to auto-load classes // call function_exists() a second time to stop the require_once from being called outside // of the auto loader -if (!function_exists('crypt_random') && !class_exists('Crypt_Random') && !function_exists('crypt_random')) { +if (!function_exists('crypt_random_string') && !class_exists('Crypt_Random') && !function_exists('crypt_random_string')) { require_once('Crypt/Random.php'); } @@ -542,10 +542,7 @@ class Net_SSH1 { $session_id = pack('H*', md5($host_key_public_modulus->toBytes() . $server_key_public_modulus->toBytes() . $anti_spoofing_cookie)); - $session_key = ''; - for ($i = 0; $i < 32; $i++) { - $session_key.= chr(crypt_random(0, 255)); - } + $session_key = crypt_random_string(32); $double_encrypted_session_key = $session_key ^ str_pad($session_id, 32, chr(0)); if ($server_key_public_modulus->compare($host_key_public_modulus) < 0) { @@ -1032,11 +1029,7 @@ class Net_SSH1 { $length = strlen($data) + 4; - $padding_length = 8 - ($length & 7); - $padding = ''; - for ($i = 0; $i < $padding_length; $i++) { - $padding.= chr(crypt_random(0, 255)); - } + $padding = crypt_random_string(8 - ($length & 7)); $data = $padding . $data; $data.= pack('N', $this->_crc($data)); @@ -1213,8 +1206,11 @@ class Net_SSH1 { $temp = chr(0) . chr(2); $modulus = $key[1]->toBytes(); $length = strlen($modulus) - strlen($m) - 3; - for ($i = 0; $i < $length; $i++) { - $temp.= chr(crypt_random(1, 255)); + $temp = ''; + while (strlen($temp) != $length) { + $block = crypt_random_string($length - strlen($temp)); + $block = str_replace("\x00", '', $block); + $temp.= $block; } $temp.= chr(0) . $m; diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index bdee4222..32ece7a5 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -80,11 +80,11 @@ if (!class_exists('Math_BigInteger')) { /** * Include Crypt_Random */ -// the class_exists() will only be called if the crypt_random function hasn't been defined and +// the class_exists() will only be called if the crypt_random_string function hasn't been defined and // will trigger a call to __autoload() if you're wanting to auto-load classes // call function_exists() a second time to stop the require_once from being called outside // of the auto loader -if (!function_exists('crypt_random') && !class_exists('Crypt_Random') && !function_exists('crypt_random')) { +if (!function_exists('crypt_random_string') && !class_exists('Crypt_Random') && !function_exists('crypt_random_string')) { require_once('Crypt/Random.php'); } @@ -967,10 +967,7 @@ class Net_SSH2 { $compression_algorithms_server_to_client = $compression_algorithms_client_to_server = implode(',', $compression_algorithms); } - $client_cookie = ''; - for ($i = 0; $i < 16; $i++) { - $client_cookie.= chr(crypt_random(0, 255)); - } + $client_cookie = crypt_random_string(16); $response = $kexinit_payload_server; $this->_string_shift($response, 1); // skip past the message number (it should be SSH_MSG_KEXINIT) @@ -1148,7 +1145,6 @@ class Net_SSH2 { $g = new Math_BigInteger(2); $x = new Math_BigInteger(); - $x->setRandomGenerator('crypt_random'); $x = $x->random(new Math_BigInteger(1), $q); $e = $g->modPow($x, $p); @@ -2400,11 +2396,7 @@ class Net_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; - - $padding = ''; - for ($i = 0; $i < $padding_length; $i++) { - $padding.= chr(crypt_random(0, 255)); - } + $padding = crypt_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);