Commit 713d9618 by Scott

Merge branch 'pr/353' into 1.8

Use password_hash functions.
parents 3fd0654f 1442ef15
......@@ -25,7 +25,7 @@
exit;
}
define('QA_DB_VERSION_CURRENT', 61);
define('QA_DB_VERSION_CURRENT', 62);
function qa_db_user_column_type_verify()
......@@ -107,7 +107,8 @@
'avatarwidth' => 'SMALLINT UNSIGNED', // pixel width of stored avatar
'avatarheight' => 'SMALLINT UNSIGNED', // pixel height of stored avatar
'passsalt' => 'BINARY(16)', // salt used to calculate passcheck - null if no password set for direct login
'passcheck' => 'BINARY(20)', // checksum from password and passsalt - null if no passowrd set for direct login
'passcheck' => 'BINARY(20)', // checksum from password and passsalt - null if no password set for direct login
'passhash' => 'VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL', // password_hash
'level' => 'TINYINT UNSIGNED NOT NULL', // basic, editor, admin, etc...
'loggedin' => 'DATETIME NOT NULL', // time of last login
'loginip' => 'INT UNSIGNED NOT NULL', // INET_ATON of IP address of last login
......@@ -1430,13 +1431,13 @@
$keyrecalc['dorecalcpoints'] = true;
break;
// Up to here: Verison 1.7
// Up to here: Version 1.7
case 59:
// upgrade from alpha version removed
break;
// Up to here: Verison 1.7.1
// Up to here: Version 1.7.1
case 60:
// add new category widget - note title must match that from qa_register_core_modules()
......@@ -1459,8 +1460,13 @@
break;
// Up to here: Verison 1.8 alpha
case 62:
// add column to qa_users to handle new bcrypt passwords
qa_db_upgrade_query('ALTER TABLE ^users ADD COLUMN passhash '.$definitions['users']['passhash'].' AFTER passcheck');
qa_db_upgrade_query($locktablesquery);
break;
// Up to here: Version 1.8 alpha
}
qa_db_set_db_version($newversion);
......@@ -1523,4 +1529,4 @@
/*
Omit PHP closing tag to help avoid accidental output
*/
\ No newline at end of file
*/
......@@ -1181,7 +1181,7 @@
{
return array(
'columns' => array(
'^users.userid', 'passsalt', 'passcheck' => 'HEX(passcheck)', 'email', 'level', 'emailcode', 'handle',
'^users.userid', 'passsalt', 'passcheck' => 'HEX(passcheck)', 'passhash', 'email', 'level', 'emailcode', 'handle',
'created' => 'UNIX_TIMESTAMP(created)', 'sessioncode', 'sessionsource', 'flags', 'loggedin' => 'UNIX_TIMESTAMP(loggedin)',
'loginip' => 'INET_NTOA(loginip)', 'written' => 'UNIX_TIMESTAMP(written)', 'writeip' => 'INET_NTOA(writeip)',
'avatarblobid' => 'BINARY avatarblobid', // cast to BINARY due to MySQL bug which renders it signed in a union
......
......@@ -44,13 +44,22 @@
{
require_once QA_INCLUDE_DIR.'util/string.php';
$salt=isset($password) ? qa_random_alphanum(16) : null;
if (QA_PASSWORD_HASH) {
qa_db_query_sub(
'INSERT INTO ^users (created, createip, email, passhash, level, handle, loggedin, loginip) '.
'VALUES (NOW(), COALESCE(INET_ATON($), 0), $, $, #, $, NOW(), COALESCE(INET_ATON($), 0))',
$ip, $email, isset($password) ? password_hash($password, PASSWORD_BCRYPT) : null, (int)$level, $handle, $ip
);
} else {
$salt = isset($password) ? qa_random_alphanum(16) : null;
qa_db_query_sub(
'INSERT INTO ^users (created, createip, email, passsalt, passcheck, level, handle, loggedin, loginip) '.
'VALUES (NOW(), COALESCE(INET_ATON($), 0), $, $, UNHEX($), #, $, NOW(), COALESCE(INET_ATON($), 0))',
$ip, $email, $salt, isset($password) ? qa_db_calc_passcheck($password, $salt) : null, (int)$level, $handle, $ip
);
}
qa_db_query_sub(
'INSERT INTO ^users (created, createip, email, passsalt, passcheck, level, handle, loggedin, loginip) '.
'VALUES (NOW(), COALESCE(INET_ATON($), 0), $, $, UNHEX($), #, $, NOW(), COALESCE(INET_ATON($), 0))',
$ip, $email, $salt, isset($password) ? qa_db_calc_passcheck($password, $salt) : null, (int)$level, $handle, $ip
);
return qa_db_last_insert_id();
}
......@@ -154,12 +163,19 @@
require_once QA_INCLUDE_DIR.'util/string.php';
$salt=qa_random_alphanum(16);
if (QA_PASSWORD_HASH) {
qa_db_query_sub(
'UPDATE ^users SET passhash=$, passsalt=NULL, passcheck=NULL WHERE userid=$',
password_hash($password, PASSWORD_BCRYPT), $userid
);
} else {
$salt = qa_random_alphanum(16);
qa_db_query_sub(
'UPDATE ^users SET passsalt=$, passcheck=UNHEX($) WHERE userid=$',
$salt, qa_db_calc_passcheck($password, $salt), $userid
);
qa_db_query_sub(
'UPDATE ^users SET passsalt=$, passcheck=UNHEX($) WHERE userid=$',
$salt, qa_db_calc_passcheck($password, $salt), $userid
);
}
}
......@@ -335,4 +351,4 @@
/*
Omit PHP closing tag to help avoid accidental output
*/
\ No newline at end of file
*/
......@@ -37,7 +37,7 @@
if (QA_FINAL_EXTERNAL_USERS)
qa_fatal_error('User accounts are handled by external code');
$userid=qa_get_logged_in_userid();
$userid = qa_get_logged_in_userid();
if (!isset($userid))
qa_redirect('login');
......@@ -52,10 +52,16 @@
qa_db_userfields_selectspec()
);
$changehandle=qa_opt('allow_change_usernames') || ((!$userpoints['qposts']) && (!$userpoints['aposts']) && (!$userpoints['cposts']));
$doconfirms=qa_opt('confirm_user_emails') && ($useraccount['level']<QA_USER_LEVEL_EXPERT);
$isconfirmed=($useraccount['flags'] & QA_USER_FLAGS_EMAIL_CONFIRMED) ? true : false;
$haspassword=isset($useraccount['passsalt']) && isset($useraccount['passcheck']);
$changehandle = qa_opt('allow_change_usernames') || (!$userpoints['qposts'] && !$userpoints['aposts'] && !$userpoints['cposts']);
$doconfirms = qa_opt('confirm_user_emails') && $useraccount['level'] < QA_USER_LEVEL_EXPERT;
$isconfirmed = ($useraccount['flags'] & QA_USER_FLAGS_EMAIL_CONFIRMED) ? true : false;
$haspasswordold = isset($useraccount['passsalt']) && isset($useraccount['passcheck']);
if (QA_PASSWORD_HASH) {
$haspassword = isset($useraccount['passhash']);
} else {
$haspassword = $haspasswordold;
}
$permit_error = qa_user_permit_error();
$isblocked = $permit_error !== false;
$pending_confirmation = $doconfirms && $permit_error == 'confirm';
......@@ -204,9 +210,18 @@
else {
$errors = array();
if ($haspassword && (strtolower(qa_db_calc_passcheck($inoldpassword, $useraccount['passsalt'])) != strtolower($useraccount['passcheck'])))
$errors['oldpassword'] = qa_lang('users/password_wrong');
$legacyPassError = strtolower(qa_db_calc_passcheck($inoldpassword, $useraccount['passsalt'])) != strtolower($useraccount['passcheck']);
if (QA_PASSWORD_HASH) {
$passError = !password_verify($inoldpassword,$useraccount['passhash']);
if (($haspasswordold && $legacyPassError) || (!$haspasswordold && $haspassword && $passError)) {
$errors['oldpassword'] = qa_lang('users/password_wrong');
}
} else {
if ($haspassword && $legacyPassError) {
$errors['oldpassword'] = qa_lang('users/password_wrong');
}
}
$useraccount['password'] = $inoldpassword;
$errors = $errors + qa_password_validate($innewpassword1, $useraccount); // array union
......@@ -463,7 +478,7 @@
),
);
if (!$haspassword) {
if (!$haspassword && !$haspasswordold) {
$qa_content['form_password']['fields']['old']['type']='static';
$qa_content['form_password']['fields']['old']['value']=qa_lang_html('users/password_none');
}
......
......@@ -68,9 +68,30 @@
$inuserid=$matchusers[0];
$userinfo=qa_db_select_with_pending(qa_db_user_account_selectspec($inuserid, true));
if (strtolower(qa_db_calc_passcheck($inpassword, $userinfo['passsalt'])) == strtolower($userinfo['passcheck'])) { // login and redirect
$legacyPassOk = strtolower(qa_db_calc_passcheck($inpassword, $userinfo['passsalt'])) == strtolower($userinfo['passcheck']);
if (QA_PASSWORD_HASH) {
$haspassword = isset($userinfo['passhash']);
$haspasswordold = isset($userinfo['passsalt']) && isset($userinfo['passcheck']);
$passOk = password_verify($inpassword,$userinfo['passhash']);
if (($haspasswordold && $legacyPassOk) || ($haspassword && $passOk)) {
// upgrade password or rehash, when options like the cost parameter changed
if ($haspasswordold || password_needs_rehash($userinfo['passhash'], PASSWORD_BCRYPT)) {
qa_db_user_set_password($inuserid, $inpassword);
}
} else {
$errors['password']=qa_lang('users/password_wrong');
}
} else {
if (!$legacyPassOk) {
$errors['password']=qa_lang('users/password_wrong');
}
}
if (!isset($errors['password'])) {
// login and redirect
require_once QA_INCLUDE_DIR.'app/users.php';
qa_set_logged_in_user($inuserid, $userinfo['handle'], !empty($inremember));
$topath=qa_get('to');
......@@ -81,9 +102,7 @@
qa_redirect('account');
else
qa_redirect('');
} else
$errors['password']=qa_lang('users/password_wrong');
}
} else
$errors['emailhandle']=qa_lang('users/user_not_found');
......@@ -174,4 +193,4 @@
/*
Omit PHP closing tag to help avoid accidental output
*/
\ No newline at end of file
*/
......@@ -49,25 +49,6 @@
qa_initialize_php();
qa_initialize_constants_1();
/**
* JSON compatibility layer for PHP 5.1
*/
if (!function_exists('json_encode') && !function_exists('json_decode')) {
require_once QA_INCLUDE_DIR.'vendor/JSON.php';
function json_encode($json)
{
$service = new Services_JSON();
return $service->encode($json);
}
function json_decode($json, $assoc = false)
{
$service = new Services_JSON($assoc ? SERVICES_JSON_LOOSE_TYPE : 0);
return $service->decode($json);
}
}
if (defined('QA_WORDPRESS_LOAD_FILE')) // if relevant, load WordPress integration in global scope
require_once QA_WORDPRESS_LOAD_FILE;
......@@ -196,6 +177,31 @@
if (!is_readable(QA_WORDPRESS_LOAD_FILE))
qa_fatal_error('Could not find wp-load.php file for WordPress integration - please check QA_WORDPRESS_INTEGRATE_PATH in qa-config.php');
}
// Polyfills
// JSON compatibility layer for PHP 5.1
if (!function_exists('json_encode') && !function_exists('json_decode')) {
require_once QA_INCLUDE_DIR.'vendor/JSON.php';
function json_encode($json)
{
$service = new Services_JSON();
return $service->encode($json);
}
function json_decode($json, $assoc = false)
{
$service = new Services_JSON($assoc ? SERVICES_JSON_LOOSE_TYPE : 0);
return $service->decode($json);
}
}
// password_hash compatibility for 5.3-5.4
define('QA_PASSWORD_HASH', !qa_php_version_below('5.3.7'));
if (QA_PASSWORD_HASH) {
require_once QA_INCLUDE_DIR.'vendor/password_compat.php';
}
}
......
<?php
/**
* A Compatibility library with PHP 5.5's simplified password hashing API.
*
* @author Anthony Ferrara <ircmaxell@php.net>
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @copyright 2012 The Authors
*/
namespace {
if (!defined('PASSWORD_BCRYPT')) {
/**
* PHPUnit Process isolation caches constants, but not function declarations.
* So we need to check if the constants are defined separately from
* the functions to enable supporting process isolation in userland
* code.
*/
define('PASSWORD_BCRYPT', 1);
define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
define('PASSWORD_BCRYPT_DEFAULT_COST', 10);
}
if (!function_exists('password_hash')) {
/**
* Hash the password using the specified algorithm
*
* @param string $password The password to hash
* @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
* @param array $options The options for the algorithm to use
*
* @return string|false The hashed password, or false on error.
*/
function password_hash($password, $algo, array $options = array()) {
if (!function_exists('crypt')) {
trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
return null;
}
if (is_null($password) || is_int($password)) {
$password = (string) $password;
}
if (!is_string($password)) {
trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
return null;
}
if (!is_int($algo)) {
trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
return null;
}
$resultLength = 0;
switch ($algo) {
case PASSWORD_BCRYPT:
$cost = PASSWORD_BCRYPT_DEFAULT_COST;
if (isset($options['cost'])) {
$cost = (int) $options['cost'];
if ($cost < 4 || $cost > 31) {
trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
return null;
}
}
// The length of salt to generate
$raw_salt_len = 16;
// The length required in the final serialization
$required_salt_len = 22;
$hash_format = sprintf("$2y$%02d$", $cost);
// The expected length of the final crypt() output
$resultLength = 60;
break;
default:
trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
return null;
}
$salt_req_encoding = false;
if (isset($options['salt'])) {
switch (gettype($options['salt'])) {
case 'NULL':
case 'boolean':
case 'integer':
case 'double':
case 'string':
$salt = (string) $options['salt'];
break;
case 'object':
if (method_exists($options['salt'], '__tostring')) {
$salt = (string) $options['salt'];
break;
}
case 'array':
case 'resource':
default:
trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
return null;
}
if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) {
trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING);
return null;
} elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
$salt_req_encoding = true;
}
} else {
$buffer = '';
$buffer_valid = false;
if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
$buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
if ($buffer) {
$buffer_valid = true;
}
}
if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
$strong = false;
$buffer = openssl_random_pseudo_bytes($raw_salt_len, $strong);
if ($buffer && $strong) {
$buffer_valid = true;
}
}
if (!$buffer_valid && @is_readable('/dev/urandom')) {
$file = fopen('/dev/urandom', 'r');
$read = 0;
$local_buffer = '';
while ($read < $raw_salt_len) {
$local_buffer .= fread($file, $raw_salt_len - $read);
$read = PasswordCompat\binary\_strlen($local_buffer);
}
fclose($file);
if ($read >= $raw_salt_len) {
$buffer_valid = true;
}
$buffer = str_pad($buffer, $raw_salt_len, "\0") ^ str_pad($local_buffer, $raw_salt_len, "\0");
}
if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) {
$buffer_length = PasswordCompat\binary\_strlen($buffer);
for ($i = 0; $i < $raw_salt_len; $i++) {
if ($i < $buffer_length) {
$buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
} else {
$buffer .= chr(mt_rand(0, 255));
}
}
}
$salt = $buffer;
$salt_req_encoding = true;
}
if ($salt_req_encoding) {
// encode string with the Base64 variant used by crypt
$base64_digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
$bcrypt64_digits =
'./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$base64_string = base64_encode($salt);
$salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits);
}
$salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len);
$hash = $hash_format . $salt;
$ret = crypt($password, $hash);
if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) {
return false;
}
return $ret;
}
/**
* Get information about the password hash. Returns an array of the information
* that was used to generate the password hash.
*
* array(
* 'algo' => 1,
* 'algoName' => 'bcrypt',
* 'options' => array(
* 'cost' => PASSWORD_BCRYPT_DEFAULT_COST,
* ),
* )
*
* @param string $hash The password hash to extract info from
*
* @return array The array of information about the hash.
*/
function password_get_info($hash) {
$return = array(
'algo' => 0,
'algoName' => 'unknown',
'options' => array(),
);
if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) {
$return['algo'] = PASSWORD_BCRYPT;
$return['algoName'] = 'bcrypt';
list($cost) = sscanf($hash, "$2y$%d$");
$return['options']['cost'] = $cost;
}
return $return;
}
/**
* Determine if the password hash needs to be rehashed according to the options provided
*
* If the answer is true, after validating the password using password_verify, rehash it.
*
* @param string $hash The hash to test
* @param int $algo The algorithm used for new password hashes
* @param array $options The options array passed to password_hash
*
* @return boolean True if the password needs to be rehashed.
*/
function password_needs_rehash($hash, $algo, array $options = array()) {
$info = password_get_info($hash);
if ($info['algo'] !== (int) $algo) {
return true;
}
switch ($algo) {
case PASSWORD_BCRYPT:
$cost = isset($options['cost']) ? (int) $options['cost'] : PASSWORD_BCRYPT_DEFAULT_COST;
if ($cost !== $info['options']['cost']) {
return true;
}
break;
}
return false;
}
/**
* Verify a password against a hash using a timing attack resistant approach
*
* @param string $password The password to verify
* @param string $hash The hash to verify against
*
* @return boolean If the password matches the hash
*/
function password_verify($password, $hash) {
if (!function_exists('crypt')) {
trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
return false;
}
$ret = crypt($password, $hash);
if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) {
return false;
}
$status = 0;
for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) {
$status |= (ord($ret[$i]) ^ ord($hash[$i]));
}
return $status === 0;
}
}
}
namespace PasswordCompat\binary {
if (!function_exists('PasswordCompat\\binary\\_strlen')) {
/**
* Count the number of bytes in a string
*
* We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
* In this case, strlen() will count the number of *characters* based on the internal encoding. A
* sequence of bytes might be regarded as a single multibyte character.
*
* @param string $binary_string The input string
*
* @internal
* @return int The number of bytes
*/
function _strlen($binary_string) {
if (function_exists('mb_strlen')) {
return mb_strlen($binary_string, '8bit');
}
return strlen($binary_string);
}
/**
* Get a substring based on byte limits
*
* @see _strlen()
*
* @param string $binary_string The input string
* @param int $start
* @param int $length
*
* @internal
* @return string The substring
*/
function _substr($binary_string, $start, $length) {
if (function_exists('mb_substr')) {
return mb_substr($binary_string, $start, $length, '8bit');
}
return substr($binary_string, $start, $length);
}
/**
* Check if current PHP version is compatible with the library
*
* @return boolean the check result
*/
function check() {
static $pass = NULL;
if (is_null($pass)) {
if (function_exists('crypt')) {
$hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
$test = crypt("password", $hash);
$pass = $test == $hash;
} else {
$pass = false;
}
}
return $pass;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment