diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -428,6 +428,7 @@ 'phutil_get_library_root' => 'moduleutils/moduleutils.php', 'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php', 'phutil_get_signal_name' => 'future/exec/execx.php', + 'phutil_hashes_are_identical' => 'utils/utils.php', 'phutil_implode_html' => 'markup/render.php', 'phutil_ini_decode' => 'utils/utils.php', 'phutil_is_hiphop_runtime' => 'utils/utils.php', diff --git a/src/utils/__tests__/PhutilUtilsTestCase.php b/src/utils/__tests__/PhutilUtilsTestCase.php --- a/src/utils/__tests__/PhutilUtilsTestCase.php +++ b/src/utils/__tests__/PhutilUtilsTestCase.php @@ -792,4 +792,30 @@ $this->assertTrue(($caught instanceof Exception)); } + public function testHashComparisons() { + $tests = array( + array('1', '12', false), + array('0', '0e123', false), + array('0e123', '0e124', false), + array('', '0', false), + array('000', '0e0', false), + array('001', '002', false), + array('0', '', false), + array('987654321', '123456789', false), + array('A', 'a', false), + array('123456789', '123456789', true), + array('hunter42', 'hunter42', true), + ); + + foreach ($tests as $key => $test) { + list($u, $v, $expect) = $test; + $actual = phutil_hashes_are_identical($u, $v); + $this->assertEqual( + $expect, + $actual, + pht('Test Case: "%s" vs "%s"', $u, $v)); + } + } + + } diff --git a/src/utils/utils.php b/src/utils/utils.php --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -1360,3 +1360,50 @@ $regex = '(\A'.$regex.'\z)'; return (bool)preg_match($regex, $path); } + + +/** + * Compare two hashes for equality. + * + * This function defuses two attacks: timing attacks and type juggling attacks. + * + * In a timing attack, the attacker observes that strings which match the + * secret take slightly longer to fail to match because more characters are + * compared. By testing a large number of strings, they can learn the secret + * character by character. This defuses timing attacks by always doing the + * same amount of work. + * + * In a type juggling attack, an attacker takes advantage of PHP's type rules + * where `"0" == "0e12345"` for any exponent. A portion of of hexadecimal + * hashes match this pattern and are vulnerable. This defuses this attack by + * performing bytewise character-by-character comparison. + * + * It is questionable how practical these attacks are, but they are possible + * in theory and defusing them is straightforward. + * + * @param string First hash. + * @param string Second hash. + * @return bool True if hashes are identical. + */ +function phutil_hashes_are_identical($u, $v) { + if (!is_string($u)) { + throw new Exception(pht('First hash argument must be a string.')); + } + + if (!is_string($v)) { + throw new Exception(pht('Second hash argument must be a string.')); + } + + if (strlen($u) !== strlen($v)) { + return false; + } + + $len = strlen($v); + + $bits = 0; + for ($ii = 0; $ii < $len; $ii++) { + $bits |= (ord($u[$ii]) ^ ord($v[$ii])); + } + + return ($bits === 0); +}