194 lines
7.7 KiB
PHP
194 lines
7.7 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* CPasswordHelper class file.
|
||
|
*
|
||
|
* @author Tom Worster <fsb@thefsb.org>
|
||
|
* @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:
|
||
|
* <pre>
|
||
|
* $hash = CPasswordHelper::hashPassword($password);
|
||
|
* </pre>
|
||
|
* 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:
|
||
|
* <pre>
|
||
|
* if (CPasswordHelper::verifyPassword($password, $hash))
|
||
|
* // password is good
|
||
|
* else
|
||
|
* // password is bad
|
||
|
* </pre>
|
||
|
*
|
||
|
* @author Tom Worster <fsb@thefsb.org>
|
||
|
* @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('_'=>'.','~'=>'/'));
|
||
|
}
|
||
|
}
|