From 41f67960fa4cc54b90074af6b981f505965e36c5 Mon Sep 17 00:00:00 2001
From: Mark Triggs <mark@dishevelled.net>
Date: Sun, 25 Mar 2012 13:24:37 +1100
Subject: [PATCH] Provide support for storing hashed passwords instead of plaintext.

  * Uses the phpass library (http://www.openwall.com/phpass/) to handle
    password hashing/salting and checking.

  * Modifies Account.php to delegate password assignment to the DB
    authentication handler.

  * Modified the DB authentication handler to check the hashed password if
    there is one, falling back to the stored plaintext password otherwise.

  * Provides a two step migration process to the new password scheme:

    - One step to activate hashed passwords for all existing (and future) users
      (upgrade/encrypted_passwords_activate.php)

    - One step to remove the old plaintext passwords from the database
      (upgrade/plaintext_passwords_deactivate.php)
---
 mysql.sql                                    |    1 +
 postgres.sql                                 |    1 +
 upgrade/encrypted_passwords_activate.php     |  115 ++++++++++++
 upgrade/encrypted_passwords_db_migration.php |  187 +++++++++++++++++++
 upgrade/plaintext_passwords_deactivate.php   |  147 +++++++++++++++
 web/conf/vufind.ini                          |    1 +
 web/services/MyResearch/Account.php          |    9 +-
 web/services/MyResearch/lib/User.php         |    2 +-
 web/sys/PasswordHash.php                     |  253 ++++++++++++++++++++++++++
 web/sys/authn/DatabaseAuthentication.php     |  109 ++++++++++-
 10 files changed, 814 insertions(+), 11 deletions(-)
 create mode 100644 upgrade/encrypted_passwords_activate.php
 create mode 100644 upgrade/encrypted_passwords_db_migration.php
 create mode 100644 upgrade/plaintext_passwords_deactivate.php
 create mode 100644 web/sys/PasswordHash.php

diff --git a/mysql.sql b/mysql.sql
index 91e72fc..8690ec9 100644
--- a/mysql.sql
+++ b/mysql.sql
@@ -100,6 +100,7 @@ CREATE TABLE `user` (
   `major` varchar(100) NOT NULL DEFAULT '',
   `home_library` varchar(100) NOT NULL DEFAULT '',
   `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+  `encrypted_password` varchar(60) DEFAULT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `username` (`username`)
 ) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=latin1;
diff --git a/postgres.sql b/postgres.sql
index 8dfcd6a..5e3a3d4 100644
--- a/postgres.sql
+++ b/postgres.sql
@@ -104,6 +104,7 @@ college varchar(100) NOT NULL DEFAULT '',
 major varchar(100) NOT NULL DEFAULT '',
 home_library varchar(100) NOT NULL DEFAULT '',
 created timestamp NOT NULL DEFAULT '1970-01-01 00:00:00',
+encrypted_password varchar(60) DEFAULT NULL,
 PRIMARY KEY (id),
 UNIQUE (username)
 ); 
diff --git a/upgrade/encrypted_passwords_activate.php b/upgrade/encrypted_passwords_activate.php
new file mode 100644
index 0000000..efb1aec
--- /dev/null
+++ b/upgrade/encrypted_passwords_activate.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Script to upgrade VuFind database to enable encrypted passwords.
+ *
+ * command line arguments:
+ *   * MySQL admin username
+ *   * MySQL admin password
+ *   * path to previous version installation
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2011.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind
+ * @package  Upgrade_Tools
+ * @author   Mark Triggs <mark@dishevelled.net>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/migration_notes Wiki
+ */
+
+$vufind_dir = isset($argv[1]) ? $argv[1] : false;
+
+if (!$vufind_dir) {
+    die("\nUsage: " . $argv[0] . " </path/to/vufind>\n\n");
+}
+
+
+// Try to read the ini file:
+$basePath = $vufind_dir . '/web/conf';
+if (!file_exists($basePath . '/config.ini')) {
+    die("\n\nCan't open {$basePath}/config.ini; aborting database upgrade.\n\n");
+}
+require_once dirname(__FILE__) . '/../web/sys/ConfigArray.php';
+$configArray = readConfig($basePath);
+
+?>
+
+#### Database upgrade ####
+
+This script will create encrypted versions of the stored passwords for all
+users in VuFind's database.  While this script does not remove the old
+passwords, it is recommended that you make a backup before proceeding with
+this script, just to be on the safe side!
+
+<?php
+
+    /*
+     * readline() does not exist on Windows.  This is a simple wrapper for portability.
+     *
+     * @param string $prompt Prompt to display to the user.
+     *
+     * @return string        User-entered response.
+     */
+    function getInput($prompt)
+    {
+        // Standard function for most uses
+        if (function_exists('readline')) {
+            $in = readline($prompt);
+            return $in;
+        } else {
+            // Or use our own if it doesn't exist (windows)
+            print $prompt;
+            $fp = fopen("php://stdin", "r");
+            $in = fgets($fp, 4094); // Maximum windows buffer size
+            fclose($fp);
+            // Seems to keep the carriage return if you don't trim
+            return trim($in);
+        }
+    }
+
+    $line = "n";
+    while ($line != "y") {
+        $line = getInput("\nDo you want to proceed? [y/n] ");
+        if ($line == "n") {
+            exit(0);
+        }
+    }
+
+
+    chdir($vufind_dir . "/web");
+
+    require_once 'sys/ConnectionManager.php';
+    require_once 'services/MyResearch/lib/User.php';
+    require_once 'sys/authn/AuthenticationFactory.php';
+
+    ConnectionManager::connectToDatabase();
+
+    $authN = AuthenticationFactory::initAuthentication('DB');
+
+    $user = new User();
+    if ($user->find(true)) {
+        while ($user->fetch()) {
+            if ($user->password != '' && !$user->encrypted_password) {
+                echo "Encrypted and storing password for " . $user->username . "... ";
+                $authN->setPasswordForUser($user, $user->password, true);
+                $user->update();
+                echo "done.\n";
+            }
+        }
+    }
+
+?>
diff --git a/upgrade/encrypted_passwords_db_migration.php b/upgrade/encrypted_passwords_db_migration.php
new file mode 100644
index 0000000..b826924
--- /dev/null
+++ b/upgrade/encrypted_passwords_db_migration.php
@@ -0,0 +1,187 @@
+<?php
+/**
+ * Script to upgrade VuFind database to enable encrypted passwords.
+ *
+ * command line arguments:
+ *   * MySQL admin username
+ *   * MySQL admin password
+ *   * path to previous version installation
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2011.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind
+ * @package  Upgrade_Tools
+ * @author   Mark Triggs <mark@dishevelled.net>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/migration_notes Wiki
+ */
+
+// Die if we don't have enough parameters:
+if (!isset($argv[3]) || !isset($argv[2]) || !isset($argv[1])) {
+    die("\nUsage: " . $argv[0] . " <mysql admin user> <mysql admin password> </path/to/vufind>\n\n");
+}
+
+$mysql_admin_user = $argv[1];
+$mysql_admin_pw = $argv[2];
+$old_config = $argv[3];
+
+// Try to read the ini file:
+$basePath = $old_config . '/web/conf';
+if (!file_exists($basePath . '/config.ini')) {
+    die("\n\nCan't open {$basePath}/config.ini; aborting database upgrade.\n\n");
+}
+require_once dirname(__FILE__) . '/../web/sys/ConfigArray.php';
+$configArray = readConfig($basePath);
+
+?>
+
+#### Database upgrade ####
+
+This script will upgrade your VuFind database from the previous version to the new
+release.  It is recommended that you make a backup before proceeding with this
+script, just to be on the safe side!
+
+<?php
+
+// get connection credentials from config.ini
+$match = preg_match(
+    "/mysql:\/\/([^:]+):([^@]+)@([^\/]+)\/(.+)/",
+    $configArray['Database']['database'], $mysql_conn
+);
+if (!$match) {
+    echo "Can't determine data needed to access your MySQL database. You have " .
+        $configArray['Database']['database'] .
+        "\nas connection string in your config.ini.\n";
+    exit(0);
+}
+
+if ($mysql_admin_user=="") {
+    $mysql_admin_user="root";
+}
+$mysql_host = $mysql_conn[3];
+$mysql_db = $mysql_conn[4];
+if ($mysql_db=="") {
+    $mysql_db="vufind";
+}
+
+echo "\nUsing the following values to access your MySQL database:\n";
+echo "MySQL admin username: " . $mysql_admin_user . "\n";
+echo "MySQL VuFind username: " . $mysql_conn[1] . "\n";
+echo "MySQL database: " . $mysql_db . "\n";
+echo "MySQL host: " . $mysql_host . "\n";
+
+$line = "n";
+
+while ($line != "y") {
+    $line = getInput("\nDo you want to proceed? [y/n] ");
+    if ($line == "n") {
+        exit(0);
+    }
+}
+
+// create a PDO with connection to database
+$dsn = 'mysql:host=' . $mysql_host . ';dbname=' . $mysql_db;
+try {
+    $db = new PDO($dsn, $mysql_admin_user, $mysql_admin_pw);
+} catch(PDOException $e) {
+    echo "Error connecting to database -- " . $e->getMessage() . "\n";
+    exit(0);
+}
+if (!$db) {
+    echo "Error connecting to Database\n";
+    exit(0);
+}
+
+// adjust existing tables:
+updateExistingTables();
+
+
+/**
+ * Support function to execute a SQL statement.
+ *
+ * @param string $sqlStatement SQL to execute.
+ * @param array  $bindParams   Bind parameters to pass with SQL.
+ *
+ * @return object              PDO SQL object (on success; does not return on error).
+ */
+function executeSQL($sqlStatement, $bindParams = array())
+{
+    global $db;
+
+    $sql = $db->prepare($sqlStatement);
+    if (!$sql->execute($bindParams)) {
+        die("Problem executing: {$sqlStatement}");
+    }
+    return $sql;
+}
+
+
+/**
+ * Routine to update existing tables in the database.
+ *
+ * @return void
+ */
+function updateExistingTables()
+{
+    $needsUpgrade = true;
+
+    // Get list of fields in user table:
+    foreach (executeSQL("DESCRIBE user")->fetchAll() as $column) {
+        if ($column['Field'] === 'encrypted_password') {
+            $needsUpgrade = false;
+        }
+    }
+
+    // Add encrypted password field if not already present:
+    if ($needsUpgrade) {
+        echo "Adding home library column to user table... ";
+        executeSQL(
+            "ALTER TABLE user ADD COLUMN encrypted_password varchar(60) " .
+            "DEFAULT NULL;"
+        );
+        echo "done!\n\n";
+    } else {
+        echo "No table updates necessary.\n\n";
+    }
+}
+
+/**
+ * readline() does not exist on Windows.  This is a simple wrapper for portability.
+ *
+ * @param string $prompt Prompt to display to the user.
+ *
+ * @return string        User-entered response.
+ */
+function getInput($prompt)
+{
+    // Standard function for most uses
+    if (function_exists('readline')) {
+        $in = readline($prompt);
+        return $in;
+    } else {
+        // Or use our own if it doesn't exist (windows)
+        print $prompt;
+        $fp = fopen("php://stdin", "r");
+        $in = fgets($fp, 4094); // Maximum windows buffer size
+        fclose($fp);
+        // Seems to keep the carriage return if you don't trim
+        return trim($in);
+    }
+}
+
+?>
diff --git a/upgrade/plaintext_passwords_deactivate.php b/upgrade/plaintext_passwords_deactivate.php
new file mode 100644
index 0000000..3d2a2ba
--- /dev/null
+++ b/upgrade/plaintext_passwords_deactivate.php
@@ -0,0 +1,147 @@
+<?php
+/**
+ * Script to upgrade VuFind database to enable encrypted passwords.
+ *
+ * command line arguments:
+ *   * MySQL admin username
+ *   * MySQL admin password
+ *   * path to previous version installation
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2011.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind
+ * @package  Upgrade_Tools
+ * @author   Mark Triggs <mark@dishevelled.net>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/migration_notes Wiki
+ */
+
+$vufind_dir = isset($argv[1]) ? $argv[1] : false;
+
+if (!$vufind_dir) {
+    die("\nUsage: " . $argv[0] . " </path/to/vufind>\n\n");
+}
+
+$vufind_dir = realpath($vufind_dir);
+
+// Try to read the ini file:
+$basePath = $vufind_dir . '/web/conf';
+if (!file_exists($basePath . '/config.ini')) {
+    die("\n\nCan't open {$basePath}/config.ini; aborting database upgrade.\n\n");
+}
+require_once dirname(__FILE__) . '/../web/sys/ConfigArray.php';
+$configArray = readConfig($basePath);
+
+?>
+
+#### Database upgrade ####
+
+This script will remove the stored plaintext passwords for users that have had
+encrypted passwords created for them.  It is recommended that you make a backup
+before proceeding with this script, just to be on the safe side!
+
+<?php
+
+    /*
+     * readline() does not exist on Windows.  This is a simple wrapper for portability.
+     *
+     * @param string $prompt Prompt to display to the user.
+     *
+     * @return string        User-entered response.
+     */
+    function getInput($prompt)
+    {
+        // Standard function for most uses
+        if (function_exists('readline')) {
+            $in = readline($prompt);
+            return $in;
+        } else {
+            // Or use our own if it doesn't exist (windows)
+            print $prompt;
+            $fp = fopen("php://stdin", "r");
+            $in = fgets($fp, 4094); // Maximum windows buffer size
+            fclose($fp);
+            // Seems to keep the carriage return if you don't trim
+            return trim($in);
+        }
+    }
+
+    $line = "n";
+    while ($line != "y") {
+        $line = getInput("\nDo you want to proceed? [y/n] ");
+        if ($line == "n") {
+            exit(0);
+        }
+    }
+
+    chdir($vufind_dir . "/web");
+
+    require_once 'sys/ConnectionManager.php';
+    require_once 'services/MyResearch/lib/User.php';
+    require_once 'sys/authn/AuthenticationFactory.php';
+
+    ConnectionManager::connectToDatabase();
+
+    $authN = AuthenticationFactory::initAuthentication('DB');
+
+    $rollback_file = $vufind_dir . "/upgrade/" . "vufind-rollback.sql";
+    $rollback = fopen($rollback_file, "a");
+
+    if (!$rollback) {
+        die("Failed to open 'vufind-rollback.sql' for writing.  Aborting.");
+    }
+
+    fprintf($rollback, "########## %s - BEGIN VUFIND ROLLBACK LOG ##########\n\n",
+            date(DATE_RFC822));
+
+    $user = new User();
+    if ($user->find(true)) {
+        while ($user->fetch()) {
+            if ($user->password != '' && $user->encrypted_password) {
+                echo "Removing stored password for " . $user->username . "... ";
+
+                fprintf($rollback,
+                        "update user set password = '%s' where username = '%s';\n",
+                        mysql_real_escape_string($user->password),
+                        mysql_real_escape_string($user->username));
+
+                $user->password = '';
+                $user->update();
+                echo "done.\n";
+            }
+        }
+    }
+
+    fprintf($rollback, "\n########## %s - END VUFIND ROLLBACK LOG ##########\n",
+            date(DATE_RFC822));
+
+    fclose($rollback);
+
+?>
+
+#### Upgrade complete ####
+
+A script that can be used to recover users stored plaintext password has been
+written to:
+
+  <? echo $rollback_file ?>.
+
+Since it contains users' passwords, please store this file somewhere secure,
+and remove it once you are confident you won't need it again.
+
+
diff --git a/web/conf/vufind.ini b/web/conf/vufind.ini
index d56e3fa..014bab0 100644
--- a/web/conf/vufind.ini
+++ b/web/conf/vufind.ini
@@ -82,6 +82,7 @@ college = 130
 home_library = 130
 major = 130
 created = 142
+encrypted_password = 2
 
 [user__keys]
 id = N
diff --git a/web/services/MyResearch/Account.php b/web/services/MyResearch/Account.php
index a81c1b8..0943ccc 100644
--- a/web/services/MyResearch/Account.php
+++ b/web/services/MyResearch/Account.php
@@ -123,8 +123,13 @@ class Account extends Action
             if (!$user->find()) {
                 // We need to reassign the username since we cleared it out when
                 // we did the search for duplicate email addresses:
+
+                $authN = AuthenticationFactory::initAuthentication('DB');
+
                 $user->username = $_POST['username'];
-                $user->password = $_POST['password'];
+
+                $authN->setPasswordForUser($user, $_POST['password']);
+
                 $user->firstname = $_POST['firstname'];
                 $user->lastname = $_POST['lastname'];
                 $user->created = date('Y-m-d h:i:s');
@@ -135,7 +140,7 @@ class Account extends Action
         } else {
             return new PEAR_Error('That username is already taken');
         }
-        
+
         return true;
     }
 }
diff --git a/web/services/MyResearch/lib/User.php b/web/services/MyResearch/lib/User.php
index 1b3b42f..b7ca742 100644
--- a/web/services/MyResearch/lib/User.php
+++ b/web/services/MyResearch/lib/User.php
@@ -61,6 +61,7 @@ class User extends DB_DataObject
     public $home_library;                         // string(100)  not_null
     public $major;                           // string(100)  not_null
     public $created;                         // datetime(19)  not_null binary
+    public $encrypted_password;              // string(60)
 
     /* Static get */
     function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('User',$k,$v); }
@@ -445,5 +446,4 @@ class User extends DB_DataObject
         }
         return true;
     }
-
 }
diff --git a/web/sys/PasswordHash.php b/web/sys/PasswordHash.php
new file mode 100644
index 0000000..12958c7
--- /dev/null
+++ b/web/sys/PasswordHash.php
@@ -0,0 +1,253 @@
+<?php
+#
+# Portable PHP password hashing framework.
+#
+# Version 0.3 / genuine.
+#
+# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in
+# the public domain.  Revised in subsequent years, still public domain.
+#
+# There's absolutely no warranty.
+#
+# The homepage URL for this framework is:
+#
+#	http://www.openwall.com/phpass/
+#
+# Please be sure to update the Version line if you edit this file in any way.
+# It is suggested that you leave the main version number intact, but indicate
+# your project name (after the slash) and add your own revision information.
+#
+# 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.
+#
+# Obviously, since this code is in the public domain, the above are not
+# requirements (there can be none), but merely suggestions.
+#
+class PasswordHash {
+	var $itoa64;
+	var $iteration_count_log2;
+	var $portable_hashes;
+	var $random_state;
+
+	function PasswordHash($iteration_count_log2, $portable_hashes)
+	{
+		$this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+		if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31)
+			$iteration_count_log2 = 8;
+		$this->iteration_count_log2 = $iteration_count_log2;
+
+		$this->portable_hashes = $portable_hashes;
+
+		$this->random_state = microtime();
+		if (function_exists('getmypid'))
+			$this->random_state .= getmypid();
+	}
+
+	function get_random_bytes($count)
+	{
+		$output = '';
+		if (is_readable('/dev/urandom') &&
+		    ($fh = @fopen('/dev/urandom', 'rb'))) {
+			$output = fread($fh, $count);
+			fclose($fh);
+		}
+
+		if (strlen($output) < $count) {
+			$output = '';
+			for ($i = 0; $i < $count; $i += 16) {
+				$this->random_state =
+				    md5(microtime() . $this->random_state);
+				$output .=
+				    pack('H*', md5($this->random_state));
+			}
+			$output = substr($output, 0, $count);
+		}
+
+		return $output;
+	}
+
+	function encode64($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;
+	}
+
+	function gensalt_private($input)
+	{
+		$output = '$P$';
+		$output .= $this->itoa64[min($this->iteration_count_log2 +
+			((PHP_VERSION >= '5') ? 5 : 3), 30)];
+		$output .= $this->encode64($input, 6);
+
+		return $output;
+	}
+
+	function crypt_private($password, $setting)
+	{
+		$output = '*0';
+		if (substr($setting, 0, 2) == $output)
+			$output = '*1';
+
+		$id = substr($setting, 0, 3);
+		# We use "$P$", phpBB3 uses "$H$" for the same thing
+		if ($id != '$P$' && $id != '$H$')
+			return $output;
+
+		$count_log2 = strpos($this->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;
+
+		# We're kind of forced to 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 .= $this->encode64($hash, 16);
+
+		return $output;
+	}
+
+	function gensalt_extended($input)
+	{
+		$count_log2 = min($this->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;
+
+		$output = '_';
+		$output .= $this->itoa64[$count & 0x3f];
+		$output .= $this->itoa64[($count >> 6) & 0x3f];
+		$output .= $this->itoa64[($count >> 12) & 0x3f];
+		$output .= $this->itoa64[($count >> 18) & 0x3f];
+
+		$output .= $this->encode64($input, 3);
+
+		return $output;
+	}
+
+	function gensalt_blowfish($input)
+	{
+		# This one needs to use a different order of characters and a
+		# different encoding scheme from the one in encode64() above.
+		# 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 which
+		# 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') + $this->iteration_count_log2 / 10);
+		$output .= chr(ord('0') + $this->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;
+	}
+
+	function HashPassword($password)
+	{
+		$random = '';
+
+		if (CRYPT_BLOWFISH == 1 && !$this->portable_hashes) {
+			$random = $this->get_random_bytes(16);
+			$hash =
+			    crypt($password, $this->gensalt_blowfish($random));
+			if (strlen($hash) == 60)
+				return $hash;
+		}
+
+		if (CRYPT_EXT_DES == 1 && !$this->portable_hashes) {
+			if (strlen($random) < 3)
+				$random = $this->get_random_bytes(3);
+			$hash =
+			    crypt($password, $this->gensalt_extended($random));
+			if (strlen($hash) == 20)
+				return $hash;
+		}
+
+		if (strlen($random) < 6)
+			$random = $this->get_random_bytes(6);
+		$hash =
+		    $this->crypt_private($password,
+		    $this->gensalt_private($random));
+		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 '*';
+	}
+
+	function CheckPassword($password, $stored_hash)
+	{
+		$hash = $this->crypt_private($password, $stored_hash);
+		if ($hash[0] == '*')
+			$hash = crypt($password, $stored_hash);
+
+		return $hash == $stored_hash;
+	}
+}
+
+?>
diff --git a/web/sys/authn/DatabaseAuthentication.php b/web/sys/authn/DatabaseAuthentication.php
index 0baf713..0e1c60d 100644
--- a/web/sys/authn/DatabaseAuthentication.php
+++ b/web/sys/authn/DatabaseAuthentication.php
@@ -28,6 +28,8 @@
 require_once 'Authentication.php';
 require_once 'services/MyResearch/lib/User.php';
 
+require_once 'sys/PasswordHash.php';
+
 /**
  * Database-driven authentication module.
  *
@@ -39,6 +41,83 @@ require_once 'services/MyResearch/lib/User.php';
  */
 class DatabaseAuthentication implements Authentication
 {
+    private $passHasher;
+
+
+    public function __construct($configurationFilePath = '')
+    {
+        /* Set the iteration count for password stretching and enable
+         * portable hashes to avoid creating a dependency on a particular
+         * platform or PHP version. */
+        $this->passHasher = new PasswordHash(8, true);
+    }
+
+
+    /**
+     * Generate and return a salted password hash
+     *
+     * @param string $password to be hashed
+     *
+     * @return string A hashed password if successful.  False otherwise.
+     * @access public
+     */
+    public function hashPassword($password)
+    {
+        $hashedPassword =
+            $this->passHasher->HashPassword($password);
+
+        /* phpass specifies that a length < 20 means something went
+         * wrong. */
+        if (strlen($hashedPassword) >= 20) {
+            return $hashedPassword;
+        } else {
+            return false;
+        }
+    }
+
+
+    /**
+     * Set the appropriate password properties for the user.  This might be the
+     * unhashed 'password' column, the hashed 'encrypted_password' column, or
+     * both.
+     *
+     * @param object the $user object whose properties will be set.  Note that
+     *               the caller is responsible for persisting the object to the
+     *               DB.
+     * @param string $password to set
+     *
+     * @return array
+     * @access public
+     */
+    public function setPasswordForUser($user, $password, $forceHash = false)
+    {
+        $test = new User();
+
+        $test->query("SELECT count(1) as cnt from {$test->__table} " .
+                     "WHERE password != ''");
+
+        if ($test->fetch() && ($test->cnt > 0)) {
+            /* Set the plaintext password */
+            $user->password = $password;
+        }
+
+
+        $test->query("SELECT count(1) as cnt from {$test->__table} " .
+                     "WHERE encrypted_password IS NOT NULL");
+
+        if ($forceHash || ($test->fetch() && ($test->cnt > 0))) {
+            /* Set the encrypted password */
+            $hashedPassword = $this->hashPassword($user->password);
+
+            if ($hashedPassword) {
+                $user->encrypted_password = $hashedPassword;
+            }
+        }
+
+        return $user;
+    }
+
+
     /**
      * Attempt to authenticate the current user.
      *
@@ -49,17 +128,31 @@ class DatabaseAuthentication implements Authentication
     {
         $username = $_POST['username'];
         $password = $_POST['password'];
+
         if (($username == '') || ($password == '')) {
-            $user = new PEAR_Error('authentication_error_blank');
-        } else {
-            $user = new User();
-            $user->username = $username;
-            $user->password = $password;
-            if (!$user->find(true)) {
-                $user = new PEAR_Error('authentication_error_invalid');
+            return new PEAR_Error('authentication_error_blank');
+        }
+
+        $user = new User();
+        $user->username = $username;
+
+        if (!$user->find(true)) {
+            return new PEAR_Error('authentication_error_invalid');
+        }
+
+        if ($user->encrypted_password) {
+            /* If the user has an encrypted password we'll use it. */
+            if ($this->passHasher->CheckPassword($password,
+                                                 $user->encrypted_password)) {
+                return $user;
             }
+        } else if ($user->password === $password) {
+            return $user;
         }
-        return $user;
+
+        /* No encrypted password and the password they provided was
+         * wrong */
+        return new PEAR_Error('authentication_error_invalid');
     }
 }
 ?>
-- 
1.7.2.5

