Hash the password for an unknown username at login.
An unknown username got a fast answer, which showed which usernames exist.
This commit is contained in:
+3
-4
@@ -29,14 +29,13 @@ if (isset($_SESSION['editor'])) {
|
||||
$getid = $db->execute_query($query, array($username)) or db_fail();
|
||||
$row = $getid->fetch_assoc();
|
||||
|
||||
$stored_password = '';
|
||||
$stored_password = NULL;
|
||||
if (isset($row))
|
||||
$stored_password = $row['password'];
|
||||
$stored_password = (string)$row['password'];
|
||||
else $denied = TRUE;
|
||||
$getid->close();
|
||||
$compare_password = is_string($_POST['password'] ?? NULL) ? $_POST['password'] : '';
|
||||
// password_verify() also accepts hashes made by the old crypt() call
|
||||
if (!password_verify($compare_password, $stored_password)) {
|
||||
if (!editor_password_valid($compare_password, $stored_password)) {
|
||||
$denied = TRUE;
|
||||
record_login_failure($username);
|
||||
open_db('close');
|
||||
|
||||
@@ -211,6 +211,21 @@ function password_hash_is_stored($username, $hash) {
|
||||
return $row && $row['password'] === $hash;
|
||||
}
|
||||
|
||||
/*
|
||||
* TRUE if $password matches $stored_hash. For an unknown editor
|
||||
* ($stored_hash is NULL), it hashes the password anyway, so that the
|
||||
* answer takes as long as for a wrong password, and does not show which
|
||||
* usernames exist.
|
||||
*/
|
||||
function editor_password_valid($password, $stored_hash) {
|
||||
if ($stored_hash === NULL) {
|
||||
password_hash($password, password_algorithm());
|
||||
return FALSE;
|
||||
}
|
||||
// password_verify() also accepts hashes made by the old crypt() call
|
||||
return password_verify($password, $stored_hash);
|
||||
}
|
||||
|
||||
/*
|
||||
* After a successful login, move an old hash (MD5-crypt, bcrypt) to the
|
||||
* current algorithm. If the column is too short and MySQL cut the new
|
||||
|
||||
@@ -23,6 +23,30 @@ class PasswordTest extends TestCase {
|
||||
$this->assertSame(defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_BCRYPT, password_algorithm());
|
||||
}
|
||||
|
||||
public function testEditorPasswordValid(): void {
|
||||
$hash = password_hash('secret', password_algorithm());
|
||||
$this->assertTrue(editor_password_valid('secret', $hash));
|
||||
$this->assertFalse(editor_password_valid('wrong', $hash));
|
||||
$this->assertFalse(editor_password_valid('', ''));
|
||||
$this->assertFalse(editor_password_valid('secret', NULL));
|
||||
$this->assertFalse(editor_password_valid('', NULL));
|
||||
}
|
||||
|
||||
// A fast answer for an unknown username would show which usernames exist
|
||||
public function testAnUnknownEditorTakesAsLongAsAWrongPassword(): void {
|
||||
$hash = password_hash('secret', password_algorithm());
|
||||
$time = function ($stored) {
|
||||
$best = INF;
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$start = hrtime(TRUE);
|
||||
editor_password_valid('wrong', $stored);
|
||||
$best = min($best, hrtime(TRUE) - $start);
|
||||
}
|
||||
return $best;
|
||||
};
|
||||
$this->assertGreaterThan($time($hash) / 2, $time(NULL));
|
||||
}
|
||||
|
||||
public function testHashIsStoredAndChecked(): void {
|
||||
$this->addEditor('old');
|
||||
$hash = password_hash('secret', password_algorithm());
|
||||
|
||||
Reference in New Issue
Block a user