From f4dc7e6b214e1fa7a31bb99bbd5b13961acfff38 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Sun, 13 Mar 2016 23:23:48 +0900 Subject: [PATCH] Move all functionality of old Password class to new Password class --- classes/security/Password.class.php | 320 ++------------------------ common/framework/password.php | 55 ++++- tests/unit/framework/PasswordTest.php | 14 +- 3 files changed, 73 insertions(+), 316 deletions(-) diff --git a/classes/security/Password.class.php b/classes/security/Password.class.php index fa39fd906..c0a1da502 100644 --- a/classes/security/Password.class.php +++ b/classes/security/Password.class.php @@ -1,366 +1,76 @@ */ -/** - * This class can be used to hash passwords using various algorithms and check their validity. - * It is fully compatible with previous defaults, while also supporting bcrypt and pbkdf2. - * - * @file Password.class.php - * @author Kijin Sung (kijin@kijinsung.com) - * @package /classes/security - * @version 1.1 - */ class Password { - /** - * @brief Custom algorithms are stored here - * @var array - */ - protected static $_custom = array(); - - /** - * @brief Register a custom algorithm for password checking - * @param string $name The name of the algorithm - * @param string $regexp The regular expression to detect the algorithm - * @param callable $callback The function to call to regenerate the hash - * @return void - */ public static function registerCustomAlgorithm($name, $regexp, $callback) { - self::$_custom[$name] = array('regexp' => $regexp, 'callback' => $callback); + Rhymix\Framework\Password::addAlgorithm($name, $regexp, $callback); } - /** - * @brief Return the list of hashing algorithms supported by this server - * @return array - */ public function getSupportedAlgorithms() { - $retval = array(); - if(function_exists('hash_hmac') && in_array('sha256', hash_algos())) - { - $retval['pbkdf2'] = 'pbkdf2'; - } - if(version_compare(PHP_VERSION, '5.3.7', '>=') && defined('CRYPT_BLOWFISH')) - { - $retval['bcrypt'] = 'bcrypt'; - } - $retval['md5'] = 'md5'; - return $retval; + return Rhymix\Framework\Password::getSupportedAlgorithms(); } - /** - * @brief Return the best hashing algorithm supported by this server - * @return string - */ public function getBestAlgorithm() { - $algos = $this->getSupportedAlgorithms(); - return key($algos); + return Rhymix\Framework\Password::getBestSupportedAlgorithm(); } - /** - * @brief Return the currently selected hashing algorithm - * @return string - */ public function getCurrentlySelectedAlgorithm() { - if(function_exists('getModel')) - { - $config = getModel('member')->getMemberConfig(); - $algorithm = $config->password_hashing_algorithm; - if(strval($algorithm) === '') - { - $algorithm = 'md5'; // Historical default for XE - } - } - else - { - $algorithm = 'md5'; - } - return $algorithm; + return Rhymix\Framework\Password::getDefaultAlgorithm(); } - /** - * @brief Return the currently configured work factor for bcrypt and other adjustable algorithms - * @return int - */ public function getWorkFactor() { - if(function_exists('getModel')) - { - $config = getModel('member')->getMemberConfig(); - $work_factor = $config->password_hashing_work_factor; - if(!$work_factor || $work_factor < 4 || $work_factor > 31) - { - $work_factor = 8; // Reasonable default - } - } - else - { - $work_factor = 8; - } - return $work_factor; + return Rhymix\Framework\Password::getWorkFactor(); } - /** - * @brief Create a hash using the specified algorithm - * @param string $password The password - * @param string $algorithm The algorithm (optional) - * @return string - */ public function createHash($password, $algorithm = null) { - if($algorithm === null) - { - $algorithm = $this->getCurrentlySelectedAlgorithm(); - } - if(!array_key_exists($algorithm, $this->getSupportedAlgorithms())) - { - return false; - } - - $password = trim($password); - - switch($algorithm) - { - case 'md5': - return md5($password); - - case 'pbkdf2': - $iterations = pow(2, $this->getWorkFactor() + 5); - $salt = $this->createSecureSalt(12, 'alnum'); - $hash = base64_encode($this->pbkdf2($password, $salt, 'sha256', $iterations, 24)); - return 'sha256:'.sprintf('%07d', $iterations).':'.$salt.':'.$hash; - - case 'bcrypt': - return $this->bcrypt($password); - - default: - return false; - } + return Rhymix\Framework\Password::hashPassword($password, $algorithm); } - /** - * @brief Check if a password matches a hash - * @param string $password The password - * @param string $hash The hash - * @param string $algorithm The algorithm (optional) - * @return bool - */ public function checkPassword($password, $hash, $algorithm = null) { - if($algorithm === null) - { - $algorithm = $this->checkAlgorithm($hash); - } - - $password = trim($password); - - switch($algorithm) - { - case 'md5': - return md5($password) === $hash || md5(sha1(md5($password))) === $hash; - - case 'mysql_old_password': - return (class_exists('Context') && substr(Context::getDBType(), 0, 5) === 'mysql') ? - DB::getInstance()->isValidOldPassword($password, $hash) : false; - - case 'mysql_password': - return $hash[0] === '*' && substr($hash, 1) === strtoupper(sha1(sha1($password, true))); - - case 'pbkdf2': - $hash = explode(':', $hash); - $hash[3] = base64_decode($hash[3]); - $hash_to_compare = $this->pbkdf2($password, $hash[2], $hash[0], intval($hash[1], 10), strlen($hash[3])); - return Rhymix\Framework\Security::compareStrings($hash_to_compare, $hash[3]); - - case 'bcrypt': - $hash_to_compare = $this->bcrypt($password, $hash); - return Rhymix\Framework\Security::compareStrings($hash_to_compare, $hash); - - default: - if($algorithm && isset(self::$_custom[$algorithm])) - { - $hash_callback = self::$_custom[$algorithm]['callback']; - $hash_to_compare = $hash_callback($password, $hash); - return Rhymix\Framework\Security::compareStrings($hash_to_compare, $hash); - } - if(in_array($algorithm, hash_algos())) - { - return Rhymix\Framework\Security::compareStrings(hash($algorithm, $password), $hash); - } - return false; - } + return Rhymix\Framework\Password::checkPassword($password, $hash, $algorithm); } - /** - * @brief Check the algorithm used to create a hash - * @param string $hash The hash - * @return string - */ function checkAlgorithm($hash) { - foreach(self::$_custom as $name => $definition) - { - if(preg_match($definition['regexp'], $hash)) - { - return $name; - } - } - - if(preg_match('/^\$2[axy]\$([0-9]{2})\$/', $hash, $matches)) - { - return 'bcrypt'; - } - elseif(preg_match('/^sha[0-9]+:([0-9]+):/', $hash, $matches)) - { - return 'pbkdf2'; - } - elseif(strlen($hash) === 32 && ctype_xdigit($hash)) - { - return 'md5'; - } - elseif(strlen($hash) === 40 && ctype_xdigit($hash)) - { - return 'sha1'; - } - elseif(strlen($hash) === 64 && ctype_xdigit($hash)) - { - return 'sha256'; - } - elseif(strlen($hash) === 96 && ctype_xdigit($hash)) - { - return 'sha384'; - } - elseif(strlen($hash) === 128 && ctype_xdigit($hash)) - { - return 'sha512'; - } - elseif(strlen($hash) === 16 && ctype_xdigit($hash)) - { - return 'mysql_old_password'; - } - elseif(strlen($hash) === 41 && $hash[0] === '*') - { - return 'mysql_password'; - } - else - { - return false; - } + $algos = Rhymix\Framework\Password::checkAlgorithm($hash); + return count($algos) ? $algos[0] : false; } - /** - * @brief Check the work factor of a hash - * @param string $hash The hash - * @return int - */ function checkWorkFactor($hash) { - if(preg_match('/^\$2[axy]\$([0-9]{2})\$/', $hash, $matches)) - { - return intval($matches[1], 10); - } - elseif(preg_match('/^sha[0-9]+:([0-9]+):/', $hash, $matches)) - { - return max(0, round(log($matches[1], 2)) - 5); - } - else - { - return false; - } + return Rhymix\Framework\Password::checkWorkFactor($hash); } - /** - * @brief Generate a cryptographically secure random string to use as a salt - * @param int $length The number of bytes to return - * @param string $format hex or alnum - * @return string - */ public function createSecureSalt($length, $format = 'hex') { return Rhymix\Framework\Security::getRandom($length, $format); } - /** - * @brief Generate a temporary password using the secure salt generator - * @param int $length The number of bytes to return - * @return string - */ public function createTemporaryPassword($length = 16) { - while(true) - { - $source = base64_encode($this->createSecureSalt(64, 'binary')); - $source = strtr($source, 'iIoOjl10/', '@#$%&*-!?'); - $source_length = strlen($source); - for($i = 0; $i < $source_length - $length; $i++) - { - $candidate = substr($source, $i, $length); - if(preg_match('/[a-z]/', $candidate) && preg_match('/[A-Z]/', $candidate) && - preg_match('/[0-9]/', $candidate) && preg_match('/[^a-zA-Z0-9]/', $candidate)) - { - return $candidate; - } - } - } + return Rhymix\Framework\Password::getRandomPassword($length); } - /** - * @brief Generate the PBKDF2 hash of a string using a salt - * @param string $password The password - * @param string $salt The salt - * @param string $algorithm The algorithm (optional, default is sha256) - * @param int $iterations Iteration count (optional, default is 8192) - * @param int $length The length of the hash (optional, default is 32) - * @return string - */ public function pbkdf2($password, $salt, $algorithm = 'sha256', $iterations = 8192, $length = 24) { - if(function_exists('hash_pbkdf2')) - { - return hash_pbkdf2($algorithm, $password, $salt, $iterations, $length, true); - } - else - { - $output = ''; - $block_count = ceil($length / strlen(hash($algorithm, '', true))); // key length divided by the length of one hash - for($i = 1; $i <= $block_count; $i++) - { - $last = $salt . pack('N', $i); // $i encoded as 4 bytes, big endian - $last = $xorsum = hash_hmac($algorithm, $last, $password, true); // first iteration - for($j = 1; $j < $iterations; $j++) // The other $count - 1 iterations - { - $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true)); - } - $output .= $xorsum; - } - return substr($output, 0, $length); - } + $hash = Rhymix\Framework\Security::pbkdf2($password, $salt, $algorithm, $iterations, $length); + $hash = explode(':', $hash); + return base64_decode($hash[3]); } - /** - * @brief Generate the bcrypt hash of a string using a salt - * @param string $password The password - * @param string $salt The salt (optional, auto-generated if empty) - * @return string - */ public function bcrypt($password, $salt = null) { - if($salt === null) - { - $salt = '$2y$'.sprintf('%02d', $this->getWorkFactor()).'$'.$this->createSecureSalt(22, 'alnum'); - } - return crypt($password, $salt); + return Rhymix\Framework\Security::bcrypt($password, $salt); } - /** - * @brief Compare two strings in constant time - * @param string $a The first string - * @param string $b The second string - * @return bool - */ function strcmpConstantTime($a, $b) { return Rhymix\Framework\Security::compareStrings($a, $b); diff --git a/common/framework/password.php b/common/framework/password.php index 2e4e67017..bac5e284f 100644 --- a/common/framework/password.php +++ b/common/framework/password.php @@ -106,11 +106,11 @@ class Password } /** - * Get the currently selected hashing algorithm. + * Get the current default hashing algorithm. * * @return string */ - public static function getSelectedAlgorithm() + public static function getDefaultAlgorithm() { if (function_exists('getModel')) { @@ -152,6 +152,31 @@ class Password return $work_factor; } + /** + * Generate a reasonably strong random password. + * + * @param int $length + * @return string + */ + public static function getRandomPassword($length = 16) + { + while(true) + { + $source = base64_encode(Security::getRandom(64, 'binary')); + $source = strtr($source, 'iIoOjl10/', '@#$%&*-!?'); + $source_length = strlen($source); + for($i = 0; $i < $source_length - $length; $i++) + { + $candidate = substr($source, $i, $length); + if(preg_match('/[a-z]/', $candidate) && preg_match('/[A-Z]/', $candidate) && + preg_match('/[0-9]/', $candidate) && preg_match('/[^a-zA-Z0-9]/', $candidate)) + { + return $candidate; + } + } + } + } + /** * Hash a password. * @@ -160,12 +185,18 @@ class Password * On error, false will be returned. * * @param string $password - * @param string|array $algos + * @param string|array $algos (optional) * @param string $salt (optional) * @return string|false */ - public static function hashPassword($password, $algos, $salt = null) + public static function hashPassword($password, $algos = null, $salt = null) { + // If the algorithm is null, use the default algorithm. + if ($algos === null) + { + $algos = self::getDefaultAlgorithm(); + } + // Initialize the chain of hashes. $algos = array_map('strtolower', array_map('trim', is_array($algos) ? $algos : explode(',', $algos))); $hashchain = preg_replace('/\\s+/', ' ', trim($password)); @@ -292,16 +323,22 @@ class Password */ public static function checkPassword($password, $hash, $algos = null) { - if (!$algos) + if ($algos === null) { $algos = self::checkAlgorithm($hash); + foreach ($algos as $algo) + { + if (Security::compareStrings($hash, self::hashPassword($password, $algo, $hash))) + { + return true; + } + } + return false; } - if (!is_array($algos)) + else { - $algos = explode(',', $algos); + return Security::compareStrings($hash, self::hashPassword($password, $algos, $hash)); } - - return Security::compareStrings($hash, self::hashPassword($password, $algos, $hash)); } /** diff --git a/tests/unit/framework/PasswordTest.php b/tests/unit/framework/PasswordTest.php index 1bf859bbf..610092653 100644 --- a/tests/unit/framework/PasswordTest.php +++ b/tests/unit/framework/PasswordTest.php @@ -27,9 +27,9 @@ class PasswordTest extends \Codeception\TestCase\Test $this->assertTrue($algo === 'bcrypt' || $algo === 'pbkdf2'); } - public function testGetSelectedAlgorithm() + public function testGetDefaultAlgorithm() { - $algo = Rhymix\Framework\Password::getSelectedAlgorithm(); + $algo = Rhymix\Framework\Password::getDefaultAlgorithm(); $this->assertTrue($algo === 'bcrypt' || $algo === 'pbkdf2' || $algo === 'md5'); } @@ -40,6 +40,16 @@ class PasswordTest extends \Codeception\TestCase\Test $this->assertTrue($work_factor <= 31); } + public function testGetRandomPassword() + { + $password = Rhymix\Framework\Password::getRandomPassword(16); + $this->assertEquals(16, strlen($password)); + $this->assertRegexp('/[a-z]/', $password); + $this->assertRegexp('/[A-Z]/', $password); + $this->assertRegexp('/[0-9]/', $password); + $this->assertRegexp('/[^a-zA-Z0-9]/', $password); + } + public function testHashPassword() { $password = Rhymix\Framework\Security::getRandom(32);