16, 'sha1' => 20, 'sha224' => 28, 'sha256' => 32, 'sha384' => 48, 'sha512' => 64, 'ripemd128' => 16, 'ripemd160' => 20, 'ripemd256' => 32, 'ripemd320' => 40, 'whirlpool' => 64, ); /** * @param string $secretKeyMaterial * @param string $algorithm Name of hashing algorithm * @param BagOStuff $cache * @param string|array $context Context to mix into HKDF context */ public function __construct( $secretKeyMaterial, $algorithm, $cache, $context ) { if ( strlen( $secretKeyMaterial ) < 16 ) { throw new MWException( "MWCryptHKDF secret was too short." ); } $this->skm = $secretKeyMaterial; $this->algorithm = $algorithm; $this->cache = $cache; $this->salt = ''; // Initialize a blank salt, see getSaltUsingCache() $this->prk = ''; $this->context = is_array( $context ) ? $context : array( $context ); // To prevent every call from hitting the same memcache server, pick // from a set of keys to use. mt_rand is only use to pick a random // server, and does not affect the security of the process. $this->cacheKey = wfMemcKey( 'HKDF', mt_rand( 0, 16 ) ); } /** * Save the last block generated, so the next user will compute a different PRK * from the same SKM. This should keep things unpredictable even if an attacker * is able to influence CTXinfo. */ function __destruct() { if ( $this->lastK ) { $this->cache->set( $this->cacheKey, $this->lastK ); } } /** * MW specific salt, cached from last run * @return string Binary string */ protected function getSaltUsingCache() { if ( $this->salt == '' ) { $lastSalt = $this->cache->get( $this->cacheKey ); if ( $lastSalt === false ) { // If we don't have a previous value to use as our salt, we use // 16 bytes from MWCryptRand, which will use a small amount of // entropy from our pool. Note, "XTR may be deterministic or keyed // via an optional “salt value” (i.e., a non-secret random // value)..." - http://eprint.iacr.org/2010/264.pdf. However, we // use a strongly random value since we can. $lastSalt = MWCryptRand::generate( 16 ); } // Get a binary string that is hashLen long $this->salt = hash( $this->algorithm, $lastSalt, true ); } return $this->salt; } /** * Return a singleton instance, based on the global configs. * @return HKDF */ protected static function singleton() { global $wgHKDFAlgorithm, $wgHKDFSecret, $wgSecretKey; $secret = $wgHKDFSecret ?: $wgSecretKey; if ( !$secret ) { throw new MWException( "Cannot use MWCryptHKDF without a secret." ); } // In HKDF, the context can be known to the attacker, but this will // keep simultaneous runs from producing the same output. $context = array(); $context[] = microtime(); $context[] = getmypid(); $context[] = gethostname(); // Setup salt cache. Use APC, or fallback to the main cache if it isn't setup try { $cache = ObjectCache::newAccelerator( array() ); } catch ( Exception $e ) { $cache = wfGetMainCache(); } if ( is_null( self::$singleton ) ) { self::$singleton = new self( $secret, $wgHKDFAlgorithm, $cache, $context ); } return self::$singleton; } /** * Produce $bytes of secure random data. As a side-effect, * $this->lastK is set to the last hashLen block of key material. * @param int $bytes Number of bytes of data * @param string $context Context to mix into CTXinfo * @return string Binary string of length $bytes */ protected function realGenerate( $bytes, $context = '' ) { if ( $this->prk === '' ) { $salt = $this->getSaltUsingCache(); $this->prk = self::HKDFExtract( $this->algorithm, $salt, $this->skm ); } $CTXinfo = implode( ':', array_merge( $this->context, array( $context ) ) ); return self::HKDFExpand( $this->algorithm, $this->prk, $CTXinfo, $bytes, $this->lastK ); } /** * RFC5869 defines HKDF in 2 steps, extraction and expansion. * From http://eprint.iacr.org/2010/264.pdf: * * The scheme HKDF is specifed as: * HKDF(XTS, SKM, CTXinfo, L) = K(1) || K(2) || ... || K(t) * where the values K(i) are defined as follows: * PRK = HMAC(XTS, SKM) * K(1) = HMAC(PRK, CTXinfo || 0); * K(i+1) = HMAC(PRK, K(i) || CTXinfo || i), 1 <= i < t; * where t = [L/k] and the value K(t) is truncated to its first d = L mod k bits; * the counter i is non-wrapping and of a given fixed size, e.g., a single byte. * Note that the length of the HMAC output is the same as its key length and therefore * the scheme is well defined. * * XTS is the "extractor salt" * SKM is the "secret keying material" * * N.B. http://eprint.iacr.org/2010/264.pdf seems to differ from RFC 5869 in that the test * vectors from RFC 5869 only work if K(0) = '' and K(1) = HMAC(PRK, K(0) || CTXinfo || 1) * * @param string $hash The hashing function to use (e.g., sha256) * @param string $ikm The input keying material * @param string $salt The salt to add to the ikm, to get the prk * @param string $info Optional context (change the output without affecting * the randomness properties of the output) * @param int $L Number of bytes to return * @return string Cryptographically secure pseudorandom binary string */ public static function HKDF( $hash, $ikm, $salt, $info, $L ) { $prk = self::HKDFExtract( $hash, $salt, $ikm ); $okm = self::HKDFExpand( $hash, $prk, $info, $L ); return $okm; } /** * Extract the PRK, PRK = HMAC(XTS, SKM) * Note that the hmac is keyed with XTS (the salt), * and the SKM (source key material) is the "data". * * @param string $hash The hashing function to use (e.g., sha256) * @param string $salt The salt to add to the ikm, to get the prk * @param string $ikm The input keying material * @return string Binary string (pseudorandm key) used as input to HKDFExpand */ private static function HKDFExtract( $hash, $salt, $ikm ) { return hash_hmac( $hash, $ikm, $salt, true ); } /** * Expand the key with the given context * * @param string $hash Hashing Algorithm * @param string $prk A pseudorandom key of at least HashLen octets * (usually, the output from the extract step) * @param string $info Optional context and application specific information * (can be a zero-length string) * @param int $bytes Length of output keying material in bytes * (<= 255*HashLen) * @param string &$lastK Set by this function to the last block of the expansion. * In MediaWiki, this is used to seed future Extractions. * @return string Cryptographically secure random string $bytes long */ private static function HKDFExpand( $hash, $prk, $info, $bytes, &$lastK = '' ) { $hashLen = MWCryptHKDF::$hashLength[$hash]; $rounds = ceil( $bytes / $hashLen ); $output = ''; if ( $bytes > 255 * $hashLen ) { throw new MWException( "Too many bytes requested from HDKFExpand" ); } // K(1) = HMAC(PRK, CTXinfo || 1); // K(i) = HMAC(PRK, K(i-1) || CTXinfo || i); 1 < i <= t; for ( $counter = 1; $counter <= $rounds; ++$counter ) { $lastK = hash_hmac( $hash, $lastK . $info . chr( $counter ), $prk, true ); $output .= $lastK; } return substr( $output, 0, $bytes ); } /** * Generate cryptographically random data and return it in raw binary form. * * @param int $bytes The number of bytes of random data to generate * @param string $context String to mix into HMAC context * @return string Binary string of length $bytes */ public static function generate( $bytes, $context ) { return self::singleton()->realGenerate( $bytes, $context ); } /** * Generate cryptographically random data and return it in hexadecimal string format. * See MWCryptRand::realGenerateHex for details of the char-to-byte conversion logic. * * @param int $chars The number of hex chars of random data to generate * @param string $context String to mix into HMAC context * @return string Random hex characters, $chars long */ public static function generateHex( $chars, $context = '' ) { $bytes = ceil( $chars / 2 ); $hex = bin2hex( self::singleton()->realGenerate( $bytes, $context ) ); return substr( $hex, 0, $chars ); } }