* @link http://www.yiiframework.com/ * @copyright 2008-2013 Yii Software LLC * @license http://www.yiiframework.com/license/ */ /** * CPasswordHelper provides a simple API for secure password hashing and verification. * * CPasswordHelper uses the Blowfish hash algorithm available in many PHP runtime * environments through the PHP {@link http://php.net/manual/en/function.crypt.php crypt()} * built-in function. As of Dec 2012 it is the strongest algorithm available in PHP * and the only algorithm without some security concerns surrounding it. For this reason, * CPasswordHelper fails to initialize when run in and environment that does not have * crypt() and its Blowfish option. Systems with the option include: * (1) Most *nix systems since PHP 4 (the algorithm is part of the library function crypt(3)); * (2) All PHP systems since 5.3.0; (3) All PHP systems with the * {@link http://www.hardened-php.net/suhosin/ Suhosin patch}. * For more information about password hashing, crypt() and Blowfish, please read * the Yii Wiki article * {@link http://www.yiiframework.com/wiki/425/use-crypt-for-password-storage/ Use crypt() for password storage}. * and the * PHP RFC {@link http://wiki.php.net/rfc/password_hash Adding simple password hashing API}. * * CPasswordHelper throws an exception if the Blowfish hash algorithm is not * available in the runtime PHP's crypt() function. It can be used as follows * * Generate a hash from a password: *
 * $hash = CPasswordHelper::hashPassword($password);
 * 
* This hash can be stored in a database (e.g. CHAR(64) CHARACTER SET latin1). The * hash is usually generated and saved to the database when the user enters a new password. * But it can also be useful to generate and save a hash after validating a user's * password in order to change the cost or refresh the salt. * * To verify a password, fetch the user's saved hash from the database (into $hash) and: *
 * if (CPasswordHelper::verifyPassword($password, $hash))
 *     // password is good
 * else
 *     // password is bad
 * 
* * @author Tom Worster * @package system.utils * @since 1.1.14 */ class CPasswordHelper { /** * Check for availability of PHP crypt() with the Blowfish hash option. * @throws CException if the runtime system does not have PHP crypt() or its Blowfish hash option. */ protected static function checkBlowfish() { if(!function_exists('crypt')) throw new CException(Yii::t('yii','{class} requires the PHP crypt() function. This system does not have it.', array('{class}'=>__CLASS__))); if(!defined('CRYPT_BLOWFISH') || !CRYPT_BLOWFISH) throw new CException(Yii::t('yii', '{class} requires the Blowfish option of the PHP crypt() function. This system does not have it.', array('{class}'=>__CLASS__))); } /** * Generate a secure hash from a password and a random salt. * * Uses the * PHP {@link http://php.net/manual/en/function.crypt.php crypt()} built-in function * with the Blowfish hash option. * * @param string $password The password to be hashed. * @param int $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. * @return string The password hash string, ASCII and not longer than 64 characters. * @throws CException on bad password parameter or if crypt() with Blowfish hash is not available. */ public static function hashPassword($password,$cost=13) { self::checkBlowfish(); $salt=self::generateSalt($cost); $hash=crypt($password,$salt); if(!is_string($hash) || (function_exists('mb_strlen') ? mb_strlen($hash, '8bit') : strlen($hash))<32) throw new CException(Yii::t('yii','Internal error while generating hash.')); return $hash; } /** * Verify a password against a hash. * * @param string $password The password to verify. If password is empty or not a string, method will return false. * @param string $hash The hash to verify the password against. * @return bool True if the password matches the hash. * @throws CException on bad password or hash parameters or if crypt() with Blowfish hash is not available. */ public static function verifyPassword($password, $hash) { self::checkBlowfish(); if(!is_string($password) || $password==='') return false; if (!$password || !preg_match('{^\$2[axy]\$(\d\d)\$[\./0-9A-Za-z]{22}}',$hash,$matches) || $matches[1]<4 || $matches[1]>31) return false; $test=crypt($password,$hash); if(!is_string($test) || strlen($test)<32) return false; return self::same($test, $hash); } /** * Check for sameness of two strings using an algorithm with timing * independent of the string values if the subject strings are of equal length. * * The function can be useful to prevent timing attacks. For example, if $a and $b * are both hash values from the same algorithm, then the timing of this function * does not reveal whether or not there is a match. * * NOTE: timing is affected if $a and $b are different lengths or either is not a * string. For the purpose of checking password hash this does not reveal information * useful to an attacker. * * @see http://blog.astrumfutura.com/2010/10/nanosecond-scale-remote-timing-attacks-on-php-applications-time-to-take-them-seriously/ * @see http://codereview.stackexchange.com/questions/13512 * @see https://github.com/ircmaxell/password_compat/blob/master/lib/password.php * * @param string $a First subject string to compare. * @param string $b Second subject string to compare. * @return bool true if the strings are the same, false if they are different or if * either is not a string. */ public static function same($a,$b) { if(!is_string($a) || !is_string($b)) return false; $mb=function_exists('mb_strlen'); $length=$mb ? mb_strlen($a,'8bit') : strlen($a); if($length!==($mb ? mb_strlen($b,'8bit') : strlen($b))) return false; $check=0; for($i=0;$i<$length;$i+=1) $check|=(ord($a[$i])^ord($b[$i])); return $check===0; } /** * Generates a salt that can be used to generate a password hash. * * The PHP {@link http://php.net/manual/en/function.crypt.php crypt()} built-in function * requires, for the Blowfish hash algorithm, a salt string in a specific format: * "$2a$" (in which the "a" may be replaced by "x" or "y" see PHP manual for details), * a two digit cost parameter, * "$", * 22 characters from the alphabet "./0-9A-Za-z". * * @param int $cost Cost parameter used by the Blowfish hash algorithm. * @return string the random salt value. * @throws CException in case of invalid cost number */ public static function generateSalt($cost=13) { if(!is_numeric($cost)) throw new CException(Yii::t('yii','{class}::$cost must be a number.',array('{class}'=>__CLASS__))); $cost=(int)$cost; if($cost<4 || $cost>31) throw new CException(Yii::t('yii','{class}::$cost must be between 4 and 31.',array('{class}'=>__CLASS__))); if(($random=Yii::app()->getSecurityManager()->generateRandomString(22,true))===false) if(($random=Yii::app()->getSecurityManager()->generateRandomString(22,false))===false) throw new CException(Yii::t('yii','Unable to generate random string.')); return sprintf('$2a$%02d$',$cost).strtr($random,array('_'=>'.','~'=>'/')); } }