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 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Secure password hashing functions for user authentication.
+ *
+ * Derived from 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
+ * increase by 1 at least every other Drupal version in order to counteract
+ * increases in the speed and power of computers available to crack the hashes.
+ * To compare to the hash strength of a bcrypt hash, subtract 5.
+ */
+define('DRUPAL_HASH_COUNT', 14);
+
+/**
+ * The min and max allowed log2 number of iterations for password stretching.
+ */
+define('DRUPAL_MIN_HASH_COUNT', 7);
+define('DRUPAL_MAX_HASH_COUNT', 30);
+
+/**
+ * Generates a random base 64 encoded salt prefixed with settings for the hash.
+ *
+ * A unique salt per user is concatenated to the password during hashing so that
+ * if two users have the same password it will not be evident by examining the
+ * hashed values. In addition, use of a per-user random salt is critical so that
+ * hashes cannot be readily pre-computed by someone who wishes to crack them.
+ *
+ * @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 string containing the count setting and random salt. It has three parts
+ *   separated by ':', for example - $D:14:fF3nVpiud
+ */
+function _password_generate_salt($count_log2) {
+  // Minimum log2 iterations is DRUPAL_MIN_HASH_COUNT.
+  $count_log2 = max($count_log2, DRUPAL_MIN_HASH_COUNT);
+  $output = '$D:';
+  // Maximum log2 iterations is DRUPAL_MAX_HASH_COUNT.
+  $output .= min($count_log2, DRUPAL_MAX_HASH_COUNT) .':';
+  // 6 bytes is the standard for a portable phpass hash.
+  $output .= base64_encode(drupal_random_bytes(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.
+ *
+ * @param $password
+ *   The plain-text password to hash.
+ * @param $setting
+ *   An existing hash or the output of _password_generate_salt().
+ *
+ * @return
+ *   A string containing the hashed password (and salt) or FALSE on failure.
+ */
+function _password_crypt($password, $setting)  {
+  $parts = explode(':', $setting);
+  if ((count($parts) < 3) || ($parts[0] != '$D')) {
+    return FALSE;
+  }
+  $count_log2 = $parts[1];
+  // 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 = $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;
+      }
+    }
   }
 }
 
