Index: includes/passwordhash.inc =================================================================== RCS file: includes/passwordhash.inc diff -N includes/passwordhash.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/passwordhash.inc 20 Mar 2008 02:50:30 -0000 @@ -0,0 +1,178 @@ + DRUPAL_MAX_HASH_ITERATION) { + return FALSE; + } + $salt = $parts[2]; + // Portable hashes will have an 8 char 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 crypto + // 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 $iteration_count_log2 + * Optional. Specifies the iteration count. Generally used only during mass + * operations where a value different than the default is neeeded for speed. + * + * @return + * A string containing the hashed password (and a salt), or FALSE on failure. + */ +function user_hash_password($password, $iteration_count_log2 = NULL) { + if (empty($iteration_count_log2)) { + $iteration_count_log2 = variable_get('passwordhash_count_log2', DRUPAL_HASH_ITERATION); + } + return _passwordhash_crypt($password, _passwordhash_gensalt($iteration_count_log2)); +} + +/** + * Check whether a plain text password matches a stored hashed password. + * + * @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 system_update_7001(). 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 = _passwordhash_crypt($password, $stored_hash); + return ($hash && $stored_hash == $hash); +} + +/** + * Check whether a user's hashed password needs to be replaced with a new hash. + * + * @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('passwordhash_count_log2', DRUPAL_HASH_ITERATION)); +} + Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.243 diff -u -p -r1.243 system.install --- modules/system/system.install 17 Mar 2008 16:53:58 -0000 1.243 +++ modules/system/system.install 20 Mar 2008 02:50:31 -0000 @@ -2659,6 +2659,48 @@ function system_update_7000() { } /** + * Increase the length of the password field to accomodate better hashes. + * + * Also re-hashes all current passwords to improve security. This will take a + * while to run. + */ +function system_update_7001(&$sandbox) { + $ret = array('#finished' => 0); + // A lower number than DRUPAL_HASH_ITERATION to make the update run at a reasonable speed + $drupal_new_hash_iteration = 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('passwordhash_inc', './includes/passwordhash.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'], $drupal_new_hash_iteration); + 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 updates-6.x-to-7.x" * The next series of updates should start at 8000. */ 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 20 Mar 2008 02:50:31 -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', 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 20 Mar 2008 02:50:31 -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,10 +214,13 @@ function user_save($account, $array = ar $user_fields = $table['fields']; if (!empty($array['pass'])) { - $array['pass'] = md5($array['pass']); - } - else { - // Avoid overwriting an existing password with a blank password. + // Allow alternate hashing schemes. + require_once variable_get('passwordhash_inc', './includes/passwordhash.inc'); + $array['pass'] = user_hash_password(trim($array['pass'])); + } + // Avoid overwriting an existing password with a blank password, and don't + // save it if the hashing failed and returned FALSE. + if (empty($array['pass'])) { unset($array['pass']); } @@ -1283,12 +1286,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 hashing schemes. + require_once variable_get('passwordhash_inc', './includes/passwordhash.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; + } + } } }