CPasswordHelper.php 7.76 KB
Newer Older
JULIO JARAMILLO's avatar
JULIO JARAMILLO committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
<?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 when run in an environment that does not have
 * crypt() with its Blowfish option and $2y hash fix. Compatible system is:
 *
 * (1) Most *nix systems since PHP 4 (the algorithm is part of the library function crypt(3));
 * (2) Any PHP since 5.3.7 or PHP with the {@link http://www.hardened-php.net/suhosin/ Suhosin patch} including
 * $2y fix backported. Note that Debian's 5.3.3 is not supported.
 *
 * 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(60) 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, always 60 ASCII 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:
	 *  "$2y$" (in which the "y" may be replaced by "a" 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('$2y$%02d$',$cost).strtr($random,array('_'=>'.','~'=>'/'));
	}
}