It's time to get serious with password hashing.

I've left this far to long. Now for the testing.....
This commit is contained in:
Starbeamrainbowlabs 2018-05-10 23:03:26 +01:00
parent 31618dc060
commit 8010770fd4
Signed by: sbrl
GPG Key ID: 1BE5172E637709C2
3 changed files with 104 additions and 21 deletions

View File

@ -678,7 +678,7 @@ function render_editor($editorName)
}
/**
* Saves the currently logged in uesr's data back to peppermint.json.
* Saves the currently logged in user's data back to peppermint.json.
* @package core
* @return bool Whether the user's data was saved successfully. Returns false if the user isn't logged in.
*/

View File

@ -1,7 +1,7 @@
<?php
register_module([
"name" => "Login",
"version" => "0.8.5",
"version" => "0.9",
"author" => "Starbeamrainbowlabs",
"description" => "Adds a pair of actions (login and checklogin) that allow users to login. You need this one if you want your users to be able to login.",
"id" => "page-login",
@ -92,14 +92,20 @@ register_module([
// The user wants to log in
$user = $_POST["user"];
$pass = $_POST["pass"];
if($settings->users->$user->password == hash_password($pass))
if(password_verify($pass, $settings->users->$user->password))
{
// Success! :D
$new_password_hash = hash_password_update($pass, $settings->users->$user->password);
// Update the password hash
if($new_password_hash !== null) {
$env->user_data->password = $new_password_hash;
save_userdata();
error_log("[Pepperminty Wiki] Updated password hash for $user.");
}
$env->is_logged_in = true;
$expiretime = time() + 60*60*24*30; // 30 days from now
$_SESSION["$settings->sessionprefix-user"] = $user;
$_SESSION["$settings->sessionprefix-pass"] = hash_password($pass);
$_SESSION["$settings->sessionprefix-expiretime"] = $expiretime;
$_SESSION["$settings->sessionprefix-pass"] = $hashed_pass;
$_SESSION["$settings->sessionprefix-expiretime"] = time() + 60*60*24*30; // 30 days from now
// Redirect to wherever the user was going
http_response_code(302);
header("x-login-success: yes");
@ -138,6 +144,42 @@ register_module([
}
]);
function do_password_hash_code_update() {
// There's no point if we're using Argon2i, as it doesn't take a cost
if(hash_password_properties()["algorithm"] == PASSWORD_ARGON2I)
return;
// Skip the recheck if we've done one recently
if(isset($settings->password_cost_time_lastcheck) &&
time() - $settings->password_cost_time_lastcheck < $settings->password_cost_time_interval)
return;
$new_cost = hash_password_compute_cost();
// Save the new cost, but only if it's higher than the old one
if($new_cost > $settings->password_hash_cost)
$settings->password_hash_cost = $new_cost;
// Save the current time in the settings
$settings->password_cost_time_lastcheck = time();
file_put_contents($paths->settings_file, json_encode($settings, JSON_PRETTY_PRINT));
}
/**
* Figures out the appropriate algorithm & options for hashing passwords based
* on the current settings.
* @return array The appropriate password hashing algorithm and options.
*/
function hash_password_properties() {
global $settings;
$result = [
"algorithm" => constant($settings->password_algorithm),
"options" => [ "cost" => $settings->password_hash_cost ]
];
if(isset(PASSWORD_ARGON2I) && $result["algorithm"] == PASSWORD_ARGON2I)
$result["options"] = [];
return $result;
}
/**
* Hashes the given password according to the current settings defined
* in $settings.
@ -149,15 +191,53 @@ register_module([
*/
function hash_password($pass)
{
global $settings;
if($settings->use_sha3)
{
return sha3($pass, 256);
}
else
{
return hash("sha256", $pass);
}
$props = hash_password_properties();
return password_hash(
base64_encode(hash("sha384", $pass)),
$props["algorithm"],
$props["options"]
);
}
/**
* Determines if the provided password needs re-hashing or not.
* @param string $pass The password to check.
* @param string $hash The hash of the provided password to check.
* @return string|null Returns null if an updaste is not required - otherwise returns the new updated hash.
*/
function hash_password_update($pass, $hash) {
$props = hash_password_properties();
if(password_needs_rehash($hash, $props["algorithm"], $props["options"])) {
return hash_password($pass);
}
return null;
}
/**
* Computes the appropriate cost value for password_hash based on the settings
* automatically.
* Starts at 10 and works upwards in increments of 1. Goes on until a value is
* found that's greater than the target - or 10x the target time elapses.
* @return integer The automatically calculated password hashing cost.
*/
function hash_password_compute_cost() {
global $settings;
$props = hash_password_properties();
if($props["algorithm"] == PASSWORD_ARGON2I)
return null;
if(!is_int($props["options"]["cost"]))
$props["options"]["cost"] = 10;
$start = microtime(true);
do {
$props["options"]["cost"]++;
$start_i = microtime(true);
password_hash("testing", $props["algorithm"], $props["options"]);
$end_i = microtime(true);
// Iterate until we find a cost high enough
// ....but don't keep going forever - try for at most 10x the target
// time in total (in case the specified algorithm doesn't take a
// cost parameter)
} while($end_i - $start_i < $settings->password_cost_time &&
$end_i - $start < $settings->password_cost_time * 10);
return $props["options"]["cost"];
}
?>

View File

@ -19,21 +19,24 @@
"parser": {"type": "text", "description": "The parser to use when rendering pages. Defaults to an extended version of parsedown (http://parsedown.org/)", "default": "parsedown"},
"clean_raw_html": {"type": "checkbox", "description": "Whether page sources should be cleaned of HTML before rendering. It is STRONGLY recommended that you keep this option turned on.", "default": true},
"enable_math_rendering": {"type": "checkbox", "description": "Whether to enable client side rendering of mathematical expressions with MathJax (https://www.mathjax.org/). Math expressions should be enclosed inside of dollar signs ($). Turn off if you don't use it.", "default": true},
"users": {"type": "usertable", "description": "An array of usernames and passwords - passwords should be hashed with sha256 (or sha3 if you have that option turned on)", "default": {
"users": {"type": "usertable", "description": "An array of usernames and passwords - passwords should be hashed with password_hash() (the hash action can help here)", "default": {
"admin": {
"email": "admin@somewhere.com",
"password": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
"password": "$2y$11$HBk1sh9MGVSM.vM.hZTnBOxk8sJi6PhyN/DRZUy8z845F1wQ4tudS"
},
"user": {
"email": "example@example.net",
"password": "873ac9ffea4dd04fa719e8920cd6938f0c23cd678af330939cff53c3d2855f34"
"password": "$2y$11$DrRmOVRfW0yyYD26JQkiluicOfluGoEgHgKJ7imPDicWABhowP9Fi"
}
}},
"admins": {"type": "array", "description": "An array of usernames that are administrators. Administrators can delete and move pages.", "default": [ "admin" ]},
"anonymous_user_name": { "type": "text", "description": "The default name for anonymous users.", "default": "Anonymous" },
"user_page_prefix": { "type": "text", "description": "The prefix for user pages. All user pages will be considered to be under this page. User pages have special editing restrictions that prevent anyone other thant he user they belong to from editing them. Should not include the trailing forward slash.", "default": "Users" },
"user_preferences_button_text": { "type": "text", "description": "The text to display on the button that lets logged in users change their settings. Defaults to a cog (aka a 'gear' in unicode-land).", "default": "&#x2699; " },
"use_sha3": {"type": "checkbox", "description": "Whether to use the new sha3 hashing algorithm for passwords etc.", "default": false },
"password_algorithm": {"type": "text", "description": "The algorithm to utilise when hashing passwords. Takes any value PHP's password_hash() does.", "default": "PASSWORD_DEFAULT"},
"password_cost": {"type": "number", "description": "The cost to use when hashing passwords.", "default": 12},
"password_cost_time": {"type": "number", "description": "The desired number of milliseconds to delay by when hashing passwords. Pepperminty Wiki will automatically update the value of password_cost to take the length of time specified here. If you're using PASSWORD_ARGON2I, then the auto-update will be disabled.", "default": 100},
"password_cost_time_interval": {"type": "number", "description": "The interval, in seconds, at which the password cost should be recalculated. Set to -1 to disable. Default: 1 week", "default": 604800},
"require_login_view": {"type": "checkbox", "description": "Whether to require that users login before they do anything else. Best used with the data_storage_dir option.", "default": false},
"data_storage_dir": {"type": "text", "description": "The directory in which to store all files, except the main index.php.", "default": "."},
"delayed_indexing_time": {"type": "number", "description": "The amount of time, in seconds, that pages should be blocked from being indexed by search engines after their last edit. Aka delayed indexing.", "default": 0},