From ac442755b476ec15b269be0d6a6c68e5080a6b21 Mon Sep 17 00:00:00 2001 From: Robert Kaiser Date: Thu, 27 Oct 2016 01:18:24 +0200 Subject: [PATCH] convert AuthUtils to a non-static class and instantiate it as an object, support site-wide nonces in settings --- authsystem.inc.php | 1 + authutils.php-class | 82 ++++++++++++++++++++++++++++++++------------- index.php | 38 ++++++++++----------- 3 files changed, 79 insertions(+), 42 deletions(-) diff --git a/authsystem.inc.php b/authsystem.inc.php index 53462c1..a30e2ab 100644 --- a/authsystem.inc.php +++ b/authsystem.inc.php @@ -16,6 +16,7 @@ require_once('../kairo/include/cbsm/util/document.php-class'); require_once('../kairo/include/classes/email.php-class'); // Class for sending emails require_once(__DIR__.'/authutils.php-class'); +$utils = new AuthUtils(array()); bindtextdomain('kairo_auth', 'en'); // XXX: Should negotiate locale. bind_textdomain_codeset('kairo_auth', 'utf-8'); diff --git a/authutils.php-class b/authutils.php-class index dfb89a2..2d4573f 100755 --- a/authutils.php-class +++ b/authutils.php-class @@ -7,39 +7,56 @@ class AuthUtils { // KaiRo.at authentication utilities PHP class // This class contains helper functions for the authentication system. // - // private static $pwd_cost - // Store cost parameter for use with PHP password_hash function. + // function __construct() + // CONSTRUCTOR // - // static function checkPasswordConstraints($new_password, $user_email) + // private $pwd_cost + // The cost parameter for use with PHP password_hash function. + // + // private $pwd_nonces + // The array of nonces to use for "peppering" passwords. For new hashes, the last one of those will be used. + // + // function checkPasswordConstraints($new_password, $user_email) // Check password constraints and return an array of error messages (empty if all constraints are met). // - // static function createSessionKey() + // function createSessionKey() // Return a random session key. // - // static function createVerificationCode() + // function createVerificationCode() // Return a random acount/email verification code. // - // static function createTimeCode($session, [$offset], [$validity_minutes]) + // function createTimeCode($session, [$offset], [$validity_minutes]) // Return a time-based code based on the key and ID of the given session. // An offset can be given to create a specific code for verification, otherwise and offset will be generated. // Also, an amount of minutes for the code to stay valid can be handed over, by default 10 minutes will be used. // - // static function verifyTimeCode($timecode_to_verify, $session, [$validity_minutes]) + // function verifyTimeCode($timecode_to_verify, $session, [$validity_minutes]) // Verify a given time-based code and return true if it's valid or false if it's not. // See createTimeCode() documentation for the session and validity paramerters. // - // static function pwdHash($new_password) + // function pwdHash($new_password) // Return a hash for the given password. // - // static function pwdVerify($password_to_verify, $user) + // function pwdVerify($password_to_verify, $user) // Return true if the password verifies against the pwdhash field of the user, false if not. // - // static function pwdNeedsRehash($user) + // function pwdNeedsRehash($user) // Return true if the pwdhash field of the user uses an outdated standard and needs to be rehashed. - private static $pwd_cost = 10; + function __construct($settings) { + // *** constructor *** + if (array_key_exists('pwd_nonces', $settings)) { + $this->pwd_nonces = $settings['pwd_nonces']; + } + if (array_key_exists('pwd_cost', $settings)) { + $this->pwd_cost = $settings['pwd_cost']; + } + } + + private $pwd_cost = 10; + private $pwd_nonces = array(); - static function checkPasswordConstraints($new_password, $user_email) { + function checkPasswordConstraints($new_password, $user_email) { $errors = array(); if ($new_password != trim($new_password)) { $errors[] = _('Password must not start or end with a whitespace character like a space.'); @@ -62,15 +79,15 @@ class AuthUtils { return $errors; } - static function createSessionKey() { + function createSessionKey() { return bin2hex(openssl_random_pseudo_bytes(512 / 8)); // Get 512 bits of randomness (128 byte hex string). } - static function createVerificationCode() { + function createVerificationCode() { return bin2hex(openssl_random_pseudo_bytes(512 / 8)); // Get 512 bits of randomness (128 byte hex string). } - static function createTimeCode($session, $offset = null, $validity_minutes = 10) { + function createTimeCode($session, $offset = null, $validity_minutes = 10) { // Matches TOTP algorithms, see https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm $valid_seconds = intval($validity_minutes) * 60; if ($valid_seconds < 60) { $valid_seconds = 60; } @@ -85,23 +102,42 @@ class AuthUtils { return $rest.'.'.$totp_value; } - static function verifyTimeCode($timecode_to_verify, $session, $validity_minutes = 10) { + function verifyTimeCode($timecode_to_verify, $session, $validity_minutes = 10) { if (preg_match('/^(\d+)\.\d+$/', $timecode_to_verify, $regs)) { - return ($timecode_to_verify === self::createTimeCode($session, $regs[1], $validity_minutes)); + return ($timecode_to_verify === $this->createTimeCode($session, $regs[1], $validity_minutes)); } return false; } - static function pwdHash($new_password) { - return password_hash($new_password, PASSWORD_DEFAULT, array('cost' => self::$pwd_cost)); + function pwdHash($new_password) { + $hash_prefix = ''; + if (count($this->pwd_nonces)) { + $new_password .= $this->pwd_nonces[count($this->pwd_nonces) - 1]; + $hash_prefix = (count($this->pwd_nonces) - 1).'|'; + } + return $hash_prefix.password_hash($new_password, PASSWORD_DEFAULT, array('cost' => $this->pwd_cost)); } - static function pwdVerify($password_to_verify, $userdata) { - return password_verify($password_to_verify, $userdata['pwdhash'])); + function pwdVerify($password_to_verify, $userdata) { + if (preg_match('/^(\d+)\|/', $userdata['pwdhash'], $regs)) { + $password_to_verify .= $this->pwd_nonces[$regs[1]]; + } + return password_verify($password_to_verify, $userdata['pwdhash']); } - static function pwdNeedsRehash($userdata) { - return password_needs_rehash($userdata['pwdhash'], PASSWORD_DEFAULT, array('cost' => self::$pwd_cost)); + function pwdNeedsRehash($userdata) { + $nonceid = -1; + $pwdhash = $userdata['pwdhash']; + if (preg_match('/^(\d+)\|(.+)$/', $userdata['pwdhash'], $regs)) { + $nonceid = $regs[1]; + $pwdhash = $regs[2]; + } + if ($nonceid == count($this->pwd_nonces) - 1) { + return password_needs_rehash($pwdhash, PASSWORD_DEFAULT, array('cost' => $this->pwd_cost)); + } + else { + return true; + } } } ?> diff --git a/index.php b/index.php index 3febbbf..9a6e74f 100644 --- a/index.php +++ b/index.php @@ -53,18 +53,18 @@ if (!count($errors)) { if (!preg_match('/^[^@]+@[^@]+\.[^@]+$/', $_POST['email'])) { $errors[] = _('The email address is invalid.'); } - elseif (AuthUtils::verifyTimeCode(@$_POST['tcode'], $session)) { + elseif ($utils->verifyTimeCode(@$_POST['tcode'], $session)) { $result = $db->prepare('SELECT `id`, `pwdhash`, `email`, `status`, `verify_hash` FROM `auth_users` WHERE `email` = :email;'); $result->execute(array(':email' => $_POST['email'])); $user = $result->fetch(PDO::FETCH_ASSOC); if ($user['id'] && array_key_exists('pwd', $_POST)) { // existing user, check password - if (($user['status'] == 'ok') && AuthUtils::pwdVerify(@$_POST['pwd'], $user)) { + if (($user['status'] == 'ok') && $utils->pwdVerify(@$_POST['pwd'], $user)) { // Check if a newer hashing algorithm is available // or the cost has changed - if (AuthUtils::pwdNeedsRehash($user)) { + if ($utils->pwdNeedsRehash($user)) { // If so, create a new hash, and replace the old one - $newHash = AuthUtils::pwdHash($_POST['pwd']); + $newHash = $utils->pwdHash($_POST['pwd']); $result = $db->prepare('UPDATE `auth_users` SET `pwdhash` = :pwdhash WHERE `id` = :userid;'); if (!$result->execute(array(':pwdhash' => $newHash, ':userid' => $user['id']))) { // XXXlog: Failed to update user hash! @@ -72,7 +72,7 @@ if (!count($errors)) { } // Log user in - update session key for that, see https://wiki.mozilla.org/WebAppSec/Secure_Coding_Guidelines#Login - $sesskey = AuthUtils::createSessionKey(); + $sesskey = $utils->createSessionKey(); setcookie('sessionkey', $sesskey, 0, "", "", !$running_on_localhost, true); // Last two params are secure and httponly, secure is not set on localhost. // If the session has a user set, create a new one - otherwise take existing session entry. if (intval($session['user'])) { @@ -115,13 +115,13 @@ if (!count($errors)) { else { // new user: check password, create user and send verification; existing users: re-send verification or send password change instructions if (array_key_exists('pwd', $_POST)) { - $errors += AuthUtils::checkPasswordConstraints(strval($_POST['pwd']), $_POST['email']); + $errors += $utils->checkPasswordConstraints(strval($_POST['pwd']), $_POST['email']); } if (!count($errors)) { // Put user into the DB if (!$user['id']) { - $newHash = AuthUtils::pwdHash($_POST['pwd']); - $vcode = AuthUtils::createVerificationCode(); + $newHash = $utils->pwdHash($_POST['pwd']); + $vcode = $utils->createVerificationCode(); $result = $db->prepare('INSERT INTO `auth_users` (`email`, `pwdhash`, `status`, `verify_hash`) VALUES (:email, :pwdhash, \'unverified\', :vcode);'); if (!$result->execute(array(':email' => $_POST['email'], ':pwdhash' => $newHash, ':vcode' => $vcode))) { // XXXlog: User insertion failure! @@ -162,14 +162,14 @@ if (!count($errors)) { } else { // Password reset requested with "Password forgotten?" function. - $vcode = AuthUtils::createVerificationCode(); + $vcode = $utils->createVerificationCode(); $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = :vcode WHERE `id` = :userid;'); if (!$result->execute(array(':vcode' => $vcode, ':userid' => $user['id']))) { // XXXlog: User insertion failure! $errors[] = _('Could not initiate reset request. Please contact KaiRo.at and tell the team about this.'); } else { - $resetcode = $vcode.dechex($user['id'] + $session['id']).'_'.AuthUtils::createTimeCode($session, null, 60); + $resetcode = $vcode.dechex($user['id'] + $session['id']).'_'.$utils->createTimeCode($session, null, 60); // Send email with instructions for resetting the password. $mail = new email(); $mail->setCharset('utf-8'); @@ -248,9 +248,9 @@ if (!count($errors)) { if ($row) { $tcode_session = $row; if (($regs[1] == $user['verify_hash']) && - AuthUtils::verifyTimeCode($regs[3], $session, 60)) { + $utils->verifyTimeCode($regs[3], $session, 60)) { // Set a new verify_hash for the actual password reset. - $user['verify_hash'] = AuthUtils::createVerificationCode(); + $user['verify_hash'] = $utils->createVerificationCode(); $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = :vcode WHERE `id` = :userid;'); if (!$result->execute(array(':vcode' => $user['verify_hash'], ':userid' => $user['id']))) { // XXXlog: Unexpected failure to reset verify_hash! @@ -287,12 +287,12 @@ if (!count($errors)) { $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.'); } // Check validity of time code. - if (!count($errors) && !AuthUtils::verifyTimeCode($_POST['tcode'], $session)) { + if (!count($errors) && !$utils->verifyTimeCode($_POST['tcode'], $session)) { $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.'); } - $errors += AuthUtils::checkPasswordConstraints(strval($_POST['pwd']), $user['email']); + $errors += $utils->checkPasswordConstraints(strval($_POST['pwd']), $user['email']); if (!count($errors)) { - $newHash = AuthUtils::pwdHash($_POST['pwd']); + $newHash = $utils->pwdHash($_POST['pwd']); $result = $db->prepare('UPDATE `auth_users` SET `pwdhash` = :pwdhash, `verify_hash` = \'\' WHERE `id` = :userid;'); if (!$result->execute(array(':pwdhash' => $newHash, ':userid' => $session['user']))) { // XXXlog: Password reset failure! @@ -308,7 +308,7 @@ if (!count($errors)) { } if (is_null($session)) { // Create new session and set cookie. - $sesskey = AuthUtils::createSessionKey(); + $sesskey = $utils->createSessionKey(); setcookie('sessionkey', $sesskey, 0, "", "", !$running_on_localhost, true); // Last two params are secure and httponly, secure is not set on localhost. $result = $db->prepare('INSERT INTO `auth_sessions` (`sesskey`, `time_expire`) VALUES (:sesskey, :expire);'); $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s', strtotime('+5 minutes')))); @@ -350,7 +350,7 @@ if (!count($errors)) { $inptxt->setAttribute('required', ''); $inptxt->setAttribute('placeholder', _('Email')); $litem = $ulist->appendElement('li'); - $litem->appendInputHidden('tcode', AuthUtils::createTimeCode($session)); + $litem->appendInputHidden('tcode', $utils->createTimeCode($session)); $submit = $litem->appendInputSubmit(_('Send instructions to email')); } elseif ($pagetype == 'resetpwd') { @@ -373,7 +373,7 @@ if (!count($errors)) { $inptxt->setAttribute('class', 'login'); $litem = $ulist->appendElement('li'); $litem->appendInputHidden('reset', ''); - $litem->appendInputHidden('tcode', AuthUtils::createTimeCode($session)); + $litem->appendInputHidden('tcode', $utils->createTimeCode($session)); if (!$session['logged_in'] && strlen(@$user['verify_hash'])) { $litem->appendInputHidden('vcode', $user['verify_hash']); } @@ -429,7 +429,7 @@ if (!count($errors)) { $label->setAttribute('id', 'rememprompt'); $label->setAttribute('class', 'loginprompt'); $litem = $ulist->appendElement('li'); - $litem->appendInputHidden('tcode', AuthUtils::createTimeCode($session)); + $litem->appendInputHidden('tcode', $utils->createTimeCode($session)); $submit = $litem->appendInputSubmit(_('Log in / Register')); $submit->setAttribute('class', 'loginbutton'); } -- 2.43.0