bug 546556: SSHA password hashes for mysql auth driver, r=telliott
authorLeslie Michael Orchard <lorchard@mozilla.com>
Fri, 16 Apr 2010 13:10:56 -0400
changeset 75 34e79eefc42c
parent 74 36f6c5b82e2d
child 76 511d1b8f24e6
push id1
push usertziade@mozilla.com
push dateTue, 19 Oct 2010 10:41:37 +0000
reviewerstelliott
bugs546556
bug 546556: SSHA password hashes for mysql auth driver, r=telliott
1.0/weave_user/mysql.php
--- a/1.0/weave_user/mysql.php
+++ b/1.0/weave_user/mysql.php
@@ -40,41 +40,92 @@
 	
 require_once 'weave_user/base.php';
 require_once 'weave_constants.php';
 
 #Mysql version of the authentication object.
 #Note that this object does not contain any database setup information. It assumes that the mysql
 #instance is already fully configured as part of the weave-registration server
 
+#
+#create table users
+#(
+# id int(11) NOT NULL PRIMARY KEY auto_increment
+# username varchar(32),
+# md5 varbinary(64),
+# email varbinary(64),
+# status tinyint(4) default '1',
+# alert text,
+# reset varbinary(32) default null,
+#) engine=InnoDB;
+#
+
 class WeaveAuthentication implements WeaveAuthenticationBase
 {
 	var $_dbh;
 	var $_username = null;
 	var $_alert = null;
 	
-	function hash_password($password)
-	{
-		if (defined('WEAVE_SHA_SALT'))
-		{
-			$p_array = str_split($password,(floor(strlen($password)/2))+1);
-			return hash('sha256', $p_array[0] . WEAVE_SHA_SALT . $p_array[1]);
-		}
-		else
-		{
-			return md5($password);
-		}
-	}
-
 	function __construct($username) 
 	{
 		$this->open_connection();
 		$this->_username = $username;
 	}
 
+	/**
+	 * Generate a SSHA password hash using the given password and optional 
+	 * salt.  If not supplied, the salt will be generated randomly
+	 *
+	 * @param   string $password Cleartext password
+	 * @param   string $salt     Hashing salt
+	 * @returns string SSHA password hash
+	 */
+	function generateSSHAPassword($password, $salt=null)
+	{
+		if (setlocale(LC_CTYPE, "UTF8", "en_US.UTF-8") == false) 
+			throw new Exception("Database Unavailable", 503);
+
+		// see also: http://blog.coenbijlsma.nl/2009/01/17/php-and-ssha-ldap-passwords/
+		if (null === $salt) {
+			mt_srand((double)microtime()*1000000);
+			$salt = pack("CCCCCCCC", 
+				mt_rand(), mt_rand(), mt_rand(), mt_rand(), 
+				mt_rand(), mt_rand(), mt_rand(), mt_rand());
+		}
+		$ssha = "{SSHA}" . 
+			base64_encode( pack("H*", sha1($password . $salt)) . $salt);
+		return $ssha;
+	}
+
+	/**
+	 * Determine whether the given password is valid for the given SSHA
+	 * password hash.
+	 *
+	 * @param   string $password  Cleartext password
+	 * @param   string $ssha_hash SSHA password hash
+	 * @returns boolean
+	 */
+	function validateSSHAPassword($password, $ssha_hash)
+	{
+		$salt	   = substr(base64_decode(substr($ssha_hash, 6)), 20);
+		$test_hash = $this->generateSSHAPassword($password, $salt);
+		return ($ssha_hash == $test_hash);
+	}
+
+	/**
+	 * Hash a password using the latest password hashing method (eg SSHA)
+	 *
+	 * @param   string $password Cleartext password
+	 * @returns string Password hash
+	 */
+	function hash_password($password)
+	{
+		return $this->generateSSHAPassword($password);
+	}
+	
 	function open_connection() 
 	{ 
 		$hostname = WEAVE_MYSQL_AUTH_HOST;
 		$dbname = WEAVE_MYSQL_AUTH_DB;
 		$dbuser = WEAVE_MYSQL_AUTH_USER;
 		$dbpass = WEAVE_MYSQL_AUTH_PASS;
 		
 		try
@@ -90,57 +141,138 @@ class WeaveAuthentication implements Wea
 		return true;
 	}
 	
 	function get_connection()
 	{
 		return $this->_dbh;
 	}
 
+	function update_password($password)
+	{
+		if (!$this->_username)
+		{
+			throw new Exception(WEAVE_ERROR_INVALID_USERNAME, 404);
+		}
+		if (!$password)
+		{
+			throw new Exception(WEAVE_ERROR_MISSING_PASSWORD, 404);
+		}
+		
+		if (!$this->user_exists())
+			throw new Exception("User not found", 404);
+			
+		try
+		{
+			$insert_stmt = 'update users set md5 = :md5 where username = :username';
+			$sth = $this->_dbh->prepare($insert_stmt);
+			$sth->bindParam(':username', $this->_username);
+			$phash = $this->hash_password($password);
+			$sth->bindParam(':md5', $phash);
+			$sth->execute();
+		}
+		catch( PDOException $exception )
+		{
+			error_log("update_password: " . $exception->getMessage());
+			throw new Exception("Database unavailable", 503);
+		}
+		return true;
+	
+	}
+	
+	function user_exists() 
+	{
+		try
+		{
+			$select_stmt = 'select count(*) from users where username = :username';
+			$sth = $this->_dbh->prepare($select_stmt);
+			$sth->bindParam(':username', $this->_username);
+			$sth->execute();
+		}
+		catch( PDOException $exception )
+		{
+			error_log("user_exists: " . $exception->getMessage());
+			throw new Exception("Database unavailable", 503);
+		}
 
+		$result = $sth->fetchColumn();
+		return $result;
+	}
+
+	/**
+	 * Authenticate the current user with the given password.
+	 *
+	 * Initially attempts to validate the password using the current latest 
+	 * password hashing method (SSHA), but will fallback through MD5 and 
+	 * server-salt SSHA.  If a fallback is valid, the user's password is
+	 * updated to the latest method.
+	 *
+	 * @param   string $password Cleartext password
+	 * @returns mixed - null on invalid, user ID on valid.
+	 */
 	function authenticate_user($password)
 	{
 		$result = null;
 		try
 		{
-			$select_stmt = 'select id, status, alert from users where username = ? and md5 = ?';
+			$select_stmt = '
+				SELECT id, md5, status, alert 
+				FROM users 
+				WHERE username=?
+			';
 			$sth = $this->_dbh->prepare($select_stmt);
-			$phash = $this->hash_password($password);
+			$sth->execute(array($this->_username));
+			$result = $sth->fetch(PDO::FETCH_ASSOC);
+			$sth->closeCursor();
 
-			$sth->execute(array($this->_username, $phash));
-		
-			if (!$result = $sth->fetch(PDO::FETCH_ASSOC))
-			{
-				if (defined('WEAVE_SHA_SALT') && defined('WEAVE_MD5_FALLBACK') && WEAVE_MD5_FALLBACK) #fall back to md5
-				{
-					$sth->closeCursor();
-					$phash = md5($password);
-					$sth->execute(array($this->_username, $phash));
-					$result = $sth->fetch(PDO::FETCH_ASSOC);
+			if (!$result) { 
+				// User not found.
+				return null; 
+			} else if ($result['status'] != 1) {
+				// User disabled.
+				return null;
+			} else if (strpos($result['md5'], '{SSHA}') !== false) {
+				// Looks like a {SSHA} password, so try validating it.
+				if (!$this->validateSSHAPassword($password, $result['md5'])) {
+					return null;
 				}
-				if (!$result)
+			} else {
+				// This might be a legacy password hash, so try fallbacks...
+				if (defined('WEAVE_MD5_FALLBACK') && WEAVE_MD5_FALLBACK &&
+						($result['md5'] == md5($password)) ) {
+					// Looks like a valid MD5 hash, so accept it but update it.
+					$this->update_password($password);
+				} else if (defined('WEAVE_SHA_SALT')) {
+					// We have a SHA salt, so try generating a hash with it.
+					$p_array = str_split($password, (floor(strlen($password)/2))+1);
+					$sha_hash = hash('sha256', $p_array[0].WEAVE_SHA_SALT.$p_array[1]);
+					if ($result['md5'] == $sha_hash) {
+						// Looks like a valid SHA hash, so accept it but update it.
+						$this->update_password($password);
+					} else {
+						// Ran out of legacy fallbacks, so bail.
+						return null;
+					}
+				} else {
+					// Ran out of legacy fallbacks, so bail.
 					return null;
+				}
 			}
 		}
 		catch( PDOException $exception )
 		{
 			error_log("authenticate_user: " . $exception->getMessage());
 			throw new Exception("Database unavailable", 503);
 		}
 		
 		$this->_alert = $result['alert'];
 		
-		if ($result['status'] != 1)
-			return null;
-			
 		return $result['id'];
 	}
 	
 	function get_user_alert()
 	{
 		return $this->_alert;
 	}
 	
 }
 
-
-
-?>
+/* vim: set noexpandtab */