|
1 <?php |
|
2 |
|
3 /** |
|
4 * @file |
|
5 * Secure password hashing functions for user authentication. |
|
6 * |
|
7 * Based on the Portable PHP password hashing framework. |
|
8 * @see http://www.openwall.com/phpass/ |
|
9 * |
|
10 * An alternative or custom version of this password hashing API may be |
|
11 * used by setting the variable password_inc to the name of the PHP file |
|
12 * containing replacement user_hash_password(), user_check_password(), and |
|
13 * user_needs_new_hash() functions. |
|
14 */ |
|
15 |
|
16 /** |
|
17 * The standard log2 number of iterations for password stretching. This should |
|
18 * increase by 1 every Drupal version in order to counteract increases in the |
|
19 * speed and power of computers available to crack the hashes. |
|
20 */ |
|
21 define('DRUPAL_HASH_COUNT', 15); |
|
22 |
|
23 /** |
|
24 * The minimum allowed log2 number of iterations for password stretching. |
|
25 */ |
|
26 define('DRUPAL_MIN_HASH_COUNT', 7); |
|
27 |
|
28 /** |
|
29 * The maximum allowed log2 number of iterations for password stretching. |
|
30 */ |
|
31 define('DRUPAL_MAX_HASH_COUNT', 30); |
|
32 |
|
33 /** |
|
34 * The expected (and maximum) number of characters in a hashed password. |
|
35 */ |
|
36 define('DRUPAL_HASH_LENGTH', 55); |
|
37 |
|
38 /** |
|
39 * Returns a string for mapping an int to the corresponding base 64 character. |
|
40 */ |
|
41 function _password_itoa64() { |
|
42 return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; |
|
43 } |
|
44 |
|
45 /** |
|
46 * Encodes bytes into printable base 64 using the *nix standard from crypt(). |
|
47 * |
|
48 * @param $input |
|
49 * The string containing bytes to encode. |
|
50 * @param $count |
|
51 * The number of characters (bytes) to encode. |
|
52 * |
|
53 * @return |
|
54 * Encoded string |
|
55 */ |
|
56 function _password_base64_encode($input, $count) { |
|
57 $output = ''; |
|
58 $i = 0; |
|
59 $itoa64 = _password_itoa64(); |
|
60 do { |
|
61 $value = ord($input[$i++]); |
|
62 $output .= $itoa64[$value & 0x3f]; |
|
63 if ($i < $count) { |
|
64 $value |= ord($input[$i]) << 8; |
|
65 } |
|
66 $output .= $itoa64[($value >> 6) & 0x3f]; |
|
67 if ($i++ >= $count) { |
|
68 break; |
|
69 } |
|
70 if ($i < $count) { |
|
71 $value |= ord($input[$i]) << 16; |
|
72 } |
|
73 $output .= $itoa64[($value >> 12) & 0x3f]; |
|
74 if ($i++ >= $count) { |
|
75 break; |
|
76 } |
|
77 $output .= $itoa64[($value >> 18) & 0x3f]; |
|
78 } while ($i < $count); |
|
79 |
|
80 return $output; |
|
81 } |
|
82 |
|
83 /** |
|
84 * Generates a random base 64-encoded salt prefixed with settings for the hash. |
|
85 * |
|
86 * Proper use of salts may defeat a number of attacks, including: |
|
87 * - The ability to try candidate passwords against multiple hashes at once. |
|
88 * - The ability to use pre-hashed lists of candidate passwords. |
|
89 * - The ability to determine whether two users have the same (or different) |
|
90 * password without actually having to guess one of the passwords. |
|
91 * |
|
92 * @param $count_log2 |
|
93 * Integer that determines the number of iterations used in the hashing |
|
94 * process. A larger value is more secure, but takes more time to complete. |
|
95 * |
|
96 * @return |
|
97 * A 12 character string containing the iteration count and a random salt. |
|
98 */ |
|
99 function _password_generate_salt($count_log2) { |
|
100 $output = '$S$'; |
|
101 // Ensure that $count_log2 is within set bounds. |
|
102 $count_log2 = _password_enforce_log2_boundaries($count_log2); |
|
103 // We encode the final log2 iteration count in base 64. |
|
104 $itoa64 = _password_itoa64(); |
|
105 $output .= $itoa64[$count_log2]; |
|
106 // 6 bytes is the standard salt for a portable phpass hash. |
|
107 $output .= _password_base64_encode(drupal_random_bytes(6), 6); |
|
108 return $output; |
|
109 } |
|
110 |
|
111 /** |
|
112 * Ensures that $count_log2 is within set bounds. |
|
113 * |
|
114 * @param $count_log2 |
|
115 * Integer that determines the number of iterations used in the hashing |
|
116 * process. A larger value is more secure, but takes more time to complete. |
|
117 * |
|
118 * @return |
|
119 * Integer within set bounds that is closest to $count_log2. |
|
120 */ |
|
121 function _password_enforce_log2_boundaries($count_log2) { |
|
122 if ($count_log2 < DRUPAL_MIN_HASH_COUNT) { |
|
123 return DRUPAL_MIN_HASH_COUNT; |
|
124 } |
|
125 elseif ($count_log2 > DRUPAL_MAX_HASH_COUNT) { |
|
126 return DRUPAL_MAX_HASH_COUNT; |
|
127 } |
|
128 |
|
129 return (int) $count_log2; |
|
130 } |
|
131 |
|
132 /** |
|
133 * Hash a password using a secure stretched hash. |
|
134 * |
|
135 * By using a salt and repeated hashing the password is "stretched". Its |
|
136 * security is increased because it becomes much more computationally costly |
|
137 * for an attacker to try to break the hash by brute-force computation of the |
|
138 * hashes of a large number of plain-text words or strings to find a match. |
|
139 * |
|
140 * @param $algo |
|
141 * The string name of a hashing algorithm usable by hash(), like 'sha256'. |
|
142 * @param $password |
|
143 * Plain-text password up to 512 bytes (128 to 512 UTF-8 characters) to hash. |
|
144 * @param $setting |
|
145 * An existing hash or the output of _password_generate_salt(). Must be |
|
146 * at least 12 characters (the settings and salt). |
|
147 * |
|
148 * @return |
|
149 * A string containing the hashed password (and salt) or FALSE on failure. |
|
150 * The return string will be truncated at DRUPAL_HASH_LENGTH characters max. |
|
151 */ |
|
152 function _password_crypt($algo, $password, $setting) { |
|
153 // Prevent DoS attacks by refusing to hash large passwords. |
|
154 if (strlen($password) > 512) { |
|
155 return FALSE; |
|
156 } |
|
157 // The first 12 characters of an existing hash are its setting string. |
|
158 $setting = substr($setting, 0, 12); |
|
159 |
|
160 if ($setting[0] != '$' || $setting[2] != '$') { |
|
161 return FALSE; |
|
162 } |
|
163 $count_log2 = _password_get_count_log2($setting); |
|
164 // Hashes may be imported from elsewhere, so we allow != DRUPAL_HASH_COUNT |
|
165 if ($count_log2 < DRUPAL_MIN_HASH_COUNT || $count_log2 > DRUPAL_MAX_HASH_COUNT) { |
|
166 return FALSE; |
|
167 } |
|
168 $salt = substr($setting, 4, 8); |
|
169 // Hashes must have an 8 character salt. |
|
170 if (strlen($salt) != 8) { |
|
171 return FALSE; |
|
172 } |
|
173 |
|
174 // Convert the base 2 logarithm into an integer. |
|
175 $count = 1 << $count_log2; |
|
176 |
|
177 // We rely on the hash() function being available in PHP 5.2+. |
|
178 $hash = hash($algo, $salt . $password, TRUE); |
|
179 do { |
|
180 $hash = hash($algo, $hash . $password, TRUE); |
|
181 } while (--$count); |
|
182 |
|
183 $len = strlen($hash); |
|
184 $output = $setting . _password_base64_encode($hash, $len); |
|
185 // _password_base64_encode() of a 16 byte MD5 will always be 22 characters. |
|
186 // _password_base64_encode() of a 64 byte sha512 will always be 86 characters. |
|
187 $expected = 12 + ceil((8 * $len) / 6); |
|
188 return (strlen($output) == $expected) ? substr($output, 0, DRUPAL_HASH_LENGTH) : FALSE; |
|
189 } |
|
190 |
|
191 /** |
|
192 * Parse the log2 iteration count from a stored hash or setting string. |
|
193 */ |
|
194 function _password_get_count_log2($setting) { |
|
195 $itoa64 = _password_itoa64(); |
|
196 return strpos($itoa64, $setting[3]); |
|
197 } |
|
198 |
|
199 /** |
|
200 * Hash a password using a secure hash. |
|
201 * |
|
202 * @param $password |
|
203 * A plain-text password. |
|
204 * @param $count_log2 |
|
205 * Optional integer to specify the iteration count. Generally used only during |
|
206 * mass operations where a value less than the default is needed for speed. |
|
207 * |
|
208 * @return |
|
209 * A string containing the hashed password (and a salt), or FALSE on failure. |
|
210 */ |
|
211 function user_hash_password($password, $count_log2 = 0) { |
|
212 if (empty($count_log2)) { |
|
213 // Use the standard iteration count. |
|
214 $count_log2 = variable_get('password_count_log2', DRUPAL_HASH_COUNT); |
|
215 } |
|
216 return _password_crypt('sha512', $password, _password_generate_salt($count_log2)); |
|
217 } |
|
218 |
|
219 /** |
|
220 * Check whether a plain text password matches a stored hashed password. |
|
221 * |
|
222 * Alternative implementations of this function may use other data in the |
|
223 * $account object, for example the uid to look up the hash in a custom table |
|
224 * or remote database. |
|
225 * |
|
226 * @param $password |
|
227 * A plain-text password |
|
228 * @param $account |
|
229 * A user object with at least the fields from the {users} table. |
|
230 * |
|
231 * @return |
|
232 * TRUE or FALSE. |
|
233 */ |
|
234 function user_check_password($password, $account) { |
|
235 if (substr($account->pass, 0, 2) == 'U$') { |
|
236 // This may be an updated password from user_update_7000(). Such hashes |
|
237 // have 'U' added as the first character and need an extra md5(). |
|
238 $stored_hash = substr($account->pass, 1); |
|
239 $password = md5($password); |
|
240 } |
|
241 else { |
|
242 $stored_hash = $account->pass; |
|
243 } |
|
244 |
|
245 $type = substr($stored_hash, 0, 3); |
|
246 switch ($type) { |
|
247 case '$S$': |
|
248 // A normal Drupal 7 password using sha512. |
|
249 $hash = _password_crypt('sha512', $password, $stored_hash); |
|
250 break; |
|
251 case '$H$': |
|
252 // phpBB3 uses "$H$" for the same thing as "$P$". |
|
253 case '$P$': |
|
254 // A phpass password generated using md5. This is an |
|
255 // imported password or from an earlier Drupal version. |
|
256 $hash = _password_crypt('md5', $password, $stored_hash); |
|
257 break; |
|
258 default: |
|
259 return FALSE; |
|
260 } |
|
261 return ($hash && $stored_hash == $hash); |
|
262 } |
|
263 |
|
264 /** |
|
265 * Check whether a user's hashed password needs to be replaced with a new hash. |
|
266 * |
|
267 * This is typically called during the login process when the plain text |
|
268 * password is available. A new hash is needed when the desired iteration count |
|
269 * has changed through a change in the variable password_count_log2 or |
|
270 * DRUPAL_HASH_COUNT or if the user's password hash was generated in an update |
|
271 * like user_update_7000(). |
|
272 * |
|
273 * Alternative implementations of this function might use other criteria based |
|
274 * on the fields in $account. |
|
275 * |
|
276 * @param $account |
|
277 * A user object with at least the fields from the {users} table. |
|
278 * |
|
279 * @return |
|
280 * TRUE or FALSE. |
|
281 */ |
|
282 function user_needs_new_hash($account) { |
|
283 // Check whether this was an updated password. |
|
284 if ((substr($account->pass, 0, 3) != '$S$') || (strlen($account->pass) != DRUPAL_HASH_LENGTH)) { |
|
285 return TRUE; |
|
286 } |
|
287 // Ensure that $count_log2 is within set bounds. |
|
288 $count_log2 = _password_enforce_log2_boundaries(variable_get('password_count_log2', DRUPAL_HASH_COUNT)); |
|
289 // Check whether the iteration count used differs from the standard number. |
|
290 return (_password_get_count_log2($account->pass) !== $count_log2); |
|
291 } |