Newer
Older
Dries Buytaert
committed
<?php
// $Id$
/**
* @file
* Secure password hashing functions for user authentication.
*
* Based on the Portable PHP password hashing framework.
* @see http://www.openwall.com/phpass/
*
* An alternative or custom version of this password hashing API may be
* used by setting the variable password_inc to the name of the PHP file
* containing replacement user_hash_password(), user_check_password(), and
* user_needs_new_hash() functions.
*/
/**
* The standard log2 number of iterations for password stretching. This should
Dries Buytaert
committed
* increase by 1 every Drupal version in order to counteract increases in the
* speed and power of computers available to crack the hashes.
Dries Buytaert
committed
*/
define('DRUPAL_HASH_COUNT', 14);
/**
* The minimum allowed log2 number of iterations for password stretching.
Dries Buytaert
committed
*/
define('DRUPAL_MIN_HASH_COUNT', 7);
/**
* The maximum allowed log2 number of iterations for password stretching.
*/
Dries Buytaert
committed
define('DRUPAL_MAX_HASH_COUNT', 30);
Dries Buytaert
committed
/**
* The expected (and maximum) number of characters in a hashed password.
*/
define('DRUPAL_HASH_LENGTH', 55);
Dries Buytaert
committed
/**
* Returns a string for mapping an int to the corresponding base 64 character.
*/
function _password_itoa64() {
return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
}
/**
* Encode bytes into printable base 64 using the *nix standard from crypt().
*
* @param $input
* The string containing bytes to encode.
* @param $count
* The number of characters (bytes) to encode.
*
* @return
* Encoded string
*/
Dries Buytaert
committed
function _password_base64_encode($input, $count) {
Dries Buytaert
committed
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
$output = '';
$i = 0;
$itoa64 = _password_itoa64();
do {
$value = ord($input[$i++]);
$output .= $itoa64[$value & 0x3f];
if ($i < $count) {
$value |= ord($input[$i]) << 8;
}
$output .= $itoa64[($value >> 6) & 0x3f];
if ($i++ >= $count) {
break;
}
if ($i < $count) {
$value |= ord($input[$i]) << 16;
}
$output .= $itoa64[($value >> 12) & 0x3f];
if ($i++ >= $count) {
break;
}
$output .= $itoa64[($value >> 18) & 0x3f];
} while ($i < $count);
return $output;
}
/**
* Generates a random base 64-encoded salt prefixed with settings for the hash.
*
* Proper use of salts may defeat a number of attacks, including:
* - The ability to try candidate passwords against multiple hashes at once.
* - The ability to use pre-hashed lists of candidate passwords.
* - The ability to determine whether two users have the same (or different)
* password without actually having to guess one of the passwords.
*
* @param $count_log2
* Integer that determines the number of iterations used in the hashing
* process. A larger value is more secure, but takes more time to complete.
*
* @return
* A 12 character string containing the iteration count and a random salt.
*/
function _password_generate_salt($count_log2) {
Dries Buytaert
committed
$output = '$S$';
Dries Buytaert
committed
// Minimum log2 iterations is DRUPAL_MIN_HASH_COUNT.
$count_log2 = max($count_log2, DRUPAL_MIN_HASH_COUNT);
// Maximum log2 iterations is DRUPAL_MAX_HASH_COUNT.
// We encode the final log2 iteration count in base 64.
$itoa64 = _password_itoa64();
$output .= $itoa64[min($count_log2, DRUPAL_MAX_HASH_COUNT)];
// 6 bytes is the standard salt for a portable phpass hash.
$output .= _password_base64_encode(drupal_random_bytes(6), 6);
return $output;
}
/**
* Hash a password using a secure stretched hash.
*
* By using a salt and repeated hashing the password is "stretched". Its
* security is increased because it becomes much more computationally costly
* for an attacker to try to break the hash by brute-force computation of the
* hashes of a large number of plain-text words or strings to find a match.
*
Dries Buytaert
committed
* @param $algo
* The string name of a hashing algorithm usable by hash(), like 'sha256'.
Dries Buytaert
committed
* @param $password
* The plain-text password to hash.
* @param $setting
Dries Buytaert
committed
* An existing hash or the output of _password_generate_salt(). Must be
* at least 12 characters (the settings and salt).
Dries Buytaert
committed
*
* @return
* A string containing the hashed password (and salt) or FALSE on failure.
Dries Buytaert
committed
* The return string will be truncated at DRUPAL_HASH_LENGTH characters max.
Dries Buytaert
committed
*/
Dries Buytaert
committed
function _password_crypt($algo, $password, $setting) {
Dries Buytaert
committed
// The first 12 characters of an existing hash are its setting string.
$setting = substr($setting, 0, 12);
Dries Buytaert
committed
if ($setting[0] != '$' || $setting[2] != '$') {
Dries Buytaert
committed
return FALSE;
}
$count_log2 = _password_get_count_log2($setting);
// Hashes may be imported from elsewhere, so we allow != DRUPAL_HASH_COUNT
if ($count_log2 < DRUPAL_MIN_HASH_COUNT || $count_log2 > DRUPAL_MAX_HASH_COUNT) {
return FALSE;
}
$salt = substr($setting, 4, 8);
// Hashes must have an 8 character salt.
if (strlen($salt) != 8) {
return FALSE;
}
// Convert the base 2 logarithm into an integer.
Dries Buytaert
committed
$count = 1 << $count_log2;
Dries Buytaert
committed
// We rely on the hash() function being available in PHP 5.2+.
$hash = hash($algo, $salt . $password, TRUE);
Dries Buytaert
committed
do {
Dries Buytaert
committed
$hash = hash($algo, $hash . $password, TRUE);
Dries Buytaert
committed
} while (--$count);
Dries Buytaert
committed
$len = strlen($hash);
$output = $setting . _password_base64_encode($hash, $len);
Dries Buytaert
committed
// _password_base64_encode() of a 16 byte MD5 will always be 22 characters.
Dries Buytaert
committed
// _password_base64_encode() of a 64 byte sha512 will always be 86 characters.
$expected = 12 + ceil((8 * $len) / 6);
return (strlen($output) == $expected) ? substr($output, 0, DRUPAL_HASH_LENGTH) : FALSE;
Dries Buytaert
committed
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
}
/**
* Parse the log2 iteration count from a stored hash or setting string.
*/
function _password_get_count_log2($setting) {
$itoa64 = _password_itoa64();
return strpos($itoa64, $setting[3]);
}
/**
* Hash a password using a secure hash.
*
* @param $password
* A plain-text password.
* @param $count_log2
* Optional integer to specify the iteration count. Generally used only during
* mass operations where a value less than the default is needed for speed.
*
* @return
* A string containing the hashed password (and a salt), or FALSE on failure.
*/
function user_hash_password($password, $count_log2 = 0) {
if (empty($count_log2)) {
// Use the standard iteration count.
$count_log2 = variable_get('password_count_log2', DRUPAL_HASH_COUNT);
}
Dries Buytaert
committed
return _password_crypt('sha512', $password, _password_generate_salt($count_log2));
Dries Buytaert
committed
}
/**
* Check whether a plain text password matches a stored hashed password.
*
* Alternative implementations of this function may use other data in the
* $account object, for example the uid to look up the hash in a custom table
* or remote database.
*
* @param $password
* A plain-text password
* @param $account
Angie Byron
committed
* A user object with at least the fields from the {users} table.
Dries Buytaert
committed
*
* @return
* TRUE or FALSE.
*/
function user_check_password($password, $account) {
Dries Buytaert
committed
if (substr($account->pass, 0, 2) == 'U$') {
Dries Buytaert
committed
// This may be an updated password from user_update_7000(). Such hashes
// have 'U' added as the first character and need an extra md5().
$stored_hash = substr($account->pass, 1);
$password = md5($password);
}
else {
$stored_hash = $account->pass;
}
Dries Buytaert
committed
$type = substr($stored_hash, 0, 3);
switch ($type) {
case '$S$':
// A normal Drupal 7 password using sha512.
$hash = _password_crypt('sha512', $password, $stored_hash);
break;
case '$H$':
// phpBB3 uses "$H$" for the same thing as "$P$".
case '$P$':
// A phpass password generated using md5. This is an
// imported password or from an earlier Drupal version.
$hash = _password_crypt('md5', $password, $stored_hash);
break;
default:
return FALSE;
}
Dries Buytaert
committed
return ($hash && $stored_hash == $hash);
}
/**
* Check whether a user's hashed password needs to be replaced with a new hash.
*
* This is typically called during the login process when the plain text
Dries Buytaert
committed
* password is available. A new hash is needed when the desired iteration count
Dries Buytaert
committed
* has changed through a change in the variable password_count_log2 or
* DRUPAL_HASH_COUNT or if the user's password hash was generated in an update
* like user_update_7000().
*
* Alternative implementations of this function might use other criteria based
* on the fields in $account.
*
* @param $account
Angie Byron
committed
* A user object with at least the fields from the {users} table.
Dries Buytaert
committed
*
* @return
* TRUE or FALSE.
*/
function user_needs_new_hash($account) {
// Check whether this was an updated password.
Dries Buytaert
committed
if ((substr($account->pass, 0, 3) != '$S$') || (strlen($account->pass) != DRUPAL_HASH_LENGTH)) {
Dries Buytaert
committed
return TRUE;
}
// Check whether the iteration count used differs from the standard number.
return (_password_get_count_log2($account->pass) != variable_get('password_count_log2', DRUPAL_HASH_COUNT));
}