From 457e79454d05d7f8ad59166c99a709afece8ac4a Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Wed, 4 May 2011 07:52:45 +0000
Subject: [PATCH] * Setup: making SSHA (salted sha1) hashes the default
 password hash for SQL and LDAP - fixing not working ssha hashes if
 mb_string.func_overload > 0 set

---
 phpgwapi/inc/class.auth.inc.php       | 27 ++++++++++++++-------------
 phpgwapi/inc/common_functions.inc.php | 21 +++++++++++++++++++++
 setup/inc/class.setup_process.inc.php |  7 +++++--
 3 files changed, 40 insertions(+), 15 deletions(-)

diff --git a/phpgwapi/inc/class.auth.inc.php b/phpgwapi/inc/class.auth.inc.php
index 091aee68c1..aab42b40ce 100644
--- a/phpgwapi/inc/class.auth.inc.php
+++ b/phpgwapi/inc/class.auth.inc.php
@@ -57,9 +57,9 @@ class auth
 
 	/**
 	 * check_password_age
-	 * check if users are supposed to change their password every x sdays, then check if password is of old age 
+	 * check if users are supposed to change their password every x sdays, then check if password is of old age
 	 * or the devil-admin reset the users password and forced the user to change his password on next login.
-	 * 
+	 *
 	 * @param string $app to know where you are/ or where you want to go
 	 * @param string $class to know where you are/ or where you want to go
 	 * @param string $method to know where you are/ or where you want to go
@@ -87,7 +87,7 @@ class auth
 		//echo egw_time::to('now','ts')-($GLOBALS['egw_info']['server']['change_pwd_every_x_days']*86400).'<br>';
 
 		// if neither timestamp isset return true, nothing to do (exept this means the password is too old)
-		if (!isset($GLOBALS['egw_info']['user']['account_lastpasswd_change']) && 
+		if (!isset($GLOBALS['egw_info']['user']['account_lastpasswd_change']) &&
 			!isset($GLOBALS['egw_info']['user'][$alpwchange]) &&
 			empty($GLOBALS['egw_info']['server']['change_pwd_every_x_days'])
 		) return true;
@@ -106,7 +106,7 @@ class auth
 			if (method_exists($backend,'getLastPwdChange')) $alpwchange_val = $backend->getLastPwdChange($GLOBALS['egw_info']['user']['account_lid']);
 			if (is_null($alpwchange_val) || $alpwchange_val === false) $alpwchange_val = $GLOBALS['egw_info']['user'][$alpwchange];
 		}
-		if (is_null($passwordAgeBorder) && $GLOBALS['egw_info']['server']['change_pwd_every_x_days']) 
+		if (is_null($passwordAgeBorder) && $GLOBALS['egw_info']['server']['change_pwd_every_x_days'])
 		{
 			$passwordAgeBorder = (egw_time::to('now','ts')-($GLOBALS['egw_info']['server']['change_pwd_every_x_days']*86400));
 		}
@@ -125,17 +125,17 @@ class auth
 			   (
 				($passwordAgeBorder > $alpwchange_val) ||
 				(
-				 $GLOBALS['egw_info']['server']['warn_about_upcoming_pwd_change'] && 
-				 $GLOBALS['egw_info']['server']['warn_about_upcoming_pwd_change'] > $daysLeftUntilChangeReq 
+				 $GLOBALS['egw_info']['server']['warn_about_upcoming_pwd_change'] &&
+				 $GLOBALS['egw_info']['server']['warn_about_upcoming_pwd_change'] > $daysLeftUntilChangeReq
 				)
 			   )
 			  ) || $alpwchange_val==0
-			 ) 
+			 )
 			)
 		{
 			if ($GLOBALS['egw']->acl->check('nopasswordchange', 1, 'preferences')) return true; // user has no rights to change password
 			if ($UserKnowsAboutPwdChange === true && !($passwordAgeBorder > $alpwchange_val || $alpwchange_val==0)) return true; // user has already been informed about the upcomming password expiration
-			if ($alpwchange_val == 0) 
+			if ($alpwchange_val == 0)
 			{
 				$message = lang('an admin required that you must change your password upon login.');
 			}
@@ -167,7 +167,7 @@ class auth
 	 * fetch the last pwd change for the user
 	 *
 	 * @param string $username username of account to authenticate
-	 * @return mixed false or shadowlastchange*24*3600 
+	 * @return mixed false or shadowlastchange*24*3600
 	 */
 	function getLastPwdChange($username)
 	{
@@ -566,8 +566,8 @@ class auth
 		$hash = base64_decode(substr($db_val,6));
 
 		/* SMD5 hashes are 16 bytes long */
-		$orig_hash = substr($hash, 0, 16);
-		$salt = substr($hash, 16);
+		$orig_hash = cut_bytes($hash, 0, 16);	// binary string need to use cut_bytes, not mb_substr(,,'utf-8')!
+		$salt = cut_bytes($hash, 16);
 
 		$new_hash = md5($form_val . $salt,true);
 		//echo '<br>  DB: ' . base64_encode($orig_hash) . '<br>FORM: ' . base64_encode($new_hash);
@@ -605,10 +605,11 @@ class auth
 		$hash = base64_decode(substr($db_val, 6));
 
 		// SHA-1 hashes are 160 bits long
-		$orig_hash = substr($hash, 0, 20);
-		$salt = substr($hash, 20);
+		$orig_hash = cut_bytes($hash, 0, 20);	// binary string need to use cut_bytes, not mb_substr(,,'utf-8')!
+		$salt = cut_bytes($hash, 20);
 		$new_hash = sha1($form_val . $salt,true);
 
+		//error_log(__METHOD__."('$form_val', '$db_val') hash='$hash', orig_hash='$orig_hash', salt='$salt', new_hash='$new_hash' returning ".array2string(strcmp($orig_hash,$new_hash) == 0));
 		return strcmp($orig_hash,$new_hash) == 0;
 	}
 
diff --git a/phpgwapi/inc/common_functions.inc.php b/phpgwapi/inc/common_functions.inc.php
index 52d58c3082..09d8d30e5d 100755
--- a/phpgwapi/inc/common_functions.inc.php
+++ b/phpgwapi/inc/common_functions.inc.php
@@ -49,6 +49,27 @@ function bytes($str)
 	return $func_overload & 2 ? mb_strlen($str,'ascii') : strlen($str);
 }
 
+/**
+ * mbstring.func_overload safe substr
+ *
+ * @param string $data
+ * @param int $offset
+ * @param int $len
+ * @return string
+ */
+function cut_bytes(&$data,$offset,$len=null)
+{
+	static $func_overload;
+
+	if (is_null($func_overload)) $func_overload = extension_loaded('mbstring') ? ini_get('mbstring.func_overload') : 0;
+
+	if (is_null($len))
+	{
+		return $func_overload ? mb_substr($data,$offset,bytes($data),'ascii') : substr($data,$offset);
+	}
+	return $func_overload ? mb_substr($data,$offset,$len,'ascii') : substr($data,$offset,$len);
+}
+
 /**
  * Format array or other types as (one-line) string, eg. for error_log statements
  *
diff --git a/setup/inc/class.setup_process.inc.php b/setup/inc/class.setup_process.inc.php
index 9039585490..9febe658d9 100755
--- a/setup/inc/class.setup_process.inc.php
+++ b/setup/inc/class.setup_process.inc.php
@@ -259,7 +259,7 @@ class setup_process
 		}
 		// always enable spellchecker, ckeditor now uses spell-as-you-type via a public webservice
 		$current_config['enabled_spellcheck'] = 'True';
-		
+
 		// always enable history logging for calendar, addressbook and infolog
 		$current_config['history'] = 'history';	// addressbook: only admin
 		$current_config['calendar_delete_history'] = 'history';	// only admins
@@ -270,7 +270,7 @@ class setup_process
 			'config_app' => 'infolog',
 			'config_name' => 'history',
 		),__FILE__,__LINE__);
-		
+
 
 		// RalfBecker: php.net recommend this for security reasons, it should be our default too
 		$current_config['usecookies'] = 'True';
@@ -294,6 +294,9 @@ class setup_process
 
 		$current_config['postpone_statistics_submit'] = time() + 2 * 30 * 3600;	// ask user in 2 month from now, when he has something to report
 
+		// use ssha (salted sha1) password hashes by default
+		$current_config['sql_encryption_type'] = $current_config['ldap_encryption_type'] = 'ssha';
+
 		if ($preset_config)
 		{
 			$current_config = array_merge($current_config,$preset_config);