Index: includes/password.inc
===================================================================
RCS file: includes/password.inc
diff -N includes/password.inc
--- includes/password.inc	26 Feb 2009 07:30:26 -0000	1.6
+++ /dev/null	1 Jan 1970 00:00:00 -0000
@@ -1,243 +0,0 @@
-<?php
-// $Id: password.inc,v 1.6 2009/02/26 07:30:26 webchick Exp $
-
-/**
- * @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
- * 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.
- */
-define('DRUPAL_HASH_COUNT', 14);
-
-/**
- * The minimum allowed log2 number of iterations for password stretching.
- */
-define('DRUPAL_MIN_HASH_COUNT', 7);
-
-/**
- * The maximum allowed log2 number of iterations for password stretching.
- */
-define('DRUPAL_MAX_HASH_COUNT', 30);
-
-/**
- * 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
- */
-function _password_base64_encode($input, $count)  {
-  $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) {
-  $output = '$P$';
-  // 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.
- *
- * @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)  {
-  // The first 12 characters of an existing hash are its setting string.
-  $setting = substr($setting, 0, 12);
-
-  if (substr($setting, 0, 3) != '$P$') {
-    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;
-  }
-
-  // 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);
-
-  $output =  $setting . _password_base64_encode($hash, 16);
-  // _password_base64_encode() of a 16 byte MD5 will always be 22 characters.
-  return (strlen($output) == 34) ? $output : FALSE;
-}
-
-/**
- * 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);
-  }
-  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$P') {
-    // 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) {
-  // Check whether this was an updated password.
-  if ((substr($account->pass, 0, 3) != '$P$') || (strlen($account->pass) != 34)) {
-    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));
-}
-
Index: modules/dblog/dblog.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/dblog/dblog.test,v
retrieving revision 1.28
diff -u -p -r1.28 dblog.test
--- modules/dblog/dblog.test	22 Aug 2009 00:58:52 -0000	1.28
+++ modules/dblog/dblog.test	25 Aug 2009 00:25:11 -0000
@@ -182,7 +182,7 @@ class DBLogTestCase extends DrupalWebTes
   private function doUser() {
     // Set user variables.
     $name = $this->randomName();
-    $pass = user_password();
+    $pass = user_generate_password();
     // Add user using form to generate add user event (which is not triggered by drupalCreateUser).
     $edit = array();
     $edit['name'] = $name;
Index: modules/openid/openid.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/openid/openid.module,v
retrieving revision 1.56
diff -u -p -r1.56 openid.module
--- modules/openid/openid.module	24 Aug 2009 00:14:21 -0000	1.56
+++ modules/openid/openid.module	25 Aug 2009 00:25:11 -0000
@@ -137,7 +137,7 @@ function openid_form_user_register_alter
     // with random password to avoid confusion.
     if (!variable_get('user_email_verification', TRUE)) {
       $form['pass']['#type'] = 'hidden';
-      $form['pass']['#value'] = user_password();
+      $form['pass']['#value'] = user_generate_password();
     }
     $form['auth_openid'] = array('#type' => 'hidden', '#value' => $_SESSION['openid']['values']['auth_openid']);
   }
@@ -430,7 +430,7 @@ function openid_authentication($response
     $form_state['redirect'] = NULL;
     $form_state['values']['name'] = (empty($response['openid.sreg.nickname'])) ? $identity : $response['openid.sreg.nickname'];
     $form_state['values']['mail'] = (empty($response['openid.sreg.email'])) ? '' : $response['openid.sreg.email'];
-    $form_state['values']['pass']  = user_password();
+    $form_state['values']['pass']  = user_generate_password();
     $form_state['values']['status'] = variable_get('user_register', 1) == 1;
     $form_state['values']['response'] = $response;
     $form = drupal_retrieve_form('user_register', $form_state);
Index: modules/simpletest/drupal_web_test_case.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v
retrieving revision 1.144
diff -u -p -r1.144 drupal_web_test_case.php
--- modules/simpletest/drupal_web_test_case.php	24 Aug 2009 00:14:21 -0000	1.144
+++ modules/simpletest/drupal_web_test_case.php	25 Aug 2009 00:25:11 -0000
@@ -861,7 +861,7 @@ class DrupalWebTestCase extends DrupalTe
     $edit['name']   = $this->randomName();
     $edit['mail']   = $edit['name'] . '@example.com';
     $edit['roles']  = array($rid => $rid);
-    $edit['pass']   = user_password();
+    $edit['pass']   = user_generate_password();
     $edit['status'] = 1;
 
     $account = user_save('', $edit);
Index: modules/user/user.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.info,v
retrieving revision 1.12
diff -u -p -r1.12 user.info
--- modules/user/user.info	19 Aug 2009 20:19:37 -0000	1.12
+++ modules/user/user.info	25 Aug 2009 00:25:12 -0000
@@ -8,6 +8,7 @@ files[] = user.module
 files[] = user.admin.inc
 files[] = user.pages.inc
 files[] = user.install
+files[] = user.password.inc
 files[] = user.test
 files[] = user.tokens.inc
 required = TRUE
Index: modules/user/user.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.install,v
retrieving revision 1.28
diff -u -p -r1.28 user.install
--- modules/user/user.install	21 Aug 2009 14:28:52 -0000	1.28
+++ modules/user/user.install	25 Aug 2009 00:25:12 -0000
@@ -256,8 +256,7 @@ function user_schema() {
  */
 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' => ''));
@@ -265,15 +264,16 @@ function user_update_7000(&$sandbox) {
     $sandbox['user_count'] = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
   }
   else {
-    require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
-    //  Hash again all current hashed passwords.
+    // Hash again all current hashed passwords at lower than the default
+    // strength to make the update run at a better speed.
+    user_authenticator()->setPasswordStorageStrength(0.1);
     $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", array(), $sandbox['user_from'], $count);
     foreach ($result as $account) {
       $has_rows = TRUE;
-      $new_hash = user_hash_password($account->pass, $hash_count_log2);
+      $new_hash = user_authenticator()->hashPassword($account['pass']);
       if ($new_hash) {
         // Indicate an updated password.
         $new_hash  = 'U' . $new_hash;
@@ -287,7 +287,7 @@ function user_update_7000(&$sandbox) {
     $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");
+      $ret[] = array('success' => TRUE, 'query' => "UPDATE {users} SET pass = 'U' . user_authenticator()->hashPassword(pass) WHERE uid > 0");
     }
   }
   return $ret;
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.1030
diff -u -p -r1.1030 user.module
--- modules/user/user.module	24 Aug 2009 00:14:23 -0000	1.1030
+++ modules/user/user.module	25 Aug 2009 00:25:12 -0000
@@ -375,19 +375,15 @@ function user_save($account, $edit = arr
   $user_fields = $table['fields'];
 
   if (!empty($edit['pass'])) {
-    // Allow alternate password hashing schemes.
-    require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
-    $edit['pass'] = user_hash_password(trim($edit['pass']));
-    // Abort if the hashing failed and returned FALSE.
-    if (!$edit['pass']) {
-      return FALSE;
-    }
+    // Retain the raw password to call the update method later.
+    $raw_password = trim($edit['pass']);
+    // Generate a hashed password to be saved into the database.
+    $edit['pass'] = user_authenticator()->hashPassword($raw_password);
   }
   else {
     // Avoid overwriting an existing password with a blank password.
     unset($edit['pass']);
   }
-
   // Get the fields form so we can recognize the fields in the $edit
   // form that should not go into the serialized data array.
   $field_form = array();
@@ -411,6 +407,7 @@ function user_save($account, $edit = arr
     if (empty($edit['access']) && empty($account->access) && user_access('administer users')) {
       $edit['access'] = REQUEST_TIME;
     }
+
     foreach ($edit as $key => $value) {
       // Form fields that don't pertain to the users, user_roles, or
       // Field API are automatically serialized into the users.data
@@ -492,15 +489,6 @@ function user_save($account, $edit = arr
       drupal_session_destroy_uid($account->uid);
     }
 
-    // If the password changed, delete all open sessions and recreate
-    // the current one.
-    if (!empty($edit['pass'])) {
-      drupal_session_destroy_uid($account->uid);
-      if ($account->uid == $GLOBALS['user']->uid) {
-        drupal_session_regenerate();
-      }
-    }
-
     // Save Field data.
     $object = (object) $edit;
     field_attach_update('user', $object);
@@ -508,6 +496,15 @@ function user_save($account, $edit = arr
     // Refresh user object.
     $user = user_load($account->uid, TRUE);
 
+    // If the password changed, delete all open sessions and recreate
+    // the current one.
+    if (!empty($raw_password)) {
+      user_authenticator()->updatePassword($raw_password, $user);
+      drupal_session_destroy_uid($account->uid);
+      if ($account->uid == $GLOBALS['user']->uid) {
+        drupal_session_regenerate();
+      }
+    }
     // Send emails after we have the new user object.
     if (isset($edit['status']) && $edit['status'] != $account->status) {
       // The user's status is changing; conditionally send notification email.
@@ -579,6 +576,8 @@ function user_save($account, $edit = arr
 
     // Build the finished user object.
     $user = user_load($edit['uid'], TRUE);
+    // Update the password using the final user object.
+    user_authenticator()->updatePassword($raw_password, $user);
   }
 
   return $user;
@@ -651,7 +650,7 @@ function user_validate_picture(&$form, &
 /**
  * Generate a random alphanumeric password.
  */
-function user_password($length = 10) {
+function user_generate_password($length = 10) {
   // This variable contains the list of allowable characters for the
   // password. Note that the number 0 and the letter 'O' have been
   // removed to avoid confusion between the two. The same is true
@@ -1775,22 +1774,9 @@ function user_authenticate($name, $passw
   if (!empty($name) && !empty($password)) {
     $account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $name))->fetchObject();
     if ($account) {
-      // Allow alternate password hashing schemes.
-      require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
-      if (user_check_password($password, $account)) {
+      if (user_authenticator()->checkPassword($password, $account)) {
         // Successful authentication.
         $uid = $account->uid;
-
-        // Update user to new password scheme if needed.
-        if (user_needs_new_hash($account)) {
-          $new_hash = user_hash_password($password);
-          if ($new_hash) {
-            db_update('users')
-              ->fields(array('pass' => $new_hash))
-              ->condition('uid', $account->uid)
-              ->execute();
-          }
-        }
       }
     }
   }
@@ -1846,7 +1832,7 @@ function user_external_login_register($n
     // Register this new user.
     $userinfo = array(
       'name' => $name,
-      'pass' => user_password(),
+      'pass' => user_generate_password(),
       'init' => $name,
       'status' => 1,
       'access' => REQUEST_TIME
@@ -2879,7 +2865,7 @@ function user_register_submit($form, &$f
     $pass = $form_state['values']['pass'];
   }
   else {
-    $pass = user_password();
+    $pass = user_generate_password();
   };
   $notify = isset($form_state['values']['notify']) ? $form_state['values']['notify'] : NULL;
   $from = variable_get('site_mail', ini_get('sendmail_from'));
@@ -3039,6 +3025,81 @@ function _user_forms(&$edit, $account, $
   return empty($groups) ? FALSE : $groups;
 }
 
+/**
+ * Returns an object that implements UserAuthenticationInterface.
+ */
+function user_authenticator() {
+  $instance = &drupal_static(__FUNCTION__);
+
+  if (empty($instance)) {
+    $class = variable_get('authentication_system', 'UserPassword');
+    $instance = new $class;
+    // Set a default password storage (hash) strength.
+    $instance->setPasswordStorageStrength(variable_get('password_storage_strength', 1.0));
+  }
+  return $instance;
+}
+
+/**
+ * An interface for authenticating users with passwords, and for updating passwords.
+ */
+interface UserAuthenticationInterface {
+  /**
+   * Set the relative strength for password storage as compared to the default.
+   * 
+   * This method may have no effect if the passwords are stored remotely or
+   * using a fixed hashing or cryptographic algorithm.
+   *
+   * @param $strength
+   *   A positive number; 1.0 for the default. Typical range is 0.01 - 1000.0.
+   */
+  public function setPasswordStorageStrength($strength);
+
+  /**
+   * Hash a password using a secure hash.
+   *
+   * This method is used when saving a user to the database.  If the
+   * implementation does not wish to return its hash value a non-empty string
+   * with a fixed value (e.g. the class name) must be returned.
+   *
+   * @param $password
+   *   A plain-text password.
+   *
+   * @return
+   *   A string containing the hashed password, or FALSE on failure.
+   */
+  public function hashPassword($password);
+
+  /**
+   * Check whether a plain text password matches the user's stored password.
+   *
+   * @param $password
+   *   A plain-text password
+   * @param $account
+   *   A user object with at least the fields from the {users} table.
+   *
+   * @return
+   *   TRUE if it matches or FALSE otherwise.
+   */
+  public function checkPassword($password, $account);
+
+  /**
+   * Update a user's password with a new password.
+   *
+   * This method may have no effect if the passwords are stored remotely or
+   * managed outside of Drupal. If updates are not possible or the operation
+   * fails, the method must return FALSE.
+   *
+   * @param $password
+   *   A plain-text password
+   * @param $account
+   *   A user object with at least the fields from the {users} table.
+   *
+   * @return
+   *   TRUE or FALSE.
+   */
+  public function updatePassword($password, $account);
+}
 
 /**
  * Implementation of hook_modules_installed().
Index: modules/user/user.password.inc
===================================================================
RCS file: modules/user/user.password.inc
diff -N modules/user/user.password.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/user/user.password.inc	25 Aug 2009 00:25:12 -0000
@@ -0,0 +1,269 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Secure password hashing functions for user authentication.
+ *
+ * This is the default Drupal implementation of the UserAuthenticationInterface.
+ *
+ * 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 authenticator_system to the name of a class
+ * that implements interface AuthenticatorInterface, with hashPassword(),
+ * checkPassword(), needsNewHash(), and setHashStrength() methods.
+ */
+class UserPassword implements UserAuthenticationInterface {
+  /**
+   * 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.
+   */
+  const DEFAULT_HASH_COUNT = 14;
+
+  /**
+   * The minimum allowed log2 number of iterations for password stretching.
+   */
+  const MIN_HASH_COUNT = 7;
+
+  /**
+   * The maximum allowed log2 number of iterations for password stretching.
+   */
+  const MAX_HASH_COUNT = 30;
+
+  /**
+   * Returns a string for mapping an int to the corresponding base 64 character.
+   */
+  protected $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+  /**
+   * The log2 number of iterations to use when generating a hash.
+   */
+  protected $countLog2Setting = self::DEFAULT_HASH_COUNT;
+
+  /**
+   * 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
+   */
+  protected function base64Encode($input, $count)  {
+    $output = '';
+    $i = 0;
+    do {
+      $value = ord($input[$i++]);
+      $output .= $this->itoa64[$value & 0x3f];
+      if ($i < $count) {
+        $value |= ord($input[$i]) << 8;
+      }
+      $output .= $this->itoa64[($value >> 6) & 0x3f];
+      if ($i++ >= $count) {
+        break;
+      }
+      if ($i < $count) {
+        $value |= ord($input[$i]) << 16;
+      }
+      $output .= $this->itoa64[($value >> 12) & 0x3f];
+      if ($i++ >= $count) {
+        break;
+      }
+      $output .= $this->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.
+   *
+   * @return
+   *   A 12 character string containing the iteration count and a random salt.
+   */
+  protected function generateSalt() {
+    // $P$ is the hash identifier for phpass.
+    $output = '$P$';
+    // Minimum log2 iterations is DRUPAL_MIN_HASH_COUNT.
+    $count_log2 = max($this->countLog2Setting, self::MIN_HASH_COUNT);
+    // Maximum log2 iterations is DRUPAL_MAX_HASH_COUNT.
+    // We encode the final log2 iteration count in base 64.
+    $output .= $this->itoa64[min($count_log2, self::MAX_HASH_COUNT)];
+    // 6 bytes is the standard salt for a portable phpass hash.
+    $output .= $this->base64Encode(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.
+   *
+   * @param $password
+   *   The plain-text password to hash.
+   * @param $setting
+   *   An existing hash or the output of generate_salt().
+   *
+   * @return
+   *   A string containing the hashed password (and salt) or FALSE on failure.
+   */
+  protected function passwordCrypt($password, $setting)  {
+    // The first 12 characters of an existing hash are its setting string.
+    $setting = substr($setting, 0, 12);
+
+    if (substr($setting, 0, 3) != '$P$') {
+      return FALSE;
+    }
+    $count_log2 = $this->getCountLog2($setting);
+    // Hashes may be imported from elsewhere, so we allow != DEFAULT_HASH_COUNT
+    if ($count_log2 < self::MIN_HASH_COUNT || $count_log2 > self::MAX_HASH_COUNT) {
+      return FALSE;
+    }
+    $salt = substr($setting, 4, 8);
+    // Hashes must have 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);
+
+    $output =  $setting . $this->base64Encode($hash, 16);
+    // this->base64Encode() of a 16 byte MD5 will always be 22 characters, plus
+    // 12 characters of base64 salt is 34 bytes total.
+    return (strlen($output) == 34) ? $output : FALSE;
+  }
+
+  /**
+   * Parse the log2 iteration count from a stored hash or setting string.
+   */
+  protected function getCountLog2($setting) {
+    return strpos($this->itoa64, $setting[3]);
+  }
+
+  /**
+   * Set the relative strength for password storage as compared to the default.
+   *
+   * @param $strength
+   *   A positive number; 1.0 for the default. Typical range is 0.01 - 1000.0.
+   */
+  public function setPasswordStorageStrength($strength) {
+    // We accept a linear scale strength and convert it to a base 2 logarithm.
+    $this->countLog2Setting = (int)(log($strength, 2) + self::DEFAULT_HASH_COUNT);
+  }
+
+  /**
+   * Hash a password using a secure hash.
+   *
+   * @param $password
+   *   A plain-text password string.
+   *
+   * @return
+   *   A string containing the hashed password (and a salt).
+   */
+  public function hashPassword($password) {
+    $hash = $this->passwordCrypt($password, $this->generateSalt());
+    if ($hash === FALSE) {
+      throw new Exception('Unable to generate a hash.');
+    }
+    return $hash;
+  }
+
+  /**
+   * 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.
+   */
+  public function checkPassword($password, $account) {
+    $raw_password = $password;
+    if (substr($account->pass, 0, 3) == 'U$P') {
+      // 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 = $this->passwordCrypt($password, $stored_hash);
+    if ($hash && $stored_hash == $hash) {
+      if ($this->getCountLog2($account->pass) != $this->countLog2Setting) {
+        $this->updatePassword($raw_password, $account);
+      }
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Update a user's password with a new password.
+   *
+   * @param $password
+   *   A plain-text password
+   * @param $account
+   *   A user object with at least the fields from the {users} table.
+   *
+   * @return
+   *   TRUE on success or FALSE otherwise.
+   */
+  public function updatePassword($password, $account) {
+    $new_hash = $this->passwordCrypt($password, $this->generateSalt());
+    if ($new_hash) {
+      db_update('users')
+        ->fields(array('pass' => $new_hash))
+        ->condition('uid', $account->uid)
+        ->execute();
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Update a user's password hash if it needs to be replaced with a new hash.
+   */
+  protected function updateHash($password, $account) {
+    $update = FALSE;
+    // Check whether this was an updated password.
+    if ((substr($account->pass, 0, 3) != '$P$') || (strlen($account->pass) != 34)) {
+      $update = TRUE;
+    }
+    // Check whether the iteration count used differs from the standard number.
+    if ($this->getCountLog2($account->pass) != $this->countLog2Setting) {
+      $update = TRUE;
+    }
+    if ($update) {
+      $new_hash = $this->updatePassword($password, $account);
+    }
+  }
+}
+
Index: modules/user/user.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.test,v
retrieving revision 1.56
diff -u -p -r1.56 user.test
--- modules/user/user.test	21 Aug 2009 14:27:47 -0000	1.56
+++ modules/user/user.test	25 Aug 2009 00:25:12 -0000
@@ -31,22 +31,22 @@ class UserRegistrationTestCase extends D
     $this->assertText(t('Your password and further instructions have been sent to your e-mail address.'), t('User registered successfully.'));
 
     // Check database for created user.
-    $users = user_load_multiple(array(), array('name' => $name, 'mail' => $mail));
-    $user = reset($users);
-    $this->assertTrue($user, t('User found in database.'));
-    $this->assertTrue($user->uid > 0, t('User has valid user id.'));
+    $accounts = user_load_multiple(array(), array('name' => $name, 'mail' => $mail));
+    $account = reset($accounts);
+    $this->assertTrue($account, t('User found in database.'));
+    $this->assertTrue($account->uid > 0, t('User has valid user id.'));
 
     // Check user fields.
-    $this->assertEqual($user->name, $name, t('Username matches.'));
-    $this->assertEqual($user->mail, $mail, t('E-mail address matches.'));
-    $this->assertEqual($user->theme, '', t('Correct theme field.'));
-    $this->assertEqual($user->signature, '', t('Correct signature field.'));
-    $this->assertTrue(($user->created > REQUEST_TIME - 20 ), t('Correct creation time.'));
-    $this->assertEqual($user->status, variable_get('user_register', 1) == 1 ? 1 : 0, t('Correct status field.'));
-    $this->assertEqual($user->timezone, variable_get('date_default_timezone'), t('Correct time zone field.'));
-    $this->assertEqual($user->language, '', t('Correct language field.'));
-    $this->assertEqual($user->picture, '', t('Correct picture field.'));
-    $this->assertEqual($user->init, $mail, t('Correct init field.'));
+    $this->assertEqual($account->name, $name, t('Username matches.'));
+    $this->assertEqual($account->mail, $mail, t('E-mail address matches.'));
+    $this->assertEqual($account->theme, '', t('Correct theme field.'));
+    $this->assertEqual($account->signature, '', t('Correct signature field.'));
+    $this->assertTrue(($account->created > REQUEST_TIME - 20 ), t('Correct creation time.'));
+    $this->assertEqual($account->status, variable_get('user_register', 1) == 1 ? 1 : 0, t('Correct status field.'));
+    $this->assertEqual($account->timezone, variable_get('date_default_timezone'), t('Correct time zone field.'));
+    $this->assertEqual($account->language, '', t('Correct language field.'));
+    $this->assertEqual($account->picture, '', t('Correct picture field.'));
+    $this->assertEqual($account->init, $mail, t('Correct init field.'));
 
     // Attempt to login with incorrect password.
     $edit = array();
@@ -56,7 +56,7 @@ class UserRegistrationTestCase extends D
     $this->assertText(t('Sorry, unrecognized username or password. Have you forgotten your password?'), t('Invalid login attempt failed.'));
 
     // Login using password reset page.
-    $url = user_pass_reset_url($user);
+    $url = user_pass_reset_url($account);
     $this->drupalGet($url);
     $this->assertText(t('This login can be used only once.'), t('Login can be used only once.'));
 
@@ -72,36 +72,33 @@ class UserRegistrationTestCase extends D
     $this->assertNoText(t('The changes have been saved.'), t('Save user password with mismatched type in password confirm.'));
 
     // Change user password.
-    $new_pass = user_password();
+    $new_pass = user_generate_password();
     $edit = array();
     $edit['pass[pass1]'] = $new_pass;
     $edit['pass[pass2]'] = $new_pass;
     $this->drupalPost(NULL, $edit, t('Save'));
     $this->assertText(t('The changes have been saved.'), t('Password changed to @password', array('@password' => $new_pass)));
 
-    // Make sure password changes are present in database.
-    require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
-
-    $user = user_load($user->uid, TRUE);
-    $this->assertTrue(user_check_password($new_pass, $user), t('Correct password in database.'));
+    $account = user_load($account->uid, TRUE);
+    $this->assertTrue(user_authenticator()->checkPassword($new_pass, $account), t('Correct password in database.'));
 
     // Logout of user account.
     $this->clickLink(t('Log out'));
-    $this->assertNoText($user->name, t('Logged out.'));
+    $this->assertNoText($account->name, t('Logged out.'));
 
     // Login user.
     $edit = array();
-    $edit['name'] = $user->name;
+    $edit['name'] = $account->name;
     $edit['pass'] = $new_pass;
     $this->drupalPost('user', $edit, t('Log in'));
     $this->assertText(t('Log out'), t('Logged in.'));
 
-    $this->assertText($user->name, t('[logged in] Username found.'));
+    $this->assertText($account->name, t('[logged in] Username found.'));
     $this->assertNoText(t('Sorry. Unrecognized username or password.'), t('[logged in] No message for unrecognized username or password.'));
     $this->assertNoText(t('User login'), t('[logged in] No user login form present.'));
 
     $this->drupalGet('user');
-    $this->assertText($user->name, t('[user auth] Not login page.'));
+    $this->assertText($account->name, t('[user auth] Not login page.'));
     $this->assertText(t('View'), t('[user auth] Found view tab on the profile page.'));
     $this->assertText(t('Edit'), t('[user auth] Found edit tab on the profile page.'));
   }
Index: scripts/password-hash.sh
===================================================================
RCS file: /cvs/drupal/drupal/scripts/password-hash.sh,v
retrieving revision 1.6
diff -u -p -r1.6 password-hash.sh
--- scripts/password-hash.sh	26 Feb 2009 07:30:29 -0000	1.6
+++ scripts/password-hash.sh	25 Aug 2009 00:25:12 -0000
@@ -85,11 +85,12 @@ while ($param = array_shift($_SERVER['ar
 
 define('DRUPAL_ROOT', getcwd());
 
-include_once DRUPAL_ROOT . '/includes/password.inc';
+include_once DRUPAL_ROOT . '/modules/user/user.module';
+include_once DRUPAL_ROOT . '/modules/user/user.password.inc';
 include_once DRUPAL_ROOT . '/includes/common.inc';
 
 foreach ($passwords as $password) {
-  print("\npassword: $password \t\thash: ". user_hash_password($password) ."\n");
+  print("\npassword: $password \t\thash: ". user_authenticator()->hashPassword($password) ."\n");
 }
 print("\n");
 
