- implemented more secure password hashing types: sha512_crypt, sha256_crypt and blowfish_crypt (later was only just broken)

- DB schema update for account_pwd to varchar(128) to accomodate sha512_crypt hashes
- enable automatic migration to sha512_crypt, if on SQL or LDAP (but only on Linux, as OpenLDAP has not native support for it)
This commit is contained in:
Ralf Becker 2011-06-05 23:22:51 +00:00
parent d0800ffafa
commit fae1d29e68
8 changed files with 182 additions and 168 deletions

View File

@ -216,20 +216,20 @@ class auth
}
/**
* return a random string of size $size
* return a random string of letters [0-9a-zA-Z] of size $size
*
* @param $size int-size of random string to return
*/
static function randomstring($size)
{
$s = '';
$random_char = array(
static $random_char = array(
'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f',
'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
'w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L',
'M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
);
$s = '';
for ($i=0; $i<$size; $i++)
{
$s .= $random_char[mt_rand(1,61)];
@ -260,13 +260,15 @@ class auth
*
* @param string $cleartext cleartext password
* @param string $encrypted encrypted password, can have a {hash} prefix, which overrides $type
* @param string $type type of encryption
* @param string $type_i type of encryption
* @param string $username used as optional key of encryption for md5_hmac
* @param string &$type=null on return detected type of hash
* @return boolean
*/
static function compare_password($cleartext,$encrypted,$type,$username='')
static function compare_password($cleartext, $encrypted, $type_in, $username='', &$type=null)
{
// allow to specify the hash type to prefix the hash, to easy migrate passwords from ldap
$type = $type_in;
$saved_enc = $encrypted;
if (preg_match('/^\\{([a-z_5]+)\\}(.+)$/i',$encrypted,$matches))
{
@ -284,31 +286,100 @@ class auth
break;
default:
$encrypted = $saved_enc;
// ToDo: the others ...
break;
}
}
elseif($encrypted[0] == '$')
{
$type = 'crypt';
}
switch($type)
{
case 'plain':
return strcmp($cleartext,$encrypted) == 0;
$ret = $cleartext === $encrypted;
break;
case 'smd5':
return self::smd5_compare($cleartext,$encrypted);
$ret = self::smd5_compare($cleartext,$encrypted);
break;
case 'sha':
return self::sha_compare($cleartext,$encrypted);
$ret = self::sha_compare($cleartext,$encrypted);
break;
case 'ssha':
return self::ssha_compare($cleartext,$encrypted);
$ret = self::ssha_compare($cleartext,$encrypted);
break;
case 'crypt':
case 'des':
case 'md5_crypt':
case 'blowish_crypt': // was for some time a typo in setup
case 'blowfish_crypt':
case 'ext_crypt':
return self::crypt_compare($cleartext,$encrypted,$type);
case 'sha256_crypt':
case 'sha512_crypt':
$ret = self::crypt_compare($cleartext, $encrypted, $type);
break;
case 'md5_hmac':
return self::md5_hmac_compare($cleartext,$encrypted,$username);
case 'md5':
$ret = self::md5_hmac_compare($cleartext,$encrypted,$username);
break;
default:
return strcmp(md5($cleartext),$encrypted) == 0 ? true : false;
$type = 'md5';
// fall through
case 'md5':
$ret = md5($cleartext) === $encrypted;
break;
}
error_log(__METHOD__."('$cleartext', '$encrypted', '$type_in', '$username') type='$type' returning ".array2string($ret));
return $ret;
}
/**
* Parameters used for crypt: const name, salt prefix, len of random salt, postfix
*
* @var array
*/
static $crypt_params = array( //
'crypt' => array('CRYPT_STD_DES', '', 2, ''),
'ext_crypt' => array('CRYPT_EXT_DES', '_J9..', 4, ''),
'md5_crypt' => array('CRYPT_MD5', '$1$', 8, '$'),
//'old_blowfish_crypt' => array('CRYPT_BLOWFISH', '$2$', 13, ''), // old blowfish hash not in line with php.net docu, but could be in use
'blowfish_crypt' => array('CRYPT_BLOWFISH', '$2a$12$', 22, ''), // $2a$12$ = 2^12 = 4096 rounds
'sha256_crypt' => array('CRYPT_SHA256', '$5$', 16, '$'), // no "round=N$" --> default of 5000 rounds
'sha512_crypt' => array('CRYPT_SHA512', '$6$', 16, '$'), // no "round=N$" --> default of 5000 rounds
);
/**
* compare crypted passwords for authentication whether des,ext_des,md5, or blowfish crypt
*
* @param string $form_val user input value for comparison
* @param string $db_val stored value / hash (from database)
* @param string &$type detected crypt type on return
* @return boolean True on successful comparison
*/
static function crypt_compare($form_val, $db_val, &$type)
{
// detect type of hash by salt part of $db_val
list($first, $dollar, $salt, $salt2) = explode('$', $db_val);
foreach(self::$crypt_params as $type => $params)
{
list(,$prefix, $random, $postfix) = $params;
list(,$d) = explode('$', $prefix);
if ($dollar === $d || !$dollar && ($first[0] === $prefix[0] || $first[0] !== '_' && !$prefix))
{
$len = !$postfix ? strlen($prefix)+$random : strlen($prefix.$salt.$postfix);
// sha(256|512) might contain options, explicit $rounds=N$ prefix in salt
if (($type == 'sha256_crypt' || $type == 'sha512_crypt') && substr($salt, 0, 7) === 'rounds=')
{
$len += strlen($salt2)+1;
}
break;
}
}
$salt = substr($db_val, 0, $len);
$new_hash = crypt($form_val, $salt);
error_log(__METHOD__."('$form_val', '$db_val') type=$type --> len=$len --> salt='$salt' --> new_hash='$new_hash' returning ".array2string($db_val === $new_hash));
return $db_val === $new_hash;
}
/**
@ -323,41 +394,30 @@ class auth
static function encrypt_ldap($password, $type=null)
{
if (is_null($type)) $type = $GLOBALS['egw_info']['server']['ldap_encryption_type'];
$salt = '';
switch(strtolower($type))
{
default: // eg. setup >> config never saved
case 'des':
$salt = self::randomstring(2);
$_password = crypt($password, $salt);
$e_password = '{crypt}'.$_password;
break;
case 'blowish_crypt': // was for some time a typo in setup
$type = $type == 'blowish_crypt' ? 'blowfish_crypt' : 'crypt';
// fall through
case 'crypt':
case 'sha256_crypt':
case 'sha512_crypt':
case 'blowfish_crypt':
if(@defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1)
{
$salt = '$2$' . self::randomstring(13);
$e_password = '{crypt}'.crypt($password,$salt);
break;
}
self::$error = 'no blowfish crypt';
break;
case 'md5_crypt':
if(@defined('CRYPT_MD5') && CRYPT_MD5 == 1)
{
$salt = '$1$' . self::randomstring(9);
$e_password = '{crypt}'.crypt($password,$salt);
break;
}
self::$error = 'no md5 crypt';
break;
case 'ext_crypt':
if(@defined('CRYPT_EXT_DES') && CRYPT_EXT_DES == 1)
list($const, $prefix, $len, $postfix) = self::$crypt_params[$type];
if(defined($const) && constant($const) == 1)
{
$salt = self::randomstring(9);
$e_password = '{crypt}'.crypt($password,$salt);
$salt = $prefix.self::randomstring($len).$postfix;
$e_password = '{crypt}'.crypt($password, $salt);
break;
}
self::$error = 'no ext crypt';
self::$error = 'no '.str_replace('_', ' ', $type);
$e_password = false;
break;
case 'md5':
/* New method taken from the openldap-software list as recommended by
@ -367,7 +427,7 @@ class auth
break;
case 'smd5':
$salt = self::randomstring(16);
$hash = md5($password . $salt,true);
$hash = md5($password . $salt, true);
$e_password = '{SMD5}' . base64_encode($hash . $salt);
break;
case 'sha':
@ -375,14 +435,15 @@ class auth
break;
case 'ssha':
$salt = self::randomstring(16);
$hash = sha1($password . $salt,true);
$hash = sha1($password . $salt, true);
$e_password = '{SSHA}' . base64_encode($hash . $salt);
break;
case 'plain':
// if plain no type is prepended
$e_password =$password;
$e_password = $password;
break;
}
error_log(__METHOD__."('$password', ".array2string($type).") returning ".array2string($e_password).(self::$error ? ' error='.self::$error : ''));
return $e_password;
}
@ -395,67 +456,43 @@ class auth
static function encrypt_sql($password)
{
/* Grab configured type, or default to md5() (old method) */
$type = @$GLOBALS['egw_info']['server']['sql_encryption_type']
? strtolower($GLOBALS['egw_info']['server']['sql_encryption_type'])
: 'md5';
$type = @$GLOBALS['egw_info']['server']['sql_encryption_type'] ?
strtolower($GLOBALS['egw_info']['server']['sql_encryption_type']) : 'md5';
switch($type)
{
case 'plain':
// since md5 is the default, type plain must be prepended, for eGroupware to understand
return '{PLAIN}'.$password;
case 'crypt':
if(@defined('CRYPT_STD_DES') && CRYPT_STD_DES == 1)
{
$salt = self::randomstring(2);
return crypt($password,$salt);
}
self::$error = 'no std crypt';
$e_password = '{PLAIN}'.$password;
break;
case 'blowfish_crypt':
if(@defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1)
{
$salt = '$2$' . self::randomstring(13);
return crypt($password,$salt);
}
self::$error = 'no blowfish crypt';
break;
case 'md5_crypt':
if(@defined('CRYPT_MD5') && CRYPT_MD5 == 1)
{
$salt = '$1$' . self::randomstring(9);
return crypt($password,$salt);
}
self::$error = 'no md5 crypt';
break;
case 'ext_crypt':
if(@defined('CRYPT_EXT_DES') && CRYPT_EXT_DES == 1)
{
$salt = self::randomstring(9);
return crypt($password,$salt);
}
self::$error = 'no ext crypt';
break;
case 'smd5':
$salt = self::randomstring(16);
$hash = md5($password . $salt,true);
return '{SMD5}' . base64_encode($hash . $salt);
case 'sha':
return '{SHA}' . base64_encode(sha1($password,true));
case 'ssha':
$salt = self::randomstring(16);
$hash = sha1($password . $salt,true);
return '{SSHA}' . base64_encode($hash . $salt);
case 'md5':
default:
/* This is the old standard for password storage in SQL */
return md5($password);
}
if (!self::$error)
{
$e_password = md5($password);
break;
// all other types are identical to ldap, so no need to doublicate the code here
case 'des':
case 'blowish_crypt': // was for some time a typo in setup
case 'crypt':
case 'sha256_crypt':
case 'sha512_crypt':
case 'blowfish_crypt':
case 'md5_crypt':
case 'ext_crypt':
case 'smd5':
case 'sha':
case 'ssha':
$e_password = self::encrypt_ldap($password, $type);
break;
default:
self::$error = 'no valid encryption available';
$e_password = false;
break;
}
return False;
error_log(__METHOD__."('$password') using '$type' returning ".array2string($e_password).(self::$error ? ' error='.self::$error : ''));
return $e_password;
}
/**
@ -584,31 +621,6 @@ class auth
return strcmp($orig_hash,$new_hash) == 0;
}
/**
* compare crypted passwords for authentication whether des,ext_des,md5, or blowfish crypt
*
* @param string $form_val user input value for comparison
* @param string $db_val stored value (from database)
* @param string $type crypt() type
* @return boolean True on successful comparison
*/
static function crypt_compare($form_val,$db_val,$type)
{
$saltlen = array(
'blowfish_crypt' => 16,
'md5_crypt' => 12,
'ext_crypt' => 9,
'crypt' => 2
);
// PHP's crypt(): salt + hash
// notice: "The encryption type is triggered by the salt argument."
$salt = substr($db_val, 0, (int)$saltlen[$type]);
$new_hash = crypt($form_val, $salt);
return strcmp($db_val,$new_hash) == 0;
}
/**
* compare md5_hmac-encrypted passwords for authentication (see RFC2104)
*

View File

@ -128,8 +128,10 @@ class auth_ldap implements auth_backend
// try to query password from ldap server (might fail because of ACL) and check if we need to migrate the hash
if (($sri = ldap_search($ldap, $userDN,"(objectclass=*)", array('userPassword'))) &&
($values = ldap_get_entries($ldap, $sri)) && isset($values[0]['userpassword'][0]) &&
($type = preg_match('/^{(.+)}/',$values[0]['userpassword'][0],$matches) ? $matches[1] : 'plain') &&
in_array(strtolower($type),explode(',',strtolower($GLOBALS['egw_info']['server']['pwd_migration_types']))))
($type = preg_match('/^{(.+)}/',$values[0]['userpassword'][0],$matches) ? strtolower($matches[1]) : 'plain') &&
// for crypt use auth::crypt_compare to detect correct sub-type, strlen("{crypt}")=7
($type != 'crypt' || auth::crypt_compare($passwd, substr($values[0]['userpassword'][0], 7), $type)) &&
in_array($type, explode(',',strtolower($GLOBALS['egw_info']['server']['pwd_migration_types']))))
{
$this->change_password($passwd, $passwd, $allValues[0]['uidnumber'][0], false);
}

View File

@ -69,23 +69,28 @@ class auth_sql implements auth_backend
{
return false;
}
if(!($match = auth::compare_password($passwd,$row['account_pwd'],$this->type,strtolower($username))) ||
preg_match('/^{(.+)}/',$row['account_pwd'],$matches) && // explicit specified hash, eg. from ldap
in_array(strtolower($matches[1]),explode(',',strtolower($GLOBALS['egw_info']['server']['pwd_migration_types']))))
if(!($match = auth::compare_password($passwd, $row['account_pwd'], $this->type, strtolower($username), $type)) ||
$type != $this->type && in_array($type, explode(',',strtolower($GLOBALS['egw_info']['server']['pwd_migration_types']))))
{
// do we have to migrate an old password ?
if($GLOBALS['egw_info']['server']['pwd_migration_allowed'] && !empty($GLOBALS['egw_info']['server']['pwd_migration_types']))
{
if (!$match)
{
foreach(explode(',', $GLOBALS['egw_info']['server']['pwd_migration_types']) as $type)
{
if(($match = auth::compare_password($passwd,$row['account_pwd'],$type,strtolower($username))))
{
$encrypted_passwd = auth::encrypt_sql($passwd);
$this->_update_passwd($encrypted_passwd,$passwd,$row['account_id'],false,true);
break;
}
}
}
if ($match)
{
$encrypted_passwd = auth::encrypt_sql($passwd);
$this->_update_passwd($encrypted_passwd,$passwd,$row['account_id'],false,true);
}
}
if (!$match) return false;
}
}

View File

@ -169,7 +169,7 @@ class setup_cmd_config extends setup_cmd
'--account-auth' => array(
array('name' => 'account_repository','allowed' => array('sql','ldap'),'default'=>'sql'),
array('name' => 'auth_type','allowed' => array('sql','ldap','mail','ads','http','sqlssl','nis','pam'),'default'=>'sql'),
array('name' => 'sql_encryption','allowed' => array('ssha','smd5','md5','blowfish_crypt','md5_crypt','crypt'),'default'=>'ssha'),
array('name' => 'sql_encryption','allowed' => array('sha512_crypt','sha256_crypt','blowfish_crypt','md5_crypt','crypt','ssha','smd5','md5'),'default'=>'sha512_crypt'),
'check_save_password','allow_cookie_auth'),
'--ldap-host' => 'ldap_host',
'--ldap-root-dn' => 'ldap_root_dn',

View File

@ -295,8 +295,10 @@ 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';
// use securest password hash by default
require_once './hook_config.inc.php'; // for sql_passwdhashes, to get securest available password hash
sql_passwdhashes(array(), true, $securest);
$current_config['sql_encryption_type'] = $current_config['ldap_encryption_type'] = $securest;
if ($preset_config)
{

View File

@ -147,55 +147,44 @@ function encryptmode($config)
function passwdhashes($config,$return_hashes=false)
{
$hashes = array(
'ssha' => 'ssha'.' ('.lang('default').')',
'smd5' => 'smd5',
'sha' => 'sha',
'des' => 'des', // historically crypt is called des in ldap
);
/* Check for available crypt methods based on what is defined by php */
if(@defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1)
$hashes = sql_passwdhashes($config,true);
if (isset($hashes['crypt']))
{
$hashes['blowish_crypt'] = 'blowish_crypt';
$hashes['des'] = 'des (=crypt)'; // old LDAP name for crypt
}
if(@defined('CRYPT_MD5') && CRYPT_MD5 == 1)
{
$hashes['md5_crypt'] = 'md5_crypt';
}
if(@defined('CRYPT_EXT_DES') && CRYPT_EXT_DES == 1)
{
$hashes['ext_crypt'] = 'ext_crypt';
}
$hashes += array(
'md5' => 'md5',
'plain' => 'plain',
);
return $return_hashes ? $hashes : _options_from($hashes, $config['ldap_encryption_type'] ? $config['ldap_encryption_type'] : 'des');
}
function sql_passwdhashes($config,$return_hashes=false)
function sql_passwdhashes($config, $return_hashes=false, &$securest=null)
{
$hashes = array(
'ssha' => 'ssha'.' ('.lang('default').')',
'smd5' => 'smd5',
'sha' => 'sha',
);
$hashes = array();
/* Check for available crypt methods based on what is defined by php */
if(@defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1)
if(defined('CRYPT_SHA512') && CRYPT_SHA512 == 1)
{
$hashes['blowish_crypt'] = 'blowish_crypt';
$hashes['sha512_crypt'] = 'sha512_crypt';
}
if(@defined('CRYPT_MD5') && CRYPT_MD5 == 1)
if(defined('CRYPT_SHA256') && CRYPT_SHA256 == 1)
{
$hashes['sha256_crypt'] = 'sha256_crypt';
}
if(defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1)
{
$hashes['blowfish_crypt'] = 'blowfish_crypt';
}
if(defined('CRYPT_MD5') && CRYPT_MD5 == 1)
{
$hashes['md5_crypt'] = 'md5_crypt';
}
if(@defined('CRYPT_EXT_DES') && CRYPT_EXT_DES == 1)
if(defined('CRYPT_EXT_DES') && CRYPT_EXT_DES == 1)
{
$hashes['ext_crypt'] = 'ext_crypt';
}
$hashes += array(
'ssha' => 'ssha',
'smd5' => 'smd5',
'sha' => 'sha',
);
if(@defined('CRYPT_STD_DES') && CRYPT_STD_DES == 1)
{
$hashes['crypt'] = 'crypt';
@ -206,6 +195,10 @@ function sql_passwdhashes($config,$return_hashes=false)
'plain' => 'plain',
);
// mark the securest algorithm for the user
list($securest) = each($hashes); reset($hashes);
$hashes[$securest] .= ' ('.lang('securest').')';
return $return_hashes ? $hashes : _options_from($hashes, $config['sql_encryption_type'] ? $config['sql_encryption_type'] : 'md5');
}

View File

@ -26,7 +26,7 @@
access denied: wrong username or password for manage-header !!! setup de Zugriff verweigert: Falsche Benutzername oder Passwort für die Headerverwaltung !!!
access denied: wrong username or password to configure the domain '%1(%2)' !!! setup de Zugriff verweigert: Falsche Benutzername oder Passwort für Konfiguration der Domain '%1(%2)' !!!
account repository need to be set to the one you migrate to! setup de Speicherort für Benutzerkonten muss auf den zu migrierenden gesetzt sein!
account repository{sql(default) | ldap},[authentication{sql | ldap | mail | ads | http | ...}],[sql encrypttion{md5 | blowfish_crypt | md5_crypt | crypt}],[check save password{ (default)|true}],[allow cookie auth{ (default)|true}] setup de Benutzer speichern{sql(Vorgabe) | ldap},[Authentifizierung{sql | ldap | mail | ads | http | ...}],[sql Verschlüsselung{ssha(Vorgabe) | smd5 | md5 | blowfish_crypt | md5_crypt | crypt}],[überprüfe Passworte{ (Vorgabe) | True}],[erlaube Cookie Authtentifizierung{ (Vorgabe) | True}]
account repository{sql(default) | ldap},[authentication{sql | ldap | mail | ads | http | ...}],[sql encrypttion{md5 | blowfish_crypt | md5_crypt | crypt}],[check save password{ (default)|true}],[allow cookie auth{ (default)|true}] setup de Benutzer speichern{sql(Vorgabe) | ldap},[Authentifizierung{sql | ldap | mail | ads | http | ...}],[sql Verschlüsselung{sha512_crypt(default) | sha256_crypt | blowfish_crypt | md5_crypt | ssha | smd5 | crypt | md5}],[überprüfe Passworte{ (Vorgabe) | True}],[erlaube Cookie Authtentifizierung{ (Vorgabe) | True}]
accounts existing setup de Benutzerkonten existieren
actions setup de Aktionen
activate safe password check setup de Aktiviere die "sichere Passwort" Überprüfung
@ -544,7 +544,7 @@ smtp server hostname or ip address setup de SMTP Server Hostname oder IP Adresse
smtp server port setup de SMTP Server Port
some or all of its tables are missing setup de Einige oder alle Tabellen fehlen
sources deleted/missing setup de Quellen gelöscht/fehlen
sql encryption type setup de SQL-Verschlüsselungstyp für das Passwort (Vorgabe SSHA)
sql encryption type setup de SQL-Verschlüsselungstyp für das Passwort
ssl validation: setup de SSL Validierung:
standard (login-name identical to egroupware user-name) setup de Standard (Loginname identisch zu eGroupWare Benutzername)
standard mailserver settings (used for mail authentication too) setup de Standard Mailserver Einstellungen (werden auch für die Mail Authentifizierung benutzt)

View File

@ -26,7 +26,7 @@
access denied: wrong username or password for manage-header !!! setup en Access denied: wrong username or password for manage-header !!!
access denied: wrong username or password to configure the domain '%1(%2)' !!! setup en Access denied: wrong username or password to configure the domain '%1(%2)' !!!
account repository need to be set to the one you migrate to! setup en Account repository need to be set to the one you migrate to!
account repository{sql(default) | ldap},[authentication{sql | ldap | mail | ads | http | ...}],[sql encrypttion{md5 | blowfish_crypt | md5_crypt | crypt}],[check save password{ (default)|true}],[allow cookie auth{ (default)|true}] setup en account repository{sql(default) | ldap},[authentication{sql | ldap | mail | ads | http | ...}],[sql encrypttion{ssha(default) | smd5 | md5 | blowfish_crypt | md5_crypt | crypt}],[check save password{ (default)|True}],[allow cookie auth{ (default)|True}]
account repository{sql(default) | ldap},[authentication{sql | ldap | mail | ads | http | ...}],[sql encrypttion{md5 | blowfish_crypt | md5_crypt | crypt}],[check save password{ (default)|true}],[allow cookie auth{ (default)|true}] setup en account repository{sql(default) | ldap},[authentication{sql | ldap | mail | ads | http | ...}],[sql encrypttion{sha512_crypt(default) | sha256_crypt | blowfish_crypt | md5_crypt | ssha | smd5 | crypt | md5}],[check save password{ (default)|True}],[allow cookie auth{ (default)|True}]
accounts existing setup en Accounts existing
actions setup en Actions
activate safe password check setup en Activate safe password check
@ -544,7 +544,7 @@ smtp server hostname or ip address setup en SMTP server hostname or IP address
smtp server port setup en SMTP server port
some or all of its tables are missing setup en Some or all of its tables are missing
sources deleted/missing setup en Sources deleted/missing
sql encryption type setup en SQL encryption type for passwords (default SSHA)
sql encryption type setup en SQL encryption type for passwords
ssl validation: setup en SSL validation:
standard (login-name identical to egroupware user-name) setup en standard (login-name identical to eGroupWare user-name)
standard mailserver settings (used for mail authentication too) setup en Standard mailserver settings (used for Mail authentication too)