From 77d62edef3d3ca020d792919bf0924960445f5be Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Tue, 26 Aug 2014 13:09:54 -0400 Subject: [PATCH] Refactor hashing utility functions from MWCryptRand and make public MWCryptRand already has some useful utility functions wrapping PHP's hash() and hash_hmac(). Let's make them public so we can use them from other code. But since "MWCryptRand" isn't really a good place for hashing functions, let's move them to "MWCryptHash" instead. Change-Id: I7542c719ac72beba7b0f6aa170bdb4c69fa6beab --- autoload.php | 1 + includes/utils/MWCryptHash.php | 111 ++++++++++++++++++ includes/utils/MWCryptRand.php | 85 +------------- .../includes/utils/MWCryptHashTest.php | 59 ++++++++++ 4 files changed, 175 insertions(+), 81 deletions(-) create mode 100644 includes/utils/MWCryptHash.php create mode 100644 tests/phpunit/includes/utils/MWCryptHashTest.php diff --git a/autoload.php b/autoload.php index 3cd9bad3c9..9c859b7d18 100644 --- a/autoload.php +++ b/autoload.php @@ -710,6 +710,7 @@ $wgAutoloadLocalClasses = array( 'MWCallableUpdate' => __DIR__ . '/includes/deferred/CallableUpdate.php', 'MWContentSerializationException' => __DIR__ . '/includes/content/ContentHandler.php', 'MWCryptHKDF' => __DIR__ . '/includes/utils/MWCryptHKDF.php', + 'MWCryptHash' => __DIR__ . '/includes/utils/MWCryptHash.php', 'MWCryptRand' => __DIR__ . '/includes/utils/MWCryptRand.php', 'MWDebug' => __DIR__ . '/includes/debug/MWDebug.php', 'MWDocGen' => __DIR__ . '/maintenance/mwdocgen.php', diff --git a/includes/utils/MWCryptHash.php b/includes/utils/MWCryptHash.php new file mode 100644 index 0000000000..b46de60234 --- /dev/null +++ b/includes/utils/MWCryptHash.php @@ -0,0 +1,111 @@ + null, + false => null, + ); + + /** + * 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 = array( 'whirlpool', 'sha256', 'sha1', 'md5' ); + + foreach ( $preference as $algorithm ) { + if ( in_array( $algorithm, $algos ) ) { + self::$algo = $algorithm; + wfDebug( __METHOD__ . ': Using the ' . self::$algo . " hash algorithm.\n" ); + + 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()" ); + } + + /** + * Return the byte-length output of the hash algorithm we are + * using in self::hash and self::hmac. + * + * @param boolean $raw True to return the length for binary data, false to + * return for hex-encoded + * @return int Number of bytes the hash outputs + */ + public static function hashLength( $raw = true ) { + $raw = (bool)$raw; + if ( is_null( self::$hashLength[$raw] ) ) { + self::$hashLength[$raw] = strlen( self::hash( '', $raw ) ); + } + + return self::$hashLength[$raw]; + } + + /** + * Generate an acceptably unstable one-way-hash of some text + * making use of the best hash algorithm that we have available. + * + * @param string $data + * @param boolean $raw True to return binary data, false to return it hex-encoded + * @return string A hash of the data + */ + public static function hash( $data, $raw = true ) { + return hash( self::hashAlgo(), $data, $raw ); + } + + /** + * Generate an acceptably unstable one-way-hmac of some text + * making use of the best hash algorithm that we have available. + * + * @param string $data + * @param string $key + * @param boolean $raw True to return binary data, false to return it hex-encoded + * @return string An hmac hash of the data + key + */ + public static function hmac( $data, $key, $raw = true ) { + return hash_hmac( self::hashAlgo(), $data, $key, $raw ); + } + +} diff --git a/includes/utils/MWCryptRand.php b/includes/utils/MWCryptRand.php index f2237909e1..53c77c22cc 100644 --- a/includes/utils/MWCryptRand.php +++ b/includes/utils/MWCryptRand.php @@ -43,16 +43,6 @@ class MWCryptRand { */ protected static $singleton = null; - /** - * The hash algorithm being used - */ - protected $algo = null; - - /** - * The number of bytes outputted by the hash algorithm - */ - protected $hashLength = null; - /** * A boolean indicating whether the previous random generation was done using * cryptographically strong random number generator or not. @@ -156,7 +146,7 @@ class MWCryptRand { // loop to gather little entropy) $minIterations = self::MIN_ITERATIONS; // Duration of time to spend doing calculations (in seconds) - $duration = ( self::MSEC_PER_BYTE / 1000 ) * $this->hashLength(); + $duration = ( self::MSEC_PER_BYTE / 1000 ) * MWCryptHash::hashLength(); // Create a buffer to use to trigger memory operations $bufLength = 10000000; $buffer = str_repeat( ' ', $bufLength ); @@ -183,7 +173,7 @@ class MWCryptRand { $iterations++; } $timeTaken = $currentTime - $startTime; - $data = $this->hash( $data ); + $data = MWCryptHash::hash( $data ); wfDebug( __METHOD__ . ": Clock drift calculation " . "(time-taken=" . ( $timeTaken * 1000 ) . "ms, " . @@ -203,7 +193,7 @@ class MWCryptRand { // Initialize the state with whatever unstable data we can find // It's important that this data is hashed right afterwards to prevent // it from being leaked into the output stream - $state = $this->hash( $this->initialRandomState() ); + $state = MWCryptHash::hash( $this->initialRandomState() ); } // Generate a new random state based on the initial random state or previous // random state by combining it with clock drift @@ -212,73 +202,6 @@ class MWCryptRand { return $state; } - /** - * Decide on the best acceptable hash algorithm we have available for hash() - * @throws MWException - * @return string A hash algorithm - */ - protected function hashAlgo() { - if ( !is_null( $this->algo ) ) { - return $this->algo; - } - - $algos = hash_algos(); - $preference = array( 'whirlpool', 'sha256', 'sha1', 'md5' ); - - foreach ( $preference as $algorithm ) { - if ( in_array( $algorithm, $algos ) ) { - $this->algo = $algorithm; - wfDebug( __METHOD__ . ": Using the {$this->algo} hash algorithm.\n" ); - - return $this->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 MWException( "Could not find an acceptable hashing function in hash_algos()" ); - } - - /** - * Return the byte-length output of the hash algorithm we are - * using in self::hash and self::hmac. - * - * @return int Number of bytes the hash outputs - */ - protected function hashLength() { - if ( is_null( $this->hashLength ) ) { - $this->hashLength = strlen( $this->hash( '' ) ); - } - - return $this->hashLength; - } - - /** - * Generate an acceptably unstable one-way-hash of some text - * making use of the best hash algorithm that we have available. - * - * @param string $data - * @return string A raw hash of the data - */ - protected function hash( $data ) { - return hash( $this->hashAlgo(), $data, true ); - } - - /** - * Generate an acceptably unstable one-way-hmac of some text - * making use of the best hash algorithm that we have available. - * - * @param string $data - * @param string $key - * @return string A raw hash of the data - */ - protected function hmac( $data, $key ) { - return hash_hmac( $this->hashAlgo(), $data, $key, true ); - } - /** * @see self::wasStrong() */ @@ -407,7 +330,7 @@ class MWCryptRand { ": Falling back to using a pseudo random state to generate randomness.\n" ); } while ( strlen( $buffer ) < $bytes ) { - $buffer .= $this->hmac( $this->randomState(), mt_rand() ); + $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; diff --git a/tests/phpunit/includes/utils/MWCryptHashTest.php b/tests/phpunit/includes/utils/MWCryptHashTest.php new file mode 100644 index 0000000000..406f42142c --- /dev/null +++ b/tests/phpunit/includes/utils/MWCryptHashTest.php @@ -0,0 +1,59 @@ +markTestSkipped( 'Hash algorithm isn\'t whirlpool' ); + } + + $this->assertEquals( 64, MWCryptHash::hashLength(), 'Raw hash length' ); + $this->assertEquals( 128, MWCryptHash::hashLength( false ), 'Hex hash length' ); + } + + public function testHash() { + if ( MWCryptHash::hashAlgo() !== 'whirlpool' ) { + $this->markTestSkipped( 'Hash algorithm isn\'t whirlpool' ); + } + + $data = 'foobar'; + $hash = '9923afaec3a86f865bb231a588f453f84e8151a2deb4109aebc6de4284be5bebcff4fab82a7e51d920237340a043736e9d13bab196006dcca0fe65314d68eab9'; + + $this->assertEquals( + pack( 'H*', $hash ), + MWCryptHash::hash( $data ), + 'Raw hash' + ); + $this->assertEquals( + $hash, + MWCryptHash::hash( $data, false ), + 'Hex hash' + ); + } + + 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' + ); + } + +} -- 2.20.1