PageRenderTime 11ms CodeModel.GetById 2ms app.highlight 6ms RepoModel.GetById 1ms app.codeStats 0ms

/framework/utils/CPasswordHelper.php

https://github.com/skyquiet/yii
PHP | 213 lines | 73 code | 17 blank | 123 comment | 21 complexity | affa561db488b8118091b5e0a9ac0d6a MD5 | raw file
  1<?php
  2/**
  3 * CPasswordHelper class file.
  4 *
  5 * @author Tom Worster <fsb@thefsb.org>
  6 * @link http://www.yiiframework.com/
  7 * @copyright 2008-2013 Yii Software LLC
  8 * @license http://www.yiiframework.com/license/
  9 */
 10
 11/**
 12 * CPasswordHelper provides a simple API for secure password hashing and verification.
 13 *
 14 * CPasswordHelper uses the Blowfish hash algorithm available in many PHP runtime
 15 * environments through the PHP {@link http://php.net/manual/en/function.crypt.php crypt()}
 16 * built-in function. As of Dec 2012 it is the strongest algorithm available in PHP
 17 * and the only algorithm without some security concerns surrounding it. For this reason,
 18 * CPasswordHelper fails to initialize when run in and environment that does not have
 19 * crypt() and its Blowfish option. Systems with the option include:
 20 * (1) Most *nix systems since PHP 4 (the algorithm is part of the library function crypt(3));
 21 * (2) All PHP systems since 5.3.0; (3) All PHP systems with the
 22 * {@link http://www.hardened-php.net/suhosin/ Suhosin patch}.
 23 * For more information about password hashing, crypt() and Blowfish, please read
 24 * the Yii Wiki article
 25 * {@link http://www.yiiframework.com/wiki/425/use-crypt-for-password-storage/ Use crypt() for password storage}.
 26 * and the
 27 * PHP RFC {@link http://wiki.php.net/rfc/password_hash Adding simple password hashing API}.
 28 *
 29 * CPasswordHelper throws an exception if the Blowfish hash algorithm is not
 30 * available in the runtime PHP's crypt() function. It can be used as follows
 31 *
 32 * Generate a hash from a password:
 33 * <pre>
 34 * $hash = CPasswordHelper::hashPassword($password);
 35 * </pre>
 36 * This hash can be stored in a database (e.g. CHAR(64) CHARACTER SET latin1). The
 37 * hash is usually generated and saved to the database when the user enters a new password.
 38 * But it can also be useful to generate and save a hash after validating a user's
 39 * password in order to change the cost or refresh the salt.
 40 *
 41 * To verify a password, fetch the user's saved hash from the database (into $hash) and:
 42 * <pre>
 43 * if (CPasswordHelper::verifyPassword($password, $hash)
 44 *     // password is good
 45 * else
 46 *     // password is bad
 47 * </pre>
 48 *
 49 * @author Tom Worster <fsb@thefsb.org>
 50 * @package system.utils
 51 * @since 1.1.14
 52 */
 53class CPasswordHelper
 54{
 55	/**
 56	 * Check for availability of PHP crypt() with the Blowfish hash option.
 57	 * @throws CException if the runtime system does not have PHP crypt() or its Blowfish hash option.
 58	 */
 59	protected static function checkBlowfish()
 60	{
 61		if(!function_exists('crypt'))
 62			throw new CException(Yii::t('yii',
 63				'{class} requires the PHP crypt() function. This system does not have it.',
 64				array("{class}"=>__CLASS__)
 65			));
 66
 67		if (!defined('CRYPT_BLOWFISH') || !CRYPT_BLOWFISH)
 68			throw new CException(Yii::t('yii',
 69				'{class} requires the Blowfish option of the PHP crypt() function. This system does not have it.',
 70				array("{class}"=>__CLASS__)
 71			));
 72    }
 73
 74	/**
 75	 * Generate a secure hash from a password and a random salt.
 76	 *
 77	 * Uses the
 78	 * PHP {@link http://php.net/manual/en/function.crypt.php crypt()} built-in function
 79	 * with the Blowfish hash option.
 80	 *
 81	 * @param string $password The password to be hashed.
 82	 * @param int $cost Cost parameter used by the Blowfish hash algorithm.
 83	 * The higher the value of cost,
 84	 * the longer it takes to generate the hash and to verify a password against it. Higher cost
 85	 * therefore slows down a brute-force attack. For best protection against brute for attacks,
 86	 * set it to the highest value that is tolerable on production servers. The time taken to
 87	 * compute the hash doubles for every increment by one of $cost. So, for example, if the
 88	 * hash takes 1 second to compute when $cost is 14 then then the compute time varies as
 89	 * 2^($cost - 14) seconds.
 90	 * @throws CException on bad password parameter or if crypt() with Blowfish hash is not available.
 91	 * @return string The password hash string, ASCII and not longer than 64 characters.
 92	 */
 93	public static function hashPassword($password,$cost=13)
 94	{
 95		self::checkBlowfish();
 96		$salt=self::generateSalt($cost);
 97		$hash=crypt($password,$salt);
 98
 99		if(!is_string($hash) || (function_exists('mb_strlen') ? mb_strlen($hash, '8bit') : strlen($hash))<32)
100			throw new CException(Yii::t('yii','Internal error while generating hash.'));
101
102		return $hash;
103    }
104
105	/**
106	 * Verify a password against a hash.
107	 *
108	 * @param string $password The password to verify.
109	 * @param string $hash The hash to verify the password against.
110	 *
111	 * @return bool True if the password matches the hash.
112	 * @throws CException on bad password or hash parameters or if crypt() with Blowfish hash is not available.
113	 */
114	public static function verifyPassword($password, $hash)
115	{
116		self::checkBlowfish();
117		if(!is_string($password) || $password === '')
118			throw new CException(Yii::t('yii','Cannot hash a password that is empty or not a string.'));
119
120		if (!$password || !preg_match('{^\$2[axy]\$(\d\d)\$[\./0-9A-Za-z]{22}}',$hash,$matches) || $matches[1] < 4 || $matches[1] > 31)
121			return false;
122
123		$test=crypt($password,$hash);
124		if(!is_string($test) || strlen($test)<32)
125			return false;
126
127		return self::same($test, $hash);
128	}
129
130	/**
131	 * Check for sameness of two strings using an algorithm with timing
132	 * independent of the string values if the subject strings are of equal length.
133	 *
134	 * The function can be useful to prevent timing attacks. For example, if $a and $b
135	 * are both hash values from the same algorithm, then the timing of this function
136	 * does not reveal whether or not there is a match.
137	 *
138	 * NOTE: timing is affected if $a and $b are different lengths or either is not a
139	 * string. For the purpose of checking password hash this does not reveal information
140	 * useful to an attacker.
141	 *
142	 * @see http://blog.astrumfutura.com/2010/10/nanosecond-scale-remote-timing-attacks-on-php-applications-time-to-take-them-seriously/
143	 * @see http://codereview.stackexchange.com/questions/13512
144	 * @see https://github.com/ircmaxell/password_compat/blob/master/lib/password.php
145	 *
146	 * @param $a string First subject string to compare.
147	 * @param $b string Second subject string to compare.
148	 *
149	 * @return bool true if the strings are the same, false if they are different or if
150	 * either is not a string.
151	 */
152	public static function same($a,$b)
153	{
154		if(!is_string($a) || !is_string($b))
155			return false;
156
157		$mb=function_exists('mb_strlen');
158		$length=$mb ? mb_strlen($a,'8bit') : strlen($a);
159		if($length!==($mb ? mb_strlen($b,'8bit') : strlen($b)))
160			return false;
161
162		$check=0;
163		for($i=0;$i<$length;$i+=1)
164			$check|=(ord($a[$i])^ord($b[$i]));
165
166		return $check===0;
167	}
168
169	/**
170	 * Generates a salt that can be used to generate a password hash.
171	 *
172	 * The PHP {@link http://php.net/manual/en/function.crypt.php crypt()} built-in function
173	 * requires, for the Blowfish hash algorithm, a salt string in a specific format:
174	 *  "$2a$" (in which the "a" may be replaced by "x" or "y" see PHP manual for details),
175	 *  a two digit cost parameter,
176	 *  "$",
177	 *  22 characters from the alphabet "./0-9A-Za-z".
178	 *
179	 * @param $cost
180	 * @throws CException
181	 * @return string the random salt value.
182	 */
183	protected static function generateSalt($cost = 13)
184	{
185		if(!is_numeric($cost))
186			throw new CException(Yii::t('yii',
187				'{class}::$cost must be a number.',
188				array("{class}"=>__CLASS__)
189			));
190
191		$cost=(int)$cost;
192		if($cost<4 || $cost>31)
193		    throw new CException(Yii::t('yii',
194				'{class}::$cost must be between 4 and 31.',
195				array("{class}"=>__CLASS__)
196			));
197
198		// Get 20 * 8bits of pseudo-random entropy from mt_rand().
199		$rand='';
200		for($i=0;$i<20;$i+=1)
201			$rand.=chr(mt_rand(0,255));
202
203		// Add the microtime for a little more entropy.
204		$rand.=microtime();
205		// Mix the bits cryptographically into a 20-byte binary string.
206		$rand=sha1($rand,true);
207		// Form the prefix that specifies Blowfish algorithm and cost parameter.
208		$salt=sprintf("$2a$%02d$",$cost);
209		// Append the random salt data in the required base64 format.
210		$salt.=str_replace('+','.',substr(base64_encode($rand),0,22));
211		return $salt;
212	}
213}