Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.760 diff -u -p -r1.760 common.inc --- includes/common.inc 17 Mar 2008 17:01:05 -0000 1.760 +++ includes/common.inc 22 Mar 2008 17:12:43 -0000 @@ -2308,6 +2308,38 @@ function drupal_urlencode($text) { } /** + * Returns a string of highly randomized bytes (over the full 8-bit range). + * + * This function is better than simply calling mt_rand() or any other built-in + * PHP function because it can return a long string of bytes (compared to < 4 + * bytes normally from mt_rand()) and uses the best available pseudo-random source. + * + * @param $count + * The number of characters (bytes) to return in the string. + */ +function drupal_random_bytes($count) { + static $random_state; + // We initialize with the somewhat random PHP process ID on the first call. + if (empty($random_state)) { + $random_state = getmypid(); + } + $output = ''; + // /dev/urandom is available on many *nix systems and is considered the best + // commonly available pseudo-random source. + if ($fh = @fopen('/dev/urandom', 'rb')) { + $output = fread($fh, $count); + fclose($fh); + } + // If /dev/urandom is not available or returns no bytes, this loop will + // generate a good set of pseudo-random bytes on any system. + while (strlen($output) < $count) { + $random_state = md5(microtime() . mt_rand() . $random_state); + $output .= md5(mt_rand() . $random_state, TRUE); + } + return substr($output, 0, $count); +} + +/** * Ensure the private key variable used to generate tokens is set. * * @return @@ -2315,7 +2347,7 @@ function drupal_urlencode($text) { */ function drupal_get_private_key() { if (!($key = variable_get('drupal_private_key', 0))) { - $key = md5(uniqid(mt_rand(), true)) . md5(uniqid(mt_rand(), true)); + $key = md5(drupal_random_bytes(64)); variable_set('drupal_private_key', $key); } return $key; Index: includes/password.inc =================================================================== RCS file: includes/password.inc diff -N includes/password.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/password.inc 22 Mar 2008 17:12:43 -0000 @@ -0,0 +1,186 @@ + DRUPAL_MAX_HASH_COUNT) { + return FALSE; + } + $salt = $parts[2]; + // Hashes must have at least an 8 character salt. + if (strlen($salt) < 8) { + return FALSE; + } + + // We must use md5() or sha1() here since they are the only cryptographic + // primitives always available in PHP 5. To implement our own low-level + // cryptographic function in PHP would result in much worse performance and + // consequently in lower iteration counts and hashes that are quicker to crack + // (by non-PHP code). + + $count = 1 << $count_log2; + + $hash = md5($salt . $password, TRUE); + do { + $hash = md5($hash . $password, TRUE); + } while (--$count); + + $base64_hash = base64_encode($hash); + $output = '$D:'. $count_log2 .':'. $salt .':'. $base64_hash; + // base64_encode() of a 16 byte binary MD5 will always be 24 characters. + return (strlen($base64_hash) == 24) ? $output : FALSE; +} + +/** + * 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); + } + return _password_crypt($password, _password_generate_salt($count_log2)); +} + +/** + * 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 + * A user object with at least the fields from the {users} table. + * + * @return + * TRUE or FALSE. + */ +function user_check_password($password, $account) { + if (substr($account->pass, 0, 3) == 'U$D') { + // 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; + } + $hash = _password_crypt($password, $stored_hash); + 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 + * password is available. A new hash is needed when the desired iteration count + * 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 + * A user object with at least the fields from the {users} table. + * + * @return + * TRUE or FALSE. + */ +function user_needs_new_hash($account) { + $parts = explode(':', $account->pass); + // Check whether this was an updated password. + if ((count($parts) < 4) || ($parts[0] != '$D')) { + return TRUE; + } + // Check whether the iteration count used differs from the standard number. + return ($parts[1] != variable_get('password_count_log2', DRUPAL_HASH_COUNT)); +} + Index: modules/user/user.install =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.install,v retrieving revision 1.7 diff -u -p -r1.7 user.install --- modules/user/user.install 15 Mar 2008 12:31:29 -0000 1.7 +++ modules/user/user.install 22 Mar 2008 17:12:43 -0000 @@ -150,10 +150,10 @@ function user_schema() { ), 'pass' => array( 'type' => 'varchar', - 'length' => 32, + 'length' => 128, 'not null' => TRUE, 'default' => '', - 'description' => t("User's password (md5 hash)."), + 'description' => t("User's password (hashed)."), ), 'mail' => array( 'type' => 'varchar', @@ -295,3 +295,55 @@ function user_schema() { return $schema; } +/** + * @defgroup user-updates-6.x-to-7.x User updates from 6.x to 7.x + * @{ + */ + +/** + * Increase the length of the password field to accommodate better hashes. + * + * Also re-hashes all current passwords to improve security. This may be a + * lengthy process, and is performed batch-wise. + */ +function user_update_7000(&$sandbox) { + $ret = array('#finished' => 0); + // Lower than DRUPAL_HASH_COUNT to make the update run at a reasonable speed. + $hash_count_log2 = 11; + // Multi-part update. + if (!isset($sandbox['user_from'])) { + db_change_field($ret, 'users', 'pass', 'pass', array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => '')); + $sandbox['user_from'] = 0; + $sandbox['user_count'] = db_result(db_query("SELECT COUNT(uid) FROM {users}")); + } + else { + require_once variable_get('password_inc', './includes/password.inc'); + // Hash again all current hashed passwords. + $has_rows = FALSE; + // Update this many per page load. + $count = 1000; + $result = db_query_range("SELECT uid, pass FROM {users} WHERE uid > 0 ORDER BY uid", $sandbox['user_from'], $count); + while ($account = db_fetch_array($result)) { + $has_rows = TRUE; + $new_hash = user_hash_password($account['pass'], $hash_count_log2); + if ($new_hash) { + // Indicate an updated password. + $new_hash = 'U'. $new_hash; + db_query("UPDATE {users} SET pass = '%s' WHERE uid = %d", $new_hash, $account['uid']); + } + } + $ret['#finished'] = $sandbox['user_from']/$sandbox['user_count']; + $sandbox['user_from'] += $count; + if (!$has_rows) { + $ret['#finished'] = 1; + $ret[] = array('success' => TRUE, 'query' => "UPDATE {users} SET pass = 'U'. user_hash_password(pass) WHERE uid > 0"); + } + } + return $ret; +} + +/** + * @} End of "defgroup user-updates-6.x-to-7.x" + * The next series of updates should start at 8000. + */ + Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.897 diff -u -p -r1.897 user.module --- modules/user/user.module 19 Mar 2008 07:35:15 -0000 1.897 +++ modules/user/user.module 22 Mar 2008 17:12:43 -0000 @@ -157,7 +157,7 @@ function user_load($array = array()) { } else if ($key == 'pass') { $query[] = "pass = '%s'"; - $params[] = md5($value); + $params[] = $value; } else { $query[]= "LOWER($key) = LOWER('%s')"; @@ -214,7 +214,13 @@ function user_save($account, $array = ar $user_fields = $table['fields']; if (!empty($array['pass'])) { - $array['pass'] = md5($array['pass']); + // Allow alternate password hashing schemes. + require_once variable_get('password_inc', './includes/password.inc'); + $array['pass'] = user_hash_password(trim($array['pass'])); + // Abort if the hashing failed and returned FALSE. + if (!$array['pass']) { + return FALSE; + } } else { // Avoid overwriting an existing password with a blank password. @@ -1283,12 +1289,26 @@ function user_login_final_validate($form function user_authenticate($form_values = array()) { global $user; + $password = trim($form_values['pass']); // Name and pass keys are required. - if (!empty($form_values['name']) && !empty($form_values['pass']) && - $account = user_load(array('name' => $form_values['name'], 'pass' => trim($form_values['pass']), 'status' => 1))) { - $user = $account; - user_authenticate_finalize($form_values); - return $user; + if (!empty($form_values['name']) && !empty($password)) { + $account = db_fetch_object(db_query("SELECT * FROM {users} WHERE name = '%s' AND status = 1", $form_values['name'])); + if ($account) { + // Allow alternate password hashing schemes. + require_once variable_get('password_inc', './includes/password.inc'); + if (user_check_password($password, $account)) { + if (user_needs_new_hash($account)) { + $new_hash = user_hash_password($password); + if ($new_hash) { + db_query("UPDATE {users} SET pass = '%s' WHERE uid = %d", $new_hash, $account->uid); + } + } + $account = user_load(array('uid' => $account->uid, 'status' => 1)); + $user = $account; + user_authenticate_finalize($form_values); + return $user; + } + } } }