Bugfix: Fix inadvertent cost-climbing when recalculating the password hashing cost

This commit is contained in:
Starbeamrainbowlabs 2018-09-30 14:47:54 +01:00
parent 1a11c84e1d
commit 57ec8b82b5
Signed by: sbrl
GPG Key ID: 1BE5172E637709C2
5 changed files with 47 additions and 14 deletions

View File

@ -1,6 +1,11 @@
# Changelog # Changelog
This file holds the changelog for Pepperminty Wiki. This is the master list of things that have changed (second only to the commit history!) - though the information for any particular release can also be found in the description of it's page for every release made on GitHub too. This file holds the changelog for Pepperminty Wiki. This is the master list of things that have changed (second only to the commit history!) - though the information for any particular release can also be found in the description of it's page for every release made on GitHub too.
## v0.17-beta2
### Fixed
- Fixed the cost-climbing bug in the last beta release
## v0.17-beta1 ## v0.17-beta1
### Added ### Added

View File

@ -59,7 +59,7 @@ $guiConfig = <<<'GUICONFIG'
"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; " }, "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; " },
"password_algorithm": { "type": "text", "description": "The algorithm to utilise when hashing passwords. Takes any value PHP's password_hash() does.", "default": "PASSWORD_DEFAULT" }, "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": { "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": { "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": 350},
"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}, "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},
"password_cost_time_lastcheck": { "type": "number", "description": "Pseudo-setting used to keep track of the last recalculation of password_cost. Is updated with the current unix timestamp every time password_cost is recalculated.", "default": 0}, "password_cost_time_lastcheck": { "type": "number", "description": "Pseudo-setting used to keep track of the last recalculation of password_cost. Is updated with the current unix timestamp every time password_cost is recalculated.", "default": 0},
"new_password_length": { "type": "number", "description": "The length of newly-generated passwords. This is currently used in the user table when creating new accounts.", "default": 32}, "new_password_length": { "type": "number", "description": "The length of newly-generated passwords. This is currently used in the user table when creating new accounts.", "default": 32},
@ -407,7 +407,7 @@ if($settings->sessionprefix == "auto")
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
/** The version of Pepperminty Wiki currently running. */ /** The version of Pepperminty Wiki currently running. */
$version = "v0.17-beta1"; $version = "v0.17-beta1";
$commit = "a45a3aa087752b4f26af0a02785efc8b08436ce2"; $commit = "1a11c84e1dd0104e1cc9dc44eaaa737982165f4c";
/// Environment /// /// Environment ///
/** Holds information about the current request environment. */ /** Holds information about the current request environment. */
$env = new stdClass(); $env = new stdClass();
@ -8016,6 +8016,22 @@ register_module([
} }
}); });
add_action("hash-cost-test", function() {
header("content-type: text/plain");
$time_compute = microtime(true);
$cost = hash_password_compute_cost();
$time_compute = (microtime(true) - $time_compute)*1000;
$time_cost = microtime(true);
password_hash("testing", PASSWORD_DEFAULT, [ "cost" => $cost ]);
$time_cost = (microtime(true) - $time_cost)*1000;
echo("Calculated cost: $cost ({$time_cost}ms)\n");
echo("Time taken: {$time_compute}ms\n");
exit(date("r"));
});
// Register a section on logging in on the help page. // Register a section on logging in on the help page.
add_help_section("30-login", "Logging in", "<p>In order to edit $settings->sitename and have your edit attributed to you, you need to be logged in. Depending on the settings, logging in may be a required step if you want to edit at all. Thankfully, loggging in is not hard. Simply click the &quot;Login&quot; link in the top left, type your username and password, and then click login.</p> add_help_section("30-login", "Logging in", "<p>In order to edit $settings->sitename and have your edit attributed to you, you need to be logged in. Depending on the settings, logging in may be a required step if you want to edit at all. Thankfully, loggging in is not hard. Simply click the &quot;Login&quot; link in the top left, type your username and password, and then click login.</p>
<p>If you do not have an account yet and would like one, try contacting <a href='mailto:" . hide_email($settings->admindetails_email) . "'>$settings->admindetails_name</a>, $settings->sitename's administrator and ask them nicely to see if they can create you an account.</p>"); <p>If you do not have an account yet and would like one, try contacting <a href='mailto:" . hide_email($settings->admindetails_email) . "'>$settings->admindetails_name</a>, $settings->sitename's administrator and ask them nicely to see if they can create you an account.</p>");
@ -8121,23 +8137,21 @@ function hash_password_compute_cost() {
$props = hash_password_properties(); $props = hash_password_properties();
if($props["algorithm"] == PASSWORD_ARGON2I) if($props["algorithm"] == PASSWORD_ARGON2I)
return null; return null;
if(!is_int($props["options"]["cost"])) $props["options"]["cost"] = 10;
$props["options"]["cost"] = 10;
$target_cost_time = $settings->password_cost_time / 1000; // The setting is in ms $target_cost_time = $settings->password_cost_time / 1000; // The setting is in ms
$start = microtime(true);
do { do {
$props["options"]["cost"]++; $props["options"]["cost"]++;
$start_i = microtime(true); $start_i = microtime(true);
password_hash("testing", $props["algorithm"], $props["options"]); password_hash("testing", $props["algorithm"], $props["options"]);
$end_i = microtime(true); $end_i = microtime(true);
echo("Attempt | cost = {$props["options"]["cost"]}, time = " . ($end_i - $start_i)*1000 . "ms\n");
// Iterate until we find a cost high enough // Iterate until we find a cost high enough
// ....but don't keep going forever - try for at most 10x the target // ....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 // time in total (in case the specified algorithm doesn't take a
// cost parameter) // cost parameter)
} while($end_i - $start_i < $target_cost_time && } while($end_i - $start_i < $target_cost_time);
$end_i - $start < $target_cost_time * 10);
return $props["options"]["cost"]; return $props["options"]["cost"];
} }

View File

@ -212,7 +212,7 @@
"author": "Starbeamrainbowlabs", "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.", "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", "id": "page-login",
"lastupdate": 1527246396, "lastupdate": 1538314946,
"optional": false "optional": false
}, },
{ {

View File

@ -150,6 +150,22 @@ register_module([
} }
}); });
add_action("hash-cost-test", function() {
header("content-type: text/plain");
$time_compute = microtime(true);
$cost = hash_password_compute_cost();
$time_compute = (microtime(true) - $time_compute)*1000;
$time_cost = microtime(true);
password_hash("testing", PASSWORD_DEFAULT, [ "cost" => $cost ]);
$time_cost = (microtime(true) - $time_cost)*1000;
echo("Calculated cost: $cost ({$time_cost}ms)\n");
echo("Time taken: {$time_compute}ms\n");
exit(date("r"));
});
// Register a section on logging in on the help page. // Register a section on logging in on the help page.
add_help_section("30-login", "Logging in", "<p>In order to edit $settings->sitename and have your edit attributed to you, you need to be logged in. Depending on the settings, logging in may be a required step if you want to edit at all. Thankfully, loggging in is not hard. Simply click the &quot;Login&quot; link in the top left, type your username and password, and then click login.</p> add_help_section("30-login", "Logging in", "<p>In order to edit $settings->sitename and have your edit attributed to you, you need to be logged in. Depending on the settings, logging in may be a required step if you want to edit at all. Thankfully, loggging in is not hard. Simply click the &quot;Login&quot; link in the top left, type your username and password, and then click login.</p>
<p>If you do not have an account yet and would like one, try contacting <a href='mailto:" . hide_email($settings->admindetails_email) . "'>$settings->admindetails_name</a>, $settings->sitename's administrator and ask them nicely to see if they can create you an account.</p>"); <p>If you do not have an account yet and would like one, try contacting <a href='mailto:" . hide_email($settings->admindetails_email) . "'>$settings->admindetails_name</a>, $settings->sitename's administrator and ask them nicely to see if they can create you an account.</p>");
@ -255,23 +271,21 @@ function hash_password_compute_cost() {
$props = hash_password_properties(); $props = hash_password_properties();
if($props["algorithm"] == PASSWORD_ARGON2I) if($props["algorithm"] == PASSWORD_ARGON2I)
return null; return null;
if(!is_int($props["options"]["cost"])) $props["options"]["cost"] = 10;
$props["options"]["cost"] = 10;
$target_cost_time = $settings->password_cost_time / 1000; // The setting is in ms $target_cost_time = $settings->password_cost_time / 1000; // The setting is in ms
$start = microtime(true);
do { do {
$props["options"]["cost"]++; $props["options"]["cost"]++;
$start_i = microtime(true); $start_i = microtime(true);
password_hash("testing", $props["algorithm"], $props["options"]); password_hash("testing", $props["algorithm"], $props["options"]);
$end_i = microtime(true); $end_i = microtime(true);
echo("Attempt | cost = {$props["options"]["cost"]}, time = " . ($end_i - $start_i)*1000 . "ms\n");
// Iterate until we find a cost high enough // Iterate until we find a cost high enough
// ....but don't keep going forever - try for at most 10x the target // ....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 // time in total (in case the specified algorithm doesn't take a
// cost parameter) // cost parameter)
} while($end_i - $start_i < $target_cost_time && } while($end_i - $start_i < $target_cost_time);
$end_i - $start < $target_cost_time * 10);
return $props["options"]["cost"]; return $props["options"]["cost"];
} }

View File

@ -36,7 +36,7 @@
"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; " }, "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; " },
"password_algorithm": { "type": "text", "description": "The algorithm to utilise when hashing passwords. Takes any value PHP's password_hash() does.", "default": "PASSWORD_DEFAULT" }, "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": { "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": { "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": 350},
"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}, "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},
"password_cost_time_lastcheck": { "type": "number", "description": "Pseudo-setting used to keep track of the last recalculation of password_cost. Is updated with the current unix timestamp every time password_cost is recalculated.", "default": 0}, "password_cost_time_lastcheck": { "type": "number", "description": "Pseudo-setting used to keep track of the last recalculation of password_cost. Is updated with the current unix timestamp every time password_cost is recalculated.", "default": 0},
"new_password_length": { "type": "number", "description": "The length of newly-generated passwords. This is currently used in the user table when creating new accounts.", "default": 32}, "new_password_length": { "type": "number", "description": "The length of newly-generated passwords. This is currently used in the user table when creating new accounts.", "default": 32},