Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.130
diff -u -u -p -r1.130 system.install
--- modules/system/system.install	5 Jul 2007 08:48:58 -0000	1.130
+++ modules/system/system.install	7 Jul 2007 23:16:03 -0000
@@ -3439,6 +3439,14 @@ function system_update_6025() {
   return $ret;
 }
 
+function system_update_6026() {
+  $ret = array();
+
+  // Increase the users table pass field to accomodate larger secure hashes
+  db_update_field($ret, 'users', 'pass');
+
+  return $ret;
+}
 
 /**
  * @} End of "defgroup updates-5.x-to-6.x"
Index: modules/user/passhash.inc
===================================================================
RCS file: modules/user/passhash.inc
diff -N modules/user/passhash.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/user/passhash.inc	7 Jul 2007 23:16:03 -0000
@@ -0,0 +1,290 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Password hashing framework based on http://www.openwall.com/phpass/
+ */
+
+/**
+ * Hash a password using the strongest available hashing method, unless the
+ * use of portable hashes is forced.
+ * 
+ * The preferred (most secure) hashing method supported by passhash is the
+ * OpenBSD-style Blowfish-based bcrypt, also supported with the crypt_blowfish
+ * package (for C applications), and known in PHP as CRYPT_BLOWFISH, with a
+ * fallback to BSDI-style extended DES-based hashes, known in PHP as
+ * CRYPT_EXT_DES, and a last resort fallback to an MD5-based variable
+ * iteration count password hashing method implemented in passhash itself.
+ * 
+ * @param $password
+ *   The password to hash
+ * @param $iteration_count_log2
+ *   This affects the number of iterations used in the hashing process.
+ *   A larger value is more secure, but takes more time to complete.
+ * @param $portable_hashes
+ *   If set to TRUE, the hashes generated might be significantly weaker,
+ *   but they will be guaranteed to be portable across servers
+ * 
+ * @return
+ *   The hashed password
+ */
+function passhash_hash_password($password, $iteration_count_log2 = 8, $portable_hashes = FALSE) {
+  if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) {
+    $iteration_count_log2 = 8;
+  }
+
+  $random = '';
+
+  if (CRYPT_BLOWFISH == 1 && !$portable_hashes) {
+    $random = _passhash_get_random_bytes(16);
+    $hash = crypt($password, _passhash_gensalt_blowfish($random, $iteration_count_log2));
+    if (strlen($hash) == 60) {
+      return $hash;
+    }
+  }
+
+  if (CRYPT_EXT_DES == 1 && !$portable_hashes) {
+    if (strlen($random) < 3) {
+      $random = _passhash_get_random_bytes(3);
+    }
+    $hash = crypt($password, _passhash_gensalt_extended($random, $iteration_count_log2));
+    if (strlen($hash) == 20) {
+      return $hash;
+    }
+  }
+
+  if (strlen($random) < 6) {
+    $random = _passhash_get_random_bytes(6);
+  }
+  $hash = _passhash_crypt_private($password, _passhash_gensalt_private($random, $iteration_count_log2));
+  if (strlen($hash) == 34) {
+    return $hash;
+  }
+
+  // Returning '*' on error is safe here, but would _not_ be safe
+  // in a crypt(3)-like function used _both_ for generating new
+  // hashes and for validating passwords against existing hashes.
+  return '*';
+}
+
+/**
+ * Check a password hash.
+ * 
+ * @param $password
+ *   The password to check
+ * @param $stored_hash
+ *   The stored hash to check against
+ * 
+ * @return
+ *   TRUE if the password matches the hash, FALSE if it doesn't 
+ */
+function passhash_check_password($password, $stored_hash) {
+  $hash = _passhash_crypt_private($password, $stored_hash);
+  if ($hash[0] == '*') {
+    $hash = crypt($password, $stored_hash);
+  }
+
+  return $hash == $stored_hash;
+}
+
+/**
+ * Returns the character set used in the hash
+ */
+function _passhash_itoa64() {
+  return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+}
+
+/**
+ * Returns a string of highly randomized characters (over the full 8-bit range)
+ * 
+ * @param $count
+ *   The number of characters to return
+ */
+function _passhash_get_random_bytes($count) {
+  $output = '';
+  if (($fh = @fopen('/dev/urandom', 'rb'))) {
+    $output = fread($fh, $count);
+    fclose($fh);
+  }
+
+  if (strlen($output) < $count) {
+    $output = '';
+    $random_state = microtime() . getmypid();
+    for ($i = 0; $i < $count; $i += 16) {
+      $random_state = md5(microtime() . $random_state);
+      $output .= pack('H*', md5($random_state));
+    }
+    $output = substr($output, 0, $count);
+  }
+
+  return $output;
+}
+
+/**
+ * Encodes a string into a predefined character set, up to a set number of
+ * characters
+ * 
+ * @param $input
+ *   The string to encode
+ * @param $count
+ *   The number of characters to encode
+ * @return
+ *   Encoded string 
+ */
+function _passhash_encode64($input, $count) {
+  $output = '';
+  $i = 0;
+  $itoa64 = _passhash_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;
+}
+
+/**
+ * Please do not change the "private" password hashing method implemented in
+ * here, thereby making your hashes incompatible.  However, if you must, please
+ * change the hash type identifier (the "$P$") to something different.
+ */
+
+/**
+ * Generates an encoded salt string for use with _passhash_crypt_private()
+ */
+function _passhash_gensalt_private($input, $iteration_count_log2) {
+  $output = '$P$';
+  $itoa64 = _passhash_itoa64();
+  $output .= $itoa64[min($iteration_count_log2 + ((PHP_VERSION >= '5') ? 5 : 3), 30)];
+  $output .= _passhash_encode64($input, 6);
+
+  return $output;
+}
+
+/**
+ * Computes a portable password hash (fallback when CRYPT_BLOWFISH and
+ * CRYPT_EXT_DES are not available or portable hashes are forced)
+ */
+function _passhash_crypt_private($password, $setting) {
+  $output = '*0';
+  if (substr($setting, 0, 2) == $output) {
+    $output = '*1';
+  }
+
+  if (substr($setting, 0, 3) != '$P$') {
+    return $output;
+  }
+
+  $count_log2 = strpos(_passhash_itoa64(), $setting[3]);
+  if ($count_log2 < 7 || $count_log2 > 30) {
+    return $output;
+  }
+
+  $count = 1 << $count_log2;
+
+  $salt = substr($setting, 4, 8);
+  if (strlen($salt) != 8) {
+    return $output;
+  }
+
+  // Use MD5 here since it's the only cryptographic primitive available in all
+  // versions of PHP currently in use.  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).
+  if (PHP_VERSION >= '5') {
+    $hash = md5($salt . $password, TRUE);
+    do {
+      $hash = md5($hash . $password, TRUE);
+    } while (--$count);
+  } else {
+    $hash = pack('H*', md5($salt . $password));
+    do {
+      $hash = pack('H*', md5($hash . $password));
+    } while (--$count);
+  }
+
+  $output = substr($setting, 0, 12);
+  $output .= _passhash_encode64($hash, 16);
+
+  return $output;
+}
+
+/**
+ * Generates an encoded salt string for use with CRYPT_EXT_DES crypt()
+ */
+function _passhash_gensalt_extended($input, $iteration_count_log2) {
+  $count_log2 = min($iteration_count_log2 + 8, 24);
+  // This should be odd to not reveal weak DES keys, and the
+  // maximum valid value is (2**24 - 1) which is odd anyway.
+  $count = (1 << $count_log2) - 1;
+
+  $itoa64 = _passhash_itoa64();
+  $output = '_';
+  $output .= $itoa64[$count & 0x3f];
+  $output .= $itoa64[($count >> 6) & 0x3f];
+  $output .= $itoa64[($count >> 12) & 0x3f];
+  $output .= $itoa64[($count >> 18) & 0x3f];
+
+  $output .= _passhash_encode64($input, 3);
+
+  return $output;
+}
+
+/**
+ * Generates an encoded salt string for use with CRYPT_BLOWFISH crypt()
+ */
+function _passhash_gensalt_blowfish($input, $iteration_count_log2) {
+  // This one needs to use a different order of characters and a
+  // different encoding scheme from the one in _passhash_encode64().
+  // We care because the last character in our encoded string will
+  // only represent 2 bits.  While two known implementations of
+  // bcrypt will happily accept and correct a salt string that
+  // has the 4 unused bits set to non-zero, we do not want to take
+  // chances and we also do not want to waste an additional byte
+  // of entropy.
+  $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+  $output = '$2a$';
+  $output .= chr(ord('0') + $iteration_count_log2 / 10);
+  $output .= chr(ord('0') + $iteration_count_log2 % 10);
+  $output .= '$';
+
+  $i = 0;
+  do {
+    $c1 = ord($input[$i++]);
+    $output .= $itoa64[$c1 >> 2];
+    $c1 = ($c1 & 0x03) << 4;
+    if ($i >= 16) {
+      $output .= $itoa64[$c1];
+      break;
+    }
+
+    $c2 = ord($input[$i++]);
+    $c1 |= $c2 >> 4;
+    $output .= $itoa64[$c1];
+    $c1 = ($c2 & 0x0f) << 2;
+
+    $c2 = ord($input[$i++]);
+    $c1 |= $c2 >> 6;
+    $output .= $itoa64[$c1];
+    $output .= $itoa64[$c2 & 0x3f];
+  } while (1);
+
+  return $output;
+}
Index: modules/user/user.js
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.js,v
retrieving revision 1.3
diff -u -u -p -r1.3 user.js
--- modules/user/user.js	1 Jul 2007 15:37:10 -0000	1.3
+++ modules/user/user.js	7 Jul 2007 23:16:03 -0000
@@ -176,5 +176,9 @@ Drupal.behaviors.userSettings = function
   $('div.user-admin-picture-radios input[@type=radio]:not(.userSettings-processed)', context).addClass('userSettings-processed').click(function () {
     $('div.user-admin-picture-settings', context)[['hide', 'show'][this.value]]();
   });
+  $('div.user-admin-hash-radios input[@type=radio]:not(.userSettings-processed)', context).addClass('userSettings-processed').click(function () {
+    value = this.value == 'passhash' ? 1 : 0;
+    $('div.user-admin-hash-settings', context)[['hide', 'show'][value]]();
+  });
 };
 
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.818
diff -u -u -p -r1.818 user.module
--- modules/user/user.module	5 Jul 2007 08:48:58 -0000	1.818
+++ modules/user/user.module	7 Jul 2007 23:16:03 -0000
@@ -139,10 +139,6 @@ function user_load($array = array()) {
       $query[] = "$key = %d";
       $params[] = $value;
     }
-    else if ($key == 'pass') {
-      $query[] = "pass = '%s'";
-      $params[] = md5($value);
-    }
     else {
       $query[]= "LOWER($key) = LOWER('%s')";
       $params[] = $value;
@@ -203,7 +199,7 @@ function user_save($account, $array = ar
     foreach ($array as $key => $value) {
       if ($key == 'pass' && !empty($value)) {
         $query .= "$key = '%s', ";
-        $v[] = md5($value);
+        $v[] = _user_get_hash($value);
       }
       else if ((substr($key, 0, 4) !== 'auth') && ($key != 'pass')) {
         if (in_array($key, $user_fields)) {
@@ -266,7 +262,7 @@ function user_save($account, $array = ar
       switch ($key) {
         case 'pass':
           $fields[] = $key;
-          $values[] = md5($value);
+          $values[] = _user_get_hash($value);
           $s[] = "'%s'";
           break;
         case 'mode':     case 'sort':
@@ -1124,7 +1120,13 @@ function user_login_authenticate_validat
  **/
 function user_login_final_validate($form, &$form_state) {
   global $user;
-  if (!$user->uid) {
+  if ($user->uid) {
+    // replace wrongly hashed passwords with the selected hashing method
+    if ((variable_get('user_hash_method', 'passhash') == 'passhash') != ($user->pass[0] == '$' || $user->pass[0] == '_')) {
+      user_save($user, array('pass' => trim($form_state['values']['pass'])));
+    }
+  }
+  else {
     form_set_error('name', t('Sorry, unrecognized username or password. <a href="@password">Have you forgotten your password?</a>', array('@password' => url('user/password'))));
     watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name']));
   }
@@ -1139,9 +1141,11 @@ function user_login_final_validate($form
 function user_authenticate($name, $pass) {
   global $user;
 
-  if ($account = user_load(array('name' => $name, 'pass' => $pass, 'status' => 1))) {
-    $user = $account;
-    return $user;
+  if ($account = user_load(array('name' => $name, 'status' => 1))) {
+    if (_user_valid_hash($pass, $account->pass)) {
+      $user = $account;
+      return $user;
+    }
   }
 }
 
@@ -2757,6 +2761,46 @@ function user_admin_settings() {
     '#default_value' => variable_get('user_picture_guidelines', ''),
     '#description' => t("This text is displayed at the picture upload form in addition to the default guidelines. It's useful for helping or instructing your users."),
   );
+  // let the admin select the password hash strength
+  $form['hash'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Password Hash'),
+  );
+  $user_hash_method = variable_get('user_hash_method', 'passhash');
+  $form['hash']['user_hash_method'] = array(
+    '#type' => 'radios',
+    '#title' => t('Hash Method'),
+    '#default_value' => $user_hash_method,
+    '#options' => array('md5' => t('original (MD5)'), 'passhash' => t('secure')),
+    '#description' => t('Select the method to hash the user passwords.  Use "secure" unless you really have a good reason.'),
+    '#prefix' => '<div class="user-admin-hash-radios">',
+    '#suffix' => '</div>',
+  );
+  // If JS is enabled, and the radio is defaulting to off, hide all
+  // the settings on page load via .css using the js-hide class so
+  // that there's no flicker.
+  $css_class = 'user-admin-hash-settings';
+  if ($user_hash_method != 'passhash') {
+    $css_class .= ' js-hide';
+  }
+  $form['hash']['settings'] = array(
+    '#prefix' => '<div class="'. $css_class .'">',
+    '#suffix' => '</div>',
+  );
+  $form['hash']['settings']['user_hash_strength'] = array(
+    '#type' => 'select',
+    '#title' => t('Hash Strength'),
+    '#default_value' => variable_get('user_hash_strength', 8),
+    '#options' => drupal_map_assoc(range(4, 12)),
+    '#description' => t('Select a higher number for a more secure but slower password hash.'),
+  );
+  $form['hash']['settings']['user_hash_portable'] = array(
+    '#type' => 'radios',
+    '#title' => t('Portable Hash'),
+    '#default_value' => variable_get('user_hash_portable', FALSE),
+    '#options' => array(FALSE => t('No'), TRUE => t('Yes')),
+    '#description' => t('Force the use of weaker hashes that are guaranteed to be portable across servers.'),
+  );
 
   return system_settings_form($form);
 }
@@ -3324,3 +3368,19 @@ function user_block_ip_action() {
   db_query("INSERT INTO {access} (mask, type, status) VALUES ('%s', '%s', %d)", $_SERVER['REMOTE_ADDR'], 'host', 0);
   watchdog('action', 'Banned IP address %ip', array('%ip' => $_SERVER['REMOTE_ADDR']));
 }
+
+function _user_get_hash($pass) {
+  include_once './'. drupal_get_path('module', 'user') .'/passhash.inc';
+  if (variable_get('user_hash_method', 'passhash') == 'passhash') {
+    return passhash_hash_password($pass, variable_get('user_hash_strength', 8), variable_get('user_hash_portable', FALSE));
+  }
+  return md5($pass);
+}
+
+function _user_valid_hash($pass, $hash) {
+  if ($hash[0] == '$' || $hash[0] == '_') {
+    include_once './'. drupal_get_path('module', 'user') .'/passhash.inc';
+    return passhash_check_password($pass, $hash);
+  }
+  return md5($pass) == $hash;
+}
Index: modules/user/user.schema
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.schema,v
retrieving revision 1.2
diff -u -u -p -r1.2 user.schema
--- modules/user/user.schema	15 Jun 2007 07:15:25 -0000	1.2
+++ modules/user/user.schema	7 Jul 2007 23:16:03 -0000
@@ -47,7 +47,7 @@ function user_schema() {
     'fields' => array(
       'uid'       => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
       'name'      => array('type' => 'varchar', 'length' => 60, 'not null' => TRUE, 'default' => ''),
-      'pass'      => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''),
+      'pass'      => array('type' => 'varchar', 'length' => 60, 'not null' => TRUE, 'default' => ''),
       'mail'      => array('type' => 'varchar', 'length' => 64, 'not null' => FALSE, 'default' => ''),
       'mode'      => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny'),
       'sort'      => array('type' => 'int', 'not null' => FALSE, 'default' => 0, 'size' => 'tiny'),
