diff --git a/core/src/conf/bootstrap_context.php b/core/src/conf/bootstrap_context.php index 422e07f838..9a4eeecae8 100644 --- a/core/src/conf/bootstrap_context.php +++ b/core/src/conf/bootstrap_context.php @@ -72,6 +72,24 @@ define("AJXP_SERVER_DEBUG" , false); define("AJXP_SKIP_CACHE" , false); + +// PBKDF2 CONSTANTS FOR A SECURE STORAGE OF PASSWORDS +// These constants may be changed without breaking existing hashes. +define("PBKDF2_HASH_ALGORITHM", "sha256"); +define("PBKDF2_ITERATIONS", 1000); +define("PBKDF2_SALT_BYTE_SIZE", 24); +define("PBKDF2_HASH_BYTE_SIZE", 24); + +define("HASH_SECTIONS", 4); +define("HASH_ALGORITHM_INDEX", 0); +define("HASH_ITERATION_INDEX", 1); +define("HASH_SALT_INDEX", 2); +define("HASH_PBKDF2_INDEX", 3); + +// CAN BE SWITCHED TO TRUE TO MAKE THE SECURE TOKEN MORE SAFE +// MAKE SURE YOU HAVE PHP.5.3, OPENSSL, AND THAT IT DOES NOT DEGRADE PERFORMANCES +define("USE_OPENSSL_RANDOM", false); + require(AJXP_BIN_FOLDER."/compat.php"); function AjaXplorer_autoload($className){ diff --git a/core/src/core/classes/class.AJXP_Utils.php b/core/src/core/classes/class.AJXP_Utils.php index 5cfa4d190c..f4ffbc7674 100644 --- a/core/src/core/classes/class.AJXP_Utils.php +++ b/core/src/core/classes/class.AJXP_Utils.php @@ -24,6 +24,27 @@ define('AJXP_SANITIZE_HTML_STRICT', 2); define('AJXP_SANITIZE_ALPHANUM', 3); define('AJXP_SANITIZE_EMAILCHARS', 4); + +// THESE ARE DEFINED IN bootstrap_context.php +// REPEAT HERE FOR BACKWARD COMPATIBILITY. +if(!defined('PBKDF2_HASH_ALGORITHM')){ + + define("PBKDF2_HASH_ALGORITHM", "sha256"); + define("PBKDF2_ITERATIONS", 1000); + define("PBKDF2_SALT_BYTE_SIZE", 24); + define("PBKDF2_HASH_BYTE_SIZE", 24); + + define("HASH_SECTIONS", 4); + define("HASH_ALGORITHM_INDEX", 0); + define("HASH_ITERATION_INDEX", 1); + define("HASH_SALT_INDEX", 2); + define("HASH_PBKDF2_INDEX", 3); + + define("USE_OPENSSL_RANDOM", false); + +} + + /** * Various functions used everywhere, static library * @package AjaXplorer @@ -1603,4 +1624,131 @@ public static function runCreateTablesQuery($p, $file){ } + + /* + * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt + * $algorithm - The hash algorithm to use. Recommended: SHA256 + * $password - The password. + * $salt - A salt that is unique to the password. + * $count - Iteration count. Higher is better, but slower. Recommended: At least 1000. + * $key_length - The length of the derived key in bytes. + * $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise. + * Returns: A $key_length-byte key derived from the password and salt. + * + * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt + * + * This implementation of PBKDF2 was originally created by https://defuse.ca + * With improvements by http://www.variations-of-shadow.com + */ + static function pbkdf2_apply($algorithm, $password, $salt, $count, $key_length, $raw_output = false) { + + $algorithm = strtolower($algorithm); + + if(!in_array($algorithm, hash_algos(), true)) + die('PBKDF2 ERROR: Invalid hash algorithm.'); + if($count <= 0 || $key_length <= 0) + die('PBKDF2 ERROR: Invalid parameters.'); + + $hash_length = strlen(hash($algorithm, "", true)); + $block_count = ceil($key_length / $hash_length); + + $output = ""; + + for($i = 1; $i <= $block_count; $i++) { + // $i encoded as 4 bytes, big endian. + $last = $salt . pack("N", $i); + // first iteration + $last = $xorsum = hash_hmac($algorithm, $last, $password, true); + + // perform the other $count - 1 iterations + for ($j = 1; $j < $count; $j++) { + $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true)); + } + + $output .= $xorsum; + } + + if($raw_output) + return substr($output, 0, $key_length); + else + return bin2hex(substr($output, 0, $key_length)); + } + + + // Compares two strings $a and $b in length-constant time. + static function pbkdf2_slow_equals($a, $b) { + $diff = strlen($a) ^ strlen($b); + for($i = 0; $i < strlen($a) && $i < strlen($b); $i++) + { + $diff |= ord($a[$i]) ^ ord($b[$i]); + } + + return $diff === 0; + } + + static function pbkdf2_validate_password($password, $correct_hash) { + $params = explode(":", $correct_hash); + + if(count($params) < HASH_SECTIONS){ + if(strlen($correct_hash) == 32 && count($params) == 1){ + return md5($password) == $correct_hash; + } + return false; + } + + $pbkdf2 = base64_decode($params[HASH_PBKDF2_INDEX]); + return self::pbkdf2_slow_equals( + $pbkdf2, + self::pbkdf2_apply( + $params[HASH_ALGORITHM_INDEX], + $password, + $params[HASH_SALT_INDEX], + (int)$params[HASH_ITERATION_INDEX], + strlen($pbkdf2), + true + ) + ); + } + + + static function pbkdf2_create_hash($password) { + // format: algorithm:iterations:salt:hash + $salt = base64_encode(mcrypt_create_iv(PBKDF2_SALT_BYTE_SIZE, MCRYPT_DEV_URANDOM)); + return PBKDF2_HASH_ALGORITHM . ":" . PBKDF2_ITERATIONS . ":" . $salt . ":" . + base64_encode(self::pbkdf2_apply( + PBKDF2_HASH_ALGORITHM, + $password, + $salt, + PBKDF2_ITERATIONS, + PBKDF2_HASH_BYTE_SIZE, + true + )); + } + + /** + * generates a random password, uses base64: 0-9a-zA-Z + * @param int [optional] $length length of password, default 24 (144 Bit) + * @return string password + */ + static function generateRandomString($length = 24) { + if(function_exists('openssl_random_pseudo_bytes') && USE_OPENSSL_RANDOM) { + $password = base64_encode(openssl_random_pseudo_bytes($length, $strong)); + if($strong == TRUE) + return substr(str_replace(array("/","+"), "", $password), 0, $length); //base64 is about 33% longer, so we need to truncate the result + } + + //fallback to mt_rand if php < 5.3 or no openssl available + $characters = '0123456789'; + $characters .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + $charactersLength = strlen($characters)-1; + $password = ''; + + //select some random characters + for ($i = 0; $i < $length; $i++) { + $password .= $characters[mt_rand(0, $charactersLength)]; + } + + return $password; + } + } \ No newline at end of file diff --git a/core/src/core/classes/class.AuthService.php b/core/src/core/classes/class.AuthService.php index c1f29f5834..e798c98e4c 100644 --- a/core/src/core/classes/class.AuthService.php +++ b/core/src/core/classes/class.AuthService.php @@ -65,7 +65,8 @@ static function generateSeed(){ * @return */ static function generateSecureToken(){ - $_SESSION["SECURE_TOKEN"] = md5(time()); + + $_SESSION["SECURE_TOKEN"] = AJXP_Utils::generateRandomString(32); //md5(time()); return $_SESSION["SECURE_TOKEN"]; } /** @@ -212,7 +213,7 @@ static function refreshRememberCookie($user){ $user->invalidateCookieString(substr($current, strpos($current, ":")+1)); } $rememberPass = $user->getCookieString(); - setcookie("AjaXplorer-remember", $user->id.":".$rememberPass, time()+3600*24*10); + setcookie("AjaXplorer-remember", $user->id.":".$rememberPass, time()+3600*24*10, null, null, (isSet($_SERVER["HTTPS"]) && strtolower($_SERVER["HTTPS"]) == "on"), true); } /** @@ -233,7 +234,7 @@ static function clearRememberCookie(){ if(!empty($current) && $user != null){ $user->invalidateCookieString(substr($current, strpos($current, ":")+1)); } - setcookie("AjaXplorer-remember", "", time()-3600); + setcookie("AjaXplorer-remember", "", time()-3600, null, null, (isSet($_SERVER["HTTPS"]) && strtolower($_SERVER["HTTPS"]) == "on"), true); } static function logTemporaryUser($parentUserId, $temporaryUserId){ diff --git a/core/src/plugins/auth.remote/class.remoteAuthDriver.php b/core/src/plugins/auth.remote/class.remoteAuthDriver.php index 34e2f7014d..9278a17658 100644 --- a/core/src/plugins/auth.remote/class.remoteAuthDriver.php +++ b/core/src/plugins/auth.remote/class.remoteAuthDriver.php @@ -132,7 +132,7 @@ function checkPassword($login, $pass, $seed){ $userStoredPass = $this->getUserPass($login); if(!$userStoredPass) return false; if($seed == "-1"){ // Seed = -1 means that password is not encoded. - return ($userStoredPass == md5($pass)); + return AJXP_Utils::pbkdf2_validate_password($pass, $userStoredPass);// ($userStoredPass == md5($pass)); }else{ return (md5($userStoredPass.$seed) == $pass); } @@ -164,7 +164,7 @@ function checkPassword($login, $pass, $seed){ $userStoredPass = $this->getUserPass($login); if(!$userStoredPass) return false; if($seed == "-1"){ // Seed = -1 means that password is not encoded. - $res = ($userStoredPass == md5($pass)); + $res = AJXP_Utils::pbkdf2_validate_password($pass, $userStoredPass); //($userStoredPass == md5($pass)); }else{ $res = (md5($userStoredPass.$seed) == $pass); } @@ -195,7 +195,7 @@ function createUser($login, $passwd){ if(!is_array($users)) $users = array(); if(array_key_exists($login, $users)) return "exists"; if($this->getOption("TRANSMIT_CLEAR_PASS") === true){ - $users[$login] = md5($passwd); + $users[$login] = AJXP_Utils::pbkdf2_create_hash($passwd); }else{ $users[$login] = $passwd; } @@ -206,7 +206,7 @@ function changePassword($login, $newPass){ $users = $this->listUsers(); if(!is_array($users) || !array_key_exists($login, $users)) return ; if($this->getOption("TRANSMIT_CLEAR_PASS") === true){ - $users[$login] = md5($newPass); + $users[$login] = AJXP_Utils::pbkdf2_create_hash($newPass); }else{ $users[$login] = $newPass; } diff --git a/core/src/plugins/auth.serial/class.serialAuthDriver.php b/core/src/plugins/auth.serial/class.serialAuthDriver.php index f0f7b7abad..5c0a81d225 100644 --- a/core/src/plugins/auth.serial/class.serialAuthDriver.php +++ b/core/src/plugins/auth.serial/class.serialAuthDriver.php @@ -106,7 +106,7 @@ function checkPassword($login, $pass, $seed){ $userStoredPass = $this->getUserPass($login); if(!$userStoredPass) return false; if($seed == "-1"){ // Seed = -1 means that password is not encoded. - return ($userStoredPass == md5($pass)); + return AJXP_Utils::pbkdf2_validate_password($pass, $userStoredPass);//($userStoredPass == md5($pass)); }else{ return (md5($userStoredPass.$seed) == $pass); } @@ -125,7 +125,7 @@ function createUser($login, $passwd){ if(!is_array($users)) $users = array(); if(array_key_exists($login, $users)) return "exists"; if($this->getOption("TRANSMIT_CLEAR_PASS") === true){ - $users[$login] = md5($passwd); + $users[$login] = AJXP_Utils::pbkdf2_create_hash($passwd);//md5($passwd); }else{ $users[$login] = $passwd; } @@ -136,7 +136,7 @@ function changePassword($login, $newPass){ $users = $this->_listAllUsers(); if(!is_array($users) || !array_key_exists($login, $users)) return ; if($this->getOption("TRANSMIT_CLEAR_PASS") === true){ - $users[$login] = md5($newPass); + $users[$login] = AJXP_Utils::pbkdf2_create_hash($newPass);//md5($newPass); }else{ $users[$login] = $newPass; } diff --git a/core/src/plugins/auth.serial_otp/class.serial_otpAuthDriver.php b/core/src/plugins/auth.serial_otp/class.serial_otpAuthDriver.php index fa303073a9..55f3a7214e 100644 --- a/core/src/plugins/auth.serial_otp/class.serial_otpAuthDriver.php +++ b/core/src/plugins/auth.serial_otp/class.serial_otpAuthDriver.php @@ -133,7 +133,7 @@ function checkPassword($login, $pass, $seed){ //No OTP token is set if ($g == '' and $y1 == '' and $y2 == ''){ - return ($userStoredPass == md5($pass)); + return AJXP_Utils::pbkdf2_validate_password($pass, $userStoredPass); //($userStoredPass == md5($pass)); } //Just the Google Authenticator set @@ -171,7 +171,7 @@ function createUser($login, $passwd){ if(!is_array($users)) $users = array(); if(array_key_exists($login, $users)) return "exists"; if($this->getOption("TRANSMIT_CLEAR_PASS") === true){ - $users[$login] = md5($passwd); + $users[$login] = AJXP_Utils::pbkdf2_create_hash($passwd); }else{ $users[$login] = $passwd; } @@ -183,7 +183,7 @@ function changePassword($login, $newPass){ $users = $this->_listAllUsers(); if(!is_array($users) || !array_key_exists($login, $users)) return ; if($this->getOption("TRANSMIT_CLEAR_PASS") === true){ - $users[$login] = md5($newPass); + $users[$login] = AJXP_Utils::pbkdf2_create_hash($newPass); }else{ $users[$login] = $newPass; } @@ -314,7 +314,7 @@ function checkGooglePass($login, $pass, $userStoredPass, $userToken, $userInvali } } - return ($userStoredPass == md5($pass) && $valid == 1); + return ( AJXP_Utils::pbkdf2_validate_password($pass, $userStoredPass) && $valid == 1); } @@ -336,7 +336,7 @@ function checkYubiPass($pass, $userStoredPass, $yubikey1, $yubikey2) { $yubi = new Auth_Yubico($this->yubico_client_id, $this->yubico_secret_key); $auth = $yubi->verify($yotp); - return ((!PEAR::isError($auth)) && $userStoredPass == md5($pass)); + return ((!PEAR::isError($auth)) && AJXP_Utils::pbkdf2_validate_password($pass, $userStoredPass)); } } ?> diff --git a/core/src/plugins/auth.sql/class.sqlAuthDriver.php b/core/src/plugins/auth.sql/class.sqlAuthDriver.php index fabb0bd6c8..6861d3c4a6 100644 --- a/core/src/plugins/auth.sql/class.sqlAuthDriver.php +++ b/core/src/plugins/auth.sql/class.sqlAuthDriver.php @@ -99,7 +99,7 @@ function checkPassword($login, $pass, $seed){ if(!$userStoredPass) return false; if($this->getOption("TRANSMIT_CLEAR_PASS") === true){ // Seed = -1 means that password is not encoded. - return ($userStoredPass == md5($pass)); + return AJXP_Utils::pbkdf2_validate_password($pass, $userStoredPass); //($userStoredPass == md5($pass)); }else{ return (md5($userStoredPass.$seed) == $pass); } @@ -116,7 +116,7 @@ function createUser($login, $passwd){ if($this->userExists($login)) return "exists"; $userData = array("login" => $login); if($this->getOption("TRANSMIT_CLEAR_PASS") === true){ - $userData["password"] = md5($passwd); + $userData["password"] = AJXP_Utils::pbkdf2_create_hash($passwd); //md5($passwd); }else{ $userData["password"] = $passwd; } @@ -127,7 +127,7 @@ function changePassword($login, $newPass){ if(!$this->userExists($login)) throw new Exception("User does not exists!"); $userData = array("login" => $login); if($this->getOption("TRANSMIT_CLEAR_PASS") === true){ - $userData["password"] = md5($newPass); + $userData["password"] = AJXP_Utils::pbkdf2_create_hash($newPass); //md5($newPass); }else{ $userData["password"] = $newPass; } diff --git a/core/src/plugins/core.conf/class.AbstractAjxpUser.php b/core/src/plugins/core.conf/class.AbstractAjxpUser.php index 2805b03dd4..dffaed5fae 100644 --- a/core/src/plugins/core.conf/class.AbstractAjxpUser.php +++ b/core/src/plugins/core.conf/class.AbstractAjxpUser.php @@ -95,11 +95,11 @@ function getCookieString(){ }else{ $hashes = explode(",", $hashes); } - $newHash = md5($this->id.":".time()); + $newHash = md5($this->id.":".AJXP_Utils::generateRandomString()); array_push($hashes, $newHash); $this->setPref("cookie_hash", implode(",",$hashes)); $this->save("user"); - return $newHash; //md5($this->id.":".$newHash.":ajxp"); + return $newHash; } function getId(){