diff --git a/publish/addeditor.php b/publish/addeditor.php index 5a9bd64..442354d 100644 --- a/publish/addeditor.php +++ b/publish/addeditor.php @@ -5,6 +5,8 @@ if ( ! isset($_SERVER['HTTPS'])) { require_once("../session.lib.php"); $added = FALSE; $csrf_failed = FALSE; +$hash_failed = FALSE; +$exists = FALSE; if (!isset($_SESSION['editor'])) { header("Location: https://xxx.org.za/publish/login"); return; @@ -16,11 +18,20 @@ if (!isset($_SESSION['editor'])) { $table = "editors"; $username = (string)($_POST['username'] ?? ''); - $password = password_hash((string)($_POST['password'] ?? ''), PASSWORD_DEFAULT); + $password = password_hash((string)($_POST['password'] ?? ''), password_algorithm()); $email = (string)($_POST['email'] ?? ''); + $found = $db->execute_query("SELECT username FROM $table WHERE username = ?", array($username)); $query = "INSERT INTO $table SET `username` = ?, `password` = ?, `email` = ?"; - if ($result = $db->execute_query($query, array($username, $password, $email))) { - $added = TRUE; + if ($found && $found->fetch_assoc()) { + $exists = TRUE; + } else if ($result = $db->execute_query($query, array($username, $password, $email))) { + // If the password column cut the hash short, the editor could never log in + if (password_hash_is_stored($username, $password)) { + $added = TRUE; + } else { + $db->execute_query("DELETE FROM $table WHERE username = ?", array($username)); + $hash_failed = TRUE; + } $is_admin = TRUE; } open_db('close'); @@ -54,6 +65,12 @@ if (!isset($_SESSION['editor'])) {

Your session has expired. Please try again.

+ +

An editor with that username already exists.

+ + +

The editor was not added: the editors.password column is too short for the password hash. Widen it to VARCHAR(255).

+
" method="post"> diff --git a/publish/login.php b/publish/login.php index fc29ab3..cff68e9 100644 --- a/publish/login.php +++ b/publish/login.php @@ -26,12 +26,15 @@ if (isset($_SESSION['editor'])) { $stored_password = $row['password']; else $denied = TRUE; $getid->close(); - open_db('close'); - $compare_password = $_POST['password'] ?? ''; + $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)) { $denied = TRUE; + open_db('close'); } else { + // Move an old hash to the current algorithm while we have the password + rehash_password($username, $compare_password, $stored_password); + open_db('close'); login_editor($username); // Keep user logged in with cookie, if they request it if (isset($_POST['remember'])) { diff --git a/session.lib.php b/session.lib.php index cc2f80d..8f30675 100644 --- a/session.lib.php +++ b/session.lib.php @@ -132,3 +132,44 @@ function login_editor($username) { unset($_SESSION['csrf_token']); $_SESSION['editor'] = $username; } + +/* + * Password hashing: Argon2id where PHP was built with it, otherwise + * bcrypt. Argon2id hashes are about 100 characters long, so the + * editors.password column must hold at least that (VARCHAR(255) is best). + */ +function password_algorithm() { + return defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_BCRYPT; +} + +/* + * Store $hash as the password of $username, and return TRUE if the database kept + * the whole hash. The database must be open. + */ +function store_password_hash($username, $hash) { + global $db; + $db->execute_query("UPDATE editors SET password = ? WHERE username = ?", array($hash, $username)); + return password_hash_is_stored($username, $hash); +} + +// TRUE if the database holds exactly $hash for $username +function password_hash_is_stored($username, $hash) { + global $db; + $check = $db->execute_query("SELECT password FROM editors WHERE username = ?", array($username)); + $row = $check ? $check->fetch_assoc() : NULL; + return $row && $row['password'] === $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 + * hash, put the old one back so that the editor can still log in. + */ +function rehash_password($username, $password, $stored_hash) { + if (!password_needs_rehash($stored_hash, password_algorithm())) + return; + if (!store_password_hash($username, password_hash($password, password_algorithm()))) { + store_password_hash($username, $stored_hash); + error_log("xxx: could not store a new password hash for $username; widen editors.password"); + } +}