From a5621b8965ebcec213d3a5b07500cfcc3a730ada Mon Sep 17 00:00:00 2001 From: Andrey Andreev Date: Fri, 9 May 2014 11:23:08 +0300 Subject: Add hash_equals() to ext/hash compat layer Introduced in PHP 5.6 Beta 1 (unfortunately, still undocumented). RFC: https://wiki.php.net/rfc/timing_attack (Yes, I am aware that the RFC talks about hash_compare(), the function was later renamed in the implementation.) --- system/core/compat/hash.php | 46 ++++++++++++++++++++++ tests/codeigniter/core/compat/hash_test.php | 32 +++++++++++++-- user_guide_src/source/changelog.rst | 2 +- .../source/general/compatibility_functions.rst | 15 ++++++- 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/system/core/compat/hash.php b/system/core/compat/hash.php index a9f59f110..b2170103e 100644 --- a/system/core/compat/hash.php +++ b/system/core/compat/hash.php @@ -39,6 +39,52 @@ defined('BASEPATH') OR exit('No direct script access allowed'); // ------------------------------------------------------------------------ +if (is_php('5.6')) +{ + return; +} + +// ------------------------------------------------------------------------ + +if ( ! function_exists('hash_equals')) +{ + /** + * hash_equals() + * + * @link http://php.net/hash_equals + * @param string $known_string + * @param string $user_string + * @return bool + */ + function hash_equals($known_string, $user_string) + { + if ( ! is_string($known_string)) + { + trigger_error('hash_equals(): Expected known_string to be a string, '.strtolower(gettype($known_string)).' given', E_USER_WARNING); + return FALSE; + } + elseif ( ! is_string($user_string)) + { + trigger_error('hash_equals(): Expected user_string to be a string, '.strtolower(gettype($user_string)).' given', E_USER_WARNING); + return FALSE; + } + elseif (($length = strlen($known_string)) !== strlen($user_string)) + { + return FALSE; + } + + $diff = 0; + for ($i = 0; $i < $length; $i++) + { + $diff |= ord($known_string[$i]) ^ ord($user_string[$i]); + } + + return ($diff === 0); + } +} + +// ------------------------------------------------------------------------ + if (is_php('5.5')) { return; diff --git a/tests/codeigniter/core/compat/hash_test.php b/tests/codeigniter/core/compat/hash_test.php index 45a5b393e..d8cd0bb16 100644 --- a/tests/codeigniter/core/compat/hash_test.php +++ b/tests/codeigniter/core/compat/hash_test.php @@ -4,12 +4,33 @@ class hash_test extends CI_TestCase { public function test_bootstrap() { - if (is_php('5.5')) + if (is_php('5.6')) { - return $this->markTestSkipped('ext/hash is available on PHP 5.5'); + return $this->markTestSkipped('ext/hash is available on PHP 5.6'); } - $this->assertTrue(function_exists('hash_pbkdf2')); + $this->assertTrue(function_exists('hash_equals')); + is_php('5.5') OR $this->assertTrue(function_exists('hash_pbkdf2')); + } + + // ------------------------------------------------------------------------ + + /** + * hash_equals() test + * + * Borrowed from PHP's own tests + * + * @depends test_bootstrap + */ + public function test_hash_equals() + { + $this->assertTrue(hash_equals('same', 'same')); + $this->assertFalse(hash_equals('not1same', 'not2same')); + $this->assertFalse(hash_equals('short', 'longer')); + $this->assertFalse(hash_equals('longer', 'short')); + $this->assertFalse(hash_equals('', 'notempty')); + $this->assertFalse(hash_equals('notempty', '')); + $this->assertTrue(hash_equals('', '')); } // ------------------------------------------------------------------------ @@ -23,6 +44,11 @@ class hash_test extends CI_TestCase { */ public function test_hash_pbkdf2() { + if (is_php('5.5')) + { + return $this->markTestSkipped('hash_pbkdf2() is available on PHP 5.5'); + } + $this->assertEquals('0c60c80f961f0e71f3a9', hash_pbkdf2('sha1', 'password', 'salt', 1, 20)); $this->assertEquals( "\x0c\x60\xc8\x0f\x96\x1f\x0e\x71\xf3\xa9\xb5\x24\xaf\x60\x12\x06\x2f\xe0\x37\xa6", diff --git a/user_guide_src/source/changelog.rst b/user_guide_src/source/changelog.rst index edf0a497b..db8f7d277 100644 --- a/user_guide_src/source/changelog.rst +++ b/user_guide_src/source/changelog.rst @@ -525,7 +525,7 @@ Release Date: Not Released - Added `compatibility layers ` for: - `Multibyte String `_ (limited support). - - `Hash `_ (just ``hash_pbkdf2()``). + - `Hash `_ (``hash_equals()``, ``hash_pbkdf2()``). - `Password Hashing `_. - `Array Functions `_ (``array_column()``, ``array_replace()``, ``array_replace_recursive()``). diff --git a/user_guide_src/source/general/compatibility_functions.rst b/user_guide_src/source/general/compatibility_functions.rst index 398403eda..e685073a1 100644 --- a/user_guide_src/source/general/compatibility_functions.rst +++ b/user_guide_src/source/general/compatibility_functions.rst @@ -97,8 +97,9 @@ Function reference Hash (Message Digest) ********************* -This compatibility layer contains only a single function at -this time - ``hash_pbkdf2()``, which otherwise requires PHP 5.5. +This compatibility layer contains backports for the ``hash_equals()`` +and ``hash_pbkdf2()`` functions, which otherwise require PHP 5.6 and/or +PHP 5.5 respectively. Dependancies ============ @@ -108,6 +109,16 @@ Dependancies Function reference ================== +.. function:: hash_equals($known_string, $user_string) + + :param string $known_string: Known string + :param string $user_string: User-supplied string + :returns: TRUE if the strings match, FALSE otherwise + :rtype: string + + For more information, please refer to the `PHP manual for + hash_equals() `_. + .. function:: hash_pbkdf2($algo, $password, $salt, $iterations[, $length = 0[, $raw_output = FALSE]]) :param string $algo: Hashing algorithm -- cgit v1.2.3-24-g4f1b