public function testHmac() { if (MWCryptHash::hashAlgo() !== 'whirlpool') { $this->markTestSkipped('Hash algorithm isn\'t whirlpool'); } $data = 'foobar'; $key = 'secret'; $hash = 'ddc94177b2020e55ce2049199fd9cc6327f416ff6dc621cc34cb43d9bec61d73372b4790c0e24957f565ecaf2d42821e6303619093e99cbe14a3b9250bda5f81'; $this->assertEquals(pack('H*', $hash), MWCryptHash::hmac($data, $key), 'Raw hmac'); $this->assertEquals($hash, MWCryptHash::hmac($data, $key, false), 'Hex hmac'); }
/** * Decide on the best acceptable hash algorithm we have available for hash() * @return string A hash algorithm */ public static function hashAlgo() { if (!is_null(self::$algo)) { return self::$algo; } $algos = hash_algos(); $preference = ['whirlpool', 'sha256', 'sha1', 'md5']; foreach ($preference as $algorithm) { if (in_array($algorithm, $algos)) { self::$algo = $algorithm; return self::$algo; } } // We only reach here if no acceptable hash is found in the list, this should // be a technical impossibility since most of php's hash list is fixed and // some of the ones we list are available as their own native functions // But since we already require at least 5.2 and hash() was default in // 5.1.2 we don't bother falling back to methods like sha1 and md5. throw new DomainException("Could not find an acceptable hashing function in hash_algos()"); }
/** * Hash data as a session ID * * Generally this will only be used when self::persistsSessionId() is false and * the provider has to base the session ID on the verified user's identity * or other static data. * * @param string $data * @param string|null $key Defaults to $this->config->get( 'SecretKey' ) * @return string */ protected final function hashToSessionId($data, $key = null) { if (!is_string($data)) { throw new \InvalidArgumentException('$data must be a string, ' . gettype($data) . ' was passed'); } if ($key !== null && !is_string($key)) { throw new \InvalidArgumentException('$key must be a string or null, ' . gettype($key) . ' was passed'); } $hash = \MWCryptHash::hmac("{$this}\n{$data}", $key ?: $this->config->get('SecretKey'), false); if (strlen($hash) < 32) { // Should never happen, even md5 is 128 bits // @codeCoverageIgnoreStart throw new \UnexpectedValueException('Hash fuction returned less than 128 bits'); // @codeCoverageIgnoreEnd } if (strlen($hash) >= 40) { $hash = wfBaseConvert($hash, 16, 32, 32); } return substr($hash, -32); }
/** * @see self::generate() */ public function realGenerate($bytes, $forceStrong = false) { wfDebug(__METHOD__ . ": Generating cryptographic random bytes for " . wfGetAllCallers(5) . "\n"); $bytes = floor($bytes); static $buffer = ''; if (is_null($this->strong)) { // Set strength to false initially until we know what source data is coming from $this->strong = true; } if (strlen($buffer) < $bytes) { // If available make use of mcrypt_create_iv URANDOM source to generate randomness // On unix-like systems this reads from /dev/urandom but does it without any buffering // and bypasses openbasedir restrictions, so it's preferable to reading directly // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate // entropy so this is also preferable to just trying to read urandom because it may work // on Windows systems as well. if (function_exists('mcrypt_create_iv')) { $rem = $bytes - strlen($buffer); $iv = mcrypt_create_iv($rem, MCRYPT_DEV_URANDOM); if ($iv === false) { wfDebug(__METHOD__ . ": mcrypt_create_iv returned false.\n"); } else { $buffer .= $iv; wfDebug(__METHOD__ . ": mcrypt_create_iv generated " . strlen($iv) . " bytes of randomness.\n"); } } } if (strlen($buffer) < $bytes) { // If available make use of openssl's random_pseudo_bytes method to // attempt to generate randomness. However don't do this on Windows // with PHP < 5.3.4 due to a bug: // http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php // http://git.php.net/?p=php-src.git;a=commitdiff;h=cd62a70863c261b07f6dadedad9464f7e213cad5 if (function_exists('openssl_random_pseudo_bytes') && (!wfIsWindows() || version_compare(PHP_VERSION, '5.3.4', '>='))) { $rem = $bytes - strlen($buffer); $openssl_bytes = openssl_random_pseudo_bytes($rem, $openssl_strong); if ($openssl_bytes === false) { wfDebug(__METHOD__ . ": openssl_random_pseudo_bytes returned false.\n"); } else { $buffer .= $openssl_bytes; wfDebug(__METHOD__ . ": openssl_random_pseudo_bytes generated " . strlen($openssl_bytes) . " bytes of " . ($openssl_strong ? "strong" : "weak") . " randomness.\n"); } if (strlen($buffer) >= $bytes) { // openssl tells us if the random source was strong, if some of our data was generated // using it use it's say on whether the randomness is strong $this->strong = !!$openssl_strong; } } } // Only read from urandom if we can control the buffer size or were passed forceStrong if (strlen($buffer) < $bytes && (function_exists('stream_set_read_buffer') || $forceStrong)) { $rem = $bytes - strlen($buffer); if (!function_exists('stream_set_read_buffer') && $forceStrong) { wfDebug(__METHOD__ . ": Was forced to read from /dev/urandom " . "without control over the buffer size.\n"); } // /dev/urandom is generally considered the best possible commonly // available random source, and is available on most *nix systems. MediaWiki\suppressWarnings(); $urandom = fopen("/dev/urandom", "rb"); MediaWiki\restoreWarnings(); // Attempt to read all our random data from urandom // php's fread always does buffered reads based on the stream's chunk_size // so in reality it will usually read more than the amount of data we're // asked for and not storing that risks depleting the system's random pool. // If stream_set_read_buffer is available set the chunk_size to the amount // of data we need. Otherwise read 8k, php's default chunk_size. if ($urandom) { // php's default chunk_size is 8k $chunk_size = 1024 * 8; if (function_exists('stream_set_read_buffer')) { // If possible set the chunk_size to the amount of data we need stream_set_read_buffer($urandom, $rem); $chunk_size = $rem; } $random_bytes = fread($urandom, max($chunk_size, $rem)); $buffer .= $random_bytes; fclose($urandom); wfDebug(__METHOD__ . ": /dev/urandom generated " . strlen($random_bytes) . " bytes of randomness.\n"); if (strlen($buffer) >= $bytes) { // urandom is always strong, set to true if all our data was generated using it $this->strong = true; } } else { wfDebug(__METHOD__ . ": /dev/urandom could not be opened.\n"); } } // If we cannot use or generate enough data from a secure source // use this loop to generate a good set of pseudo random data. // This works by initializing a random state using a pile of unstable data // and continually shoving it through a hash along with a variable salt. // We hash the random state with more salt to avoid the state from leaking // out and being used to predict the /randomness/ that follows. if (strlen($buffer) < $bytes) { wfDebug(__METHOD__ . ": Falling back to using a pseudo random state to generate randomness.\n"); } while (strlen($buffer) < $bytes) { $buffer .= MWCryptHash::hmac($this->randomState(), mt_rand()); // This code is never really cryptographically strong, if we use it // at all, then set strong to false. $this->strong = false; } // Once the buffer has been filled up with enough random data to fulfill // the request shift off enough data to handle the request and leave the // unused portion left inside the buffer for the next request for random data $generated = substr($buffer, 0, $bytes); $buffer = substr($buffer, $bytes); wfDebug(__METHOD__ . ": " . strlen($buffer) . " bytes of randomness leftover in the buffer.\n"); return $generated; }
/** * Get the user's current token. * @param bool $forceCreation Force the generation of a new token if the * user doesn't have one (default=true for backwards compatibility). * @return string|null Token */ public function getToken($forceCreation = true) { global $wgAuthenticationTokenVersion; $this->load(); if (!$this->mToken && $forceCreation) { $this->setToken(); } if (!$this->mToken) { // The user doesn't have a token, return null to indicate that. return null; } elseif ($this->mToken === self::INVALID_TOKEN) { // We return a random value here so existing token checks are very // likely to fail. return MWCryptRand::generateHex(self::TOKEN_LENGTH); } elseif ($wgAuthenticationTokenVersion === null) { // $wgAuthenticationTokenVersion not in use, so return the raw secret return $this->mToken; } else { // $wgAuthenticationTokenVersion in use, so hmac it. $ret = MWCryptHash::hmac($wgAuthenticationTokenVersion, $this->mToken, false); // The raw hash can be overly long. Shorten it up. $len = max(32, self::TOKEN_LENGTH); if (strlen($ret) < $len) { // Should never happen, even md5 is 128 bits throw new \UnexpectedValueException('Hmac returned less than 128 bits'); } return substr($ret, -$len); } }