diff --git a/framework/base/InvalidParamException.php b/framework/base/InvalidParamException.php new file mode 100644 index 0000000..852546d --- /dev/null +++ b/framework/base/InvalidParamException.php @@ -0,0 +1,28 @@ + + * @since 2.0 + */ +class InvalidParamException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Invalid Parameter'); + } +} + diff --git a/framework/util/PasswordHelper.php b/framework/util/PasswordHelper.php new file mode 100644 index 0000000..5b29cc9 --- /dev/null +++ b/framework/util/PasswordHelper.php @@ -0,0 +1,161 @@ + + * @since 2.0 + */ + +class PasswordHelper +{ + /** + * Generate a secure hash from a password and a random salt. + * + * Uses the PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function + * with the Blowfish hash option. + * + * @param string $password The password to be hashed. + * @param integer $cost Cost parameter used by the Blowfish hash algorithm. + * The higher the value of cost, + * the longer it takes to generate the hash and to verify a password against it. Higher cost + * therefore slows down a brute-force attack. For best protection against brute for attacks, + * set it to the highest value that is tolerable on production servers. The time taken to + * compute the hash doubles for every increment by one of $cost. So, for example, if the + * hash takes 1 second to compute when $cost is 14 then then the compute time varies as + * 2^($cost - 14) seconds. + * @throws Exception on bad password parameter or cost parameter + * @return string The password hash string, ASCII and not longer than 64 characters. + */ + public static function hashPassword($password, $cost = 13) + { + $salt = static::generateSalt($cost); + $hash = crypt($password, $salt); + + if (!is_string($hash) || strlen($hash) < 32) { + throw new Exception('Unknown error occurred while generating hash.'); + } + + return $hash; + } + + /** + * Verifies a password against a hash. + * @param string $password The password to verify. + * @param string $hash The hash to verify the password against. + * @return boolean whether the password is correct. + * @throws InvalidParamException on bad password or hash parameters or if crypt() with Blowfish hash is not available. + */ + public static function verifyPassword($password, $hash) + { + if (!is_string($password) || $password === '') { + throw new InvalidParamException('Password must be a string and cannot be empty.'); + } + + if (!preg_match('/^\$2[axy]\$(\d\d)\$[\./0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) { + throw new InvalidParamException('Hash is invalid.'); + } + + $test = crypt($password, $hash); + $n = strlen($test); + if (strlen($test) < 32 || $n !== strlen($hash)) { + return false; + } + + // Use a for-loop to compare two strings to prevent timing attacks. See: + // http://codereview.stackexchange.com/questions/13512 + $check = 0; + for ($i = 0; $i < $n; ++$i) { + $check |= (ord($test[$i]) ^ ord($hash[$i])); + } + + return $check === 0; + } + + /** + * Generates a salt that can be used to generate a password hash. + * + * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function + * requires, for the Blowfish hash algorithm, a salt string in a specific format: + * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters + * from the alphabet "./0-9A-Za-z". + * + * @param integer $cost the cost parameter + * @return string the random salt value. + * @throws InvalidParamException if the cost parameter is not between 4 and 30 + */ + protected static function generateSalt($cost = 13) + { + $cost = (int)$cost; + if ($cost < 4 || $cost > 30) { + throw new InvalidParamException('Cost must be between 4 and 31.'); + } + + // Get 20 * 8bits of pseudo-random entropy from mt_rand(). + $rand = ''; + for ($i = 0; $i < 20; ++$i) { + $rand .= chr(mt_rand(0, 255)); + } + + // Add the microtime for a little more entropy. + $rand .= microtime(); + // Mix the bits cryptographically into a 20-byte binary string. + $rand = sha1($rand, true); + // Form the prefix that specifies Blowfish algorithm and cost parameter. + $salt = sprintf("$2y$%02d$", $cost); + // Append the random salt data in the required base64 format. + $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); + return $salt; + } +} \ No newline at end of file diff --git a/todo.md b/todo.md index d4bc275..c80da5a 100644 --- a/todo.md +++ b/todo.md @@ -23,6 +23,8 @@ * DateValidator: should we use CDateTimeParser, or simply use strtotime()? * CompareValidator::clientValidateAttribute(): depends on CHtml::activeId() +memo + * Minimal PHP version required: 5.3.7 (http://www.php.net/manual/en/function.crypt.php) --- - base