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(60) 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: */
53: class 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','{class} requires the PHP crypt() function. This system does not have it.',
63: array('{class}'=>__CLASS__)));
64:
65: if(!defined('CRYPT_BLOWFISH') || !CRYPT_BLOWFISH)
66: throw new CException(Yii::t('yii',
67: '{class} requires the Blowfish option of the PHP crypt() function. This system does not have it.',
68: array('{class}'=>__CLASS__)));
69: }
70:
71: /**
72: * Generate a secure hash from a password and a random salt.
73: *
74: * Uses the
75: * PHP {@link http://php.net/manual/en/function.crypt.php crypt()} built-in function
76: * with the Blowfish hash option.
77: *
78: * @param string $password The password to be hashed.
79: * @param int $cost Cost parameter used by the Blowfish hash algorithm.
80: * The higher the value of cost,
81: * the longer it takes to generate the hash and to verify a password against it. Higher cost
82: * therefore slows down a brute-force attack. For best protection against brute for attacks,
83: * set it to the highest value that is tolerable on production servers. The time taken to
84: * compute the hash doubles for every increment by one of $cost. So, for example, if the
85: * hash takes 1 second to compute when $cost is 14 then then the compute time varies as
86: * 2^($cost - 14) seconds.
87: * @return string The password hash string, always 60 ASCII characters.
88: * @throws CException on bad password parameter or if crypt() with Blowfish hash is not available.
89: */
90: public static function hashPassword($password,$cost=13)
91: {
92: self::checkBlowfish();
93: $salt=self::generateSalt($cost);
94: $hash=crypt($password,$salt);
95:
96: if(!is_string($hash) || (function_exists('mb_strlen') ? mb_strlen($hash, '8bit') : strlen($hash))<32)
97: throw new CException(Yii::t('yii','Internal error while generating hash.'));
98:
99: return $hash;
100: }
101:
102: /**
103: * Verify a password against a hash.
104: *
105: * @param string $password The password to verify. If password is empty or not a string, method will return false.
106: * @param string $hash The hash to verify the password against.
107: * @return bool True if the password matches the hash.
108: * @throws CException on bad password or hash parameters or if crypt() with Blowfish hash is not available.
109: */
110: public static function verifyPassword($password, $hash)
111: {
112: self::checkBlowfish();
113: if(!is_string($password) || $password==='')
114: return false;
115:
116: if (!$password || !preg_match('{^\$2[axy]\$(\d\d)\$[\./0-9A-Za-z]{22}}',$hash,$matches) ||
117: $matches[1]<4 || $matches[1]>31)
118: return false;
119:
120: $test=crypt($password,$hash);
121: if(!is_string($test) || strlen($test)<32)
122: return false;
123:
124: return self::same($test, $hash);
125: }
126:
127: /**
128: * Check for sameness of two strings using an algorithm with timing
129: * independent of the string values if the subject strings are of equal length.
130: *
131: * The function can be useful to prevent timing attacks. For example, if $a and $b
132: * are both hash values from the same algorithm, then the timing of this function
133: * does not reveal whether or not there is a match.
134: *
135: * NOTE: timing is affected if $a and $b are different lengths or either is not a
136: * string. For the purpose of checking password hash this does not reveal information
137: * useful to an attacker.
138: *
139: * @see http://blog.astrumfutura.com/2010/10/nanosecond-scale-remote-timing-attacks-on-php-applications-time-to-take-them-seriously/
140: * @see http://codereview.stackexchange.com/questions/13512
141: * @see https://github.com/ircmaxell/password_compat/blob/master/lib/password.php
142: *
143: * @param string $a First subject string to compare.
144: * @param string $b Second subject string to compare.
145: * @return bool true if the strings are the same, false if they are different or if
146: * either is not a string.
147: */
148: public static function same($a,$b)
149: {
150: if(!is_string($a) || !is_string($b))
151: return false;
152:
153: $mb=function_exists('mb_strlen');
154: $length=$mb ? mb_strlen($a,'8bit') : strlen($a);
155: if($length!==($mb ? mb_strlen($b,'8bit') : strlen($b)))
156: return false;
157:
158: $check=0;
159: for($i=0;$i<$length;$i+=1)
160: $check|=(ord($a[$i])^ord($b[$i]));
161:
162: return $check===0;
163: }
164:
165: /**
166: * Generates a salt that can be used to generate a password hash.
167: *
168: * The PHP {@link http://php.net/manual/en/function.crypt.php crypt()} built-in function
169: * requires, for the Blowfish hash algorithm, a salt string in a specific format:
170: * "$2y$" (in which the "y" may be replaced by "a" or "y" see PHP manual for details),
171: * a two digit cost parameter,
172: * "$",
173: * 22 characters from the alphabet "./0-9A-Za-z".
174: *
175: * @param int $cost Cost parameter used by the Blowfish hash algorithm.
176: * @return string the random salt value.
177: * @throws CException in case of invalid cost number
178: */
179: public static function generateSalt($cost=13)
180: {
181: if(!is_numeric($cost))
182: throw new CException(Yii::t('yii','{class}::$cost must be a number.',array('{class}'=>__CLASS__)));
183:
184: $cost=(int)$cost;
185: if($cost<4 || $cost>31)
186: throw new CException(Yii::t('yii','{class}::$cost must be between 4 and 31.',array('{class}'=>__CLASS__)));
187:
188: if(($random=Yii::app()->getSecurityManager()->generateRandomString(22,true))===false)
189: if(($random=Yii::app()->getSecurityManager()->generateRandomString(22,false))===false)
190: throw new CException(Yii::t('yii','Unable to generate random string.'));
191: return sprintf('$2y$%02d$',$cost).strtr($random,array('_'=>'.','~'=>'/'));
192: }
193: }
194: