[evolvis-commits] r18348: rewrite authentication system; move all crypt related stuff into account.php

mirabilos at evolvis.org mirabilos at evolvis.org
Thu Apr 12 20:39:52 CEST 2012


Author: mirabilos
Date: 2012-04-12 20:39:52 +0200 (Thu, 12 Apr 2012)
New Revision: 18348

Added:
   trunk/gforge_base/evolvisforge-5.1/src/common/include/test-crypt.php
Modified:
   trunk/gforge_base/evolvisforge-5.1/src/common/include/account.php
   trunk/gforge_base/evolvisforge-5.1/src/common/include/session.php
Log:
rewrite authentication system; move all crypt related stuff into account.php

• direct support for {SSHA} on the incoming side,
  without the need for Authen::Passphrase (Perl)
• check whether php5-perl is loaded before attempting to use it
  (fix a fatal uncatchable error)
• indirect support for SHA-256 and SHA-512 unix hashes (via crypt)

Modified: trunk/gforge_base/evolvisforge-5.1/src/common/include/account.php
===================================================================
--- trunk/gforge_base/evolvisforge-5.1/src/common/include/account.php	2012-04-12 18:39:49 UTC (rev 18347)
+++ trunk/gforge_base/evolvisforge-5.1/src/common/include/account.php	2012-04-12 18:39:52 UTC (rev 18348)
@@ -146,6 +146,188 @@
 }
 
 /**
+ * account_getcipher() - Get and check {core/unix_cipher}
+ * @return	string
+ *		lowercase'd {core/unix_cipher} if usable
+ */
+function account_getcipher($flushcache=false) {
+	static $c = false;
+
+	/* for unit testing */
+	if ($flushcache) {
+		$c = false;
+	}
+
+	if (!$c) {
+		$c = strtolower(forge_get_config('unix_cipher'));
+
+		/* run self-tests (yes, there are two stories to this…) */
+		switch ($c) {
+		case 'plain':
+			return $c;
+		case 'des':
+			$t = 'aajVvTOjacxCY';
+			break;
+		case 'blowfish':
+			$t = '$2a$06$aaaaaaaaaaaaaaaaaaaaaOK7n3Kyqc0rrS6xHTCdZWeqGPpVHeytO';
+			break;
+		default:
+			$c = 'md5';
+			/* FALLTHROUGH */
+		case 'md5':
+			$t = '$1$aaaaaaaa$hkqmY7IoZFLxaTeeAIVu10';
+			break;
+		case 'sha-256':
+			$t = '$5$aaaaaaaaaaaaaaaa$YLL1DqD8nyAm9Pogst89RcvyGLokQXYoweQ4fGrwy04';
+			break;
+		case 'sha-512':
+			$t = '$6$aaaaaaaaaaaaaaaa$Ybk6Im9Sw17HFVlElf6Ehd.OGcyCUcgLc91KoMhFWRYe4CHhtavFcTVs3qhM5VD9hga6sFhnub5dLeP/H14OC1';
+			break;
+		}
+
+		if (crypt("8b \xd0\xc1\xd2\xcf\xcc\xd8", $t) !== $t) {
+			throw new Exception('Unable to crypt with "' . $c . '"!');
+			die; /* just in case… */
+		}
+	}
+	return $c;
+}
+
+/**
+ * account_chkunixpw() - Check unix password
+ *
+ * @param	string	$key
+ *			plaintext password the user typed
+ * @param	string	$salt
+ *			encrypted password to check against
+ * @return	enumerated-integer
+ *		0 = password not valid
+ *		1 = password valid but not in {core/unix_cipher} format
+ *		2 = password valid and its format matches what we want
+ */
+function account_chkunixpw($key, $salt, $unittest=false) {
+	$rv = 0;
+	$guessed = 'unknown';
+
+	if (!$key || !$salt || strlen($salt) < 2) {
+		return 0;
+	}
+
+	while (true) {
+		$lo = strtolower($salt);
+
+		/* PLAIN directly */
+		if ($salt === $key) {
+			$rv = 1;
+			$guessed = 'plain';
+			break;
+		}
+
+		/* PLAIN via LDAP {plain} */
+		if (strlen($lo) > 7 &&
+		    substr($lo, 0, 7) === '{plain}' &&
+		    substr($salt, 7) === $key) {
+			$rv = 1;
+			$guessed = 'not-preferred';
+			break;
+		}
+
+		/* UNIX CRYPT directly */
+		if (crypt($key, $salt) === $salt) {
+			$rv = 1;
+			$guessed = 'crypt';
+			break;
+		}
+
+		/* UNIX CRYPT via LDAP {crypt} */
+		if (strlen($lo) > 7 &&
+		    substr($lo, 0, 7) === '{crypt}' &&
+		    crypt($key, substr($salt, 7)) === substr($salt, 7)) {
+			$rv = 1;
+			$guessed = 'not-preferred';
+			break;
+		}
+
+		/* LDAP {ssha} */
+		if (strlen($lo) > 6 &&
+		    substr($lo, 0, 6) === '{ssha}') {
+			$chk = base64_decode(substr($salt, 6));
+			if (strlen($chk) == 24) {
+				$c_salt = substr($chk, 20);
+				$chk = substr($chk, 0, 20);
+				if (pack("H*", sha1($key . $c_salt)) === $chk) {
+					$rv = 1;
+					$guessed = 'not-preferred';
+					break;
+				}
+			}
+		}
+
+		/* any crypt or LDAP or other weird things (last resort) */
+		if (extension_loaded('perl')) {
+			try {
+				/* try Authen::Passphrase, if available */
+				$chk = (($salt[0] == '{' /*}*/) ? '' :
+				    '{crypt}') . $salt;
+				$perl = new Perl();
+				$perl->eval('use Authen::Passphrase');
+				$perl->pwhash = $chk;
+				$perl->pwplain = $key;
+				$perl->eval('$ppr = Authen::Passphrase->from_rfc2307($pwhash);');
+				if ($perl->eval('$ppr->match($pwplain)')) {
+					$rv = 1;
+					$guessed = 'needs-perl';
+				}
+			} catch (PerlException $e) {
+				$rv = 0;
+			}
+			if ($rv) {
+				break;
+			}
+		}
+
+		/* do not loop */
+		break;
+	}
+	if ($rv == 1) {
+		if ($guessed == 'crypt') {
+			/* try to guess better */
+			switch (strlen($salt)) {
+			case 13:
+				$guessed = 'des';
+				break;
+			case 34:
+				if (substr($salt, 0, 3) === '$1$') {
+					$guessed = 'md5';
+				}
+				break;
+			case 60:
+				if (substr($salt, 0, 4) === '$2a$') {
+					$guessed = 'blowfish';
+				}
+				break;
+			default:
+				if (strlen($salt) >= 47 &&
+				    substr($salt, 0, 3) === '$5$') {
+					$guessed = 'sha-256';
+				} else if (strlen($salt) >= 90 &&
+				    substr($salt, 0, 3) === '$6$') {
+					$guessed = 'sha-512';
+				}
+				break;
+			}
+		}
+		if ($guessed === account_getcipher()) {
+			$rv = 2;
+		}
+	}
+	if ($unittest) {
+		return array($rv, $guessed);
+	}
+	return $rv;
+}
+
+/**
  * account_genunixpw() - Generate unix password
  *
  * @param		string	The plaintext password string
@@ -154,7 +336,7 @@
  */
 function account_genunixpw($plainpw) {
 	$cmplensub = 0;
-	switch (strtolower(forge_get_config('unix_cipher'))) {
+	switch (account_getcipher()) {
 	case 'plain':
 		return $plainpw;
 	case 'des':
@@ -174,11 +356,22 @@
 		$salt = account_genstr(8);
 		$postfix = '$';
 		break;
+	case 'sha-256':
+		$prefix = '$5$';
+		$salt = account_genstr(16);
+		$postfix = '$';
+		break;
+	case 'sha-512':
+		$prefix = '$6$';
+		$salt = account_genstr(16);
+		$postfix = '$';
+		break;
 	}
 
 	if (!$salt) {
 		/* XXX better error handling */
-		die("Unable to generate random password salt!\n");
+		throw new Exception('Unable to generate random password salt!');
+		die; /* just in case… */
 	}
 	$salt = $prefix . $salt . $postfix;
 	$cryptpw = crypt($plainpw, $salt);
@@ -189,7 +382,9 @@
 	 */
 	if (strncmp($salt, $cryptpw, strlen($salt) - $cmplensub)) {
 		/* XXX better error handling */
-		die("Unable to crypt with '".forge_get_config('unix_cipher')."' the password!\n");
+		throw new Exception('Unable to crypt with "' .
+		    account_getcipher() . '" the password!');
+		die; /* just in case… */
 	}
 	return $cryptpw;
 }

Modified: trunk/gforge_base/evolvisforge-5.1/src/common/include/session.php
===================================================================
--- trunk/gforge_base/evolvisforge-5.1/src/common/include/session.php	2012-04-12 18:39:49 UTC (rev 18347)
+++ trunk/gforge_base/evolvisforge-5.1/src/common/include/session.php	2012-04-12 18:39:52 UTC (rev 18348)
@@ -166,111 +166,35 @@
 function session_login_valid_dbonly ($loginname, $passwd, $allowpending) {
 	global $feedback,$userstatus;
 
-	//  Try to get the users from the database using user_id and (MD5) user_pw
-	if (forge_get_config('require_unique_email')) {
-		$res = db_query_params ('SELECT user_id,status,unix_pw FROM users WHERE (user_name=$1 OR email=$1) AND user_pw=$2',
-					array ($loginname,
-					       md5($passwd))) ;
-	} else {
-		$res = db_query_params ('SELECT user_id,status,unix_pw FROM users WHERE user_name=$1 AND user_pw=$2',
-					array ($loginname,
-					       md5($passwd))) ;
-	}
+	$res = db_query_params('SELECT user_id, status, user_pw, unix_pw
+		FROM users WHERE user_name=$1' .
+	    (forge_get_config('require_unique_email') ? ' OR email=$1' : ''),
+	    array($loginname));
 	if (!$res || db_numrows($res) < 1) {
-		// No user whose MD5 passwd matches the MD5 of the provided passwd
-		// Selecting by user_name/email only
-		if (forge_get_config('require_unique_email')) {
-			$res = db_query_params ('SELECT user_id,status,user_pw,unix_pw FROM users WHERE user_name=$1 OR email=$1',
-						array ($loginname)) ;
-		} else {
-			$res = db_query_params ('SELECT user_id,status,user_pw,unix_pw FROM users WHERE user_name=$1',
-						array ($loginname)) ;
-		}
-		if (!$res || db_numrows($res) < 1) {
-			// No user by that name
-			$feedback=_('Invalid Password Or User Name');
-			return false;
-		}
+		$feedback = _('Invalid Password Or User Name');
+		return false;
+	}
 
-		$usr = db_fetch_array($res);
-		$userstatus = $usr['status'];
-		$num_uid = $usr['user_id'];
+	$usr = db_fetch_array($res);
+	$userstatus = $usr['status'];
+	$num_uid = $usr['user_id'];
 
-		// Compare (crypt) unix_pw first
-		$is_valid = false;
-		if (crypt($passwd, $usr['unix_pw']) === $usr['unix_pw']) {
-			$is_valid = true;
-		} else if (
-			/* check for "{crypt}foo" */
-			strlen($usr['unix_pw']) >= 8 &&
-			substr($foo, 0, 7) == '{crypt}' &&
-			crypt($passwd, substr($usr['unix_pw'], 7)) ===
-			substr($usr['unix_pw'], 7)) {
-			/* we regenerate both user_pw and unix_pw below → ok */
-			$is_valid = true;
-		} else if ($usr['user_pw'] != '') {
-			if ($usr['user_pw'][0] == '{' /* } */) {
-				// RFC2307 hash
-				$pwhash = $usr['user_pw'];
-			} else {
-				// crypt hash
-				$pwhash = "{crypt}" . $usr['user_pw'];
-			}
+	$is_valid = account_chkunixpw($passwd, $usr['unix_pw']);
+	if (!$is_valid) {
+		$feedback = _('Invalid Password Or User Name');
+		return false;
+	}
 
-			try {
-				/* try Authen::Passphrase, if available */
-
-				$perl = new Perl();
-				$perl->eval('use Authen::Passphrase');
-				$perl->pwhash = $pwhash;
-				$perl->pwplain = $passwd;
-				$perl->eval('$ppr = Authen::Passphrase->from_rfc2307($pwhash);');
-				if ($perl->eval('$ppr->match($pwplain)')) {
-					$is_valid = true;
-				}
-			} catch (PerlException $e) {
-				$is_valid = false;
-			}
-		}
-
-		if (!$is_valid) {
-			// There is a user with the provided user_name, but
-			// neither does the MD5 password match, nor can we
-			// authenticate it using the (crypt) unix_pw, nor
-			// is the user_pw a valid crypt or LDAP password hash
-
-			$feedback=_('Invalid Password Or User Name');
-			return false;
-		}
-
-	//=== until here  : $is_valid: is the $passwd valid  ===
-	//=== from here on: $is_valid: is the DB entry valid ===
-
-		$is_valid = false;	// update the DB, always
-	} else {
-		// MD5 password is valid, check the unix (crypt) password
-
-		$usr = db_fetch_array($res);
-		$num_uid = $usr['user_id'];
-
-		if (crypt($passwd, $usr['unix_pw']) === $usr['unix_pw']) {
-			$is_valid = true;
-		} else {
-			$is_valid = false;
-		}
+	if ($is_valid == 2 && $usr['user_pw'] !== md5($passwd)) {
+		$is_valid = 1;
 	}
 
-	// We have a user and authenticated him/her somehow, meaning that
-	// user_name='$loginname' has the valid plain text '$passwd'.
-	// Update the database with this information if required.
-
-	if (!$is_valid) {
-		// Just update both fields. It doesn’t hurt.
+	if ($is_valid != 2) {
+		/* Update the database with canonical hashes */
 		$res = db_query_params('UPDATE users
 		    SET user_pw=$1, unix_pw=$2
 		    WHERE user_name=$3',
 				       array(md5($passwd), account_genunixpw($passwd), $loginname));
-		//$is_valid = true;
 	}
 
 	// Yay.  The provided password matches both fields in the database.

Added: trunk/gforge_base/evolvisforge-5.1/src/common/include/test-crypt.php
===================================================================
--- trunk/gforge_base/evolvisforge-5.1/src/common/include/test-crypt.php	                        (rev 0)
+++ trunk/gforge_base/evolvisforge-5.1/src/common/include/test-crypt.php	2012-04-12 18:39:52 UTC (rev 18348)
@@ -0,0 +1,103 @@
+<?php
+/*-
+ * Test crypt() functions in FusionForge
+ *
+ * Copyright © 2012
+ *	Thorsten Glaser <t.glaser at tarent.de>
+ * All rights reserved.
+ *
+ * This file is part of FusionForge. FusionForge is free software;
+ * you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software
+ * Foundation; either version 2 of the Licence, or (at your option)
+ * any later version.
+ *
+ * FusionForge 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 FusionForge; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/* mock */
+
+$cfg = array('use_captcha' => false);
+function forge_get_config($s) {
+	global $cfg;
+
+	if (!isset($cfg[$s])) {
+		echo "E: forge_get_config undef '$s'\n";
+		$cfg[$s] = false;
+	}
+	return $cfg[$s];
+}
+
+/* aid */
+
+require "minijson.php";
+require "utils.php";
+
+/* to test */
+
+require "account.php";
+
+/* test batteries */
+
+$tests_unix = array(
+	'Plain' => 'fnord',
+	'DES' => 'ndCam7XnSiOQU',
+	'Blowfish' => '$2a$08$lnW6D7RO02ji5Z3SWt9Pee0WZpEbBN20bPZdNLRpCF9rErqY4bZnS',
+	'MD5' => '$1$OEaikIe0$h6vVHVs2nzbt2PUe2lx381',
+	'SHA-256' => '$5$ZP1n6RBh2Kz4neoh$3P1sF606SWhIpLkhLqAR8UrHX/vTiv/z0AI0YFtrCu6',
+	'SHA-512' => '$6$YUqLlwG2hXyAILcG$.05BP55eRz21GCUHDNx83JxtxyymfncBAi0wIxyAzUp78Gae6UUs7GvNDO128SBc48S7QuF0JOHpxjRLG2qt10',
+	'Foo' => '$1$YxJD7PNC$aECtZazSxk.JBs7iJWgQf0',
+);
+
+$tests_incoming = array_merge(array_values($tests_unix), array(
+	'{Plain}fnord',
+	'{Crypt}$1$sY3NGTnE$jJ2p3diJoDNparlT2OZKt0',
+	'{sSHA}j8KfLagdk8RkTgu7zgxe7/G482Dvx+Ep',
+	'{MD5}sV5ADI29ZpfyY4UhbTKkDw==',
+));
+
+foreach ($tests_unix as $method => $hash) {
+	echo "\nTesting UNIX with $method\n";
+	$cfg['unix_cipher'] = $method;
+	try {
+		echo "Recognised as: " . account_getcipher(true) . "\n";
+	} catch (Exception $e) {
+		echo 'getcipher, caught exception: ',  $e->getMessage(), "\n";
+	}
+	try {
+		$res = account_chkunixpw('fnord', $hash, true);
+		echo "Validated as: " . minijson_encode($res,false) . "\n";
+	} catch (Exception $e) {
+		echo 'chkunixpw, caught exception: ',  $e->getMessage(), "\n";
+	}
+	try {
+		echo "Generated as: " . account_genunixpw('fnord') . "\n";
+	} catch (Exception $e) {
+		echo 'genunixpw, caught exception: ',  $e->getMessage(), "\n";
+	}
+}
+
+$cfg['unix_cipher'] = 'PLAIN';
+try {
+	account_getcipher(true);
+} catch (Exception $e) {
+	echo "\n\nUnexpected exception: ", $e->getMessage(), "\n";
+}
+
+foreach ($tests_incoming as $hash) {
+	try {
+		$res = account_chkunixpw('fnord', $hash, true);
+	} catch (Exception $e) {
+		$res = "!" . $e->getMessage();
+	}
+	echo "\nIncoming result " . minijson_encode($res,false) . " from: $hash";
+}
+
+echo "\n\nAll tests run.\n";



More information about the evolvis-commits mailing list