From 57ec8b82b52fb5bc70c8268649676de1fbb5e625 Mon Sep 17 00:00:00 2001
From: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date: Sun, 30 Sep 2018 14:47:54 +0100
Subject: [PATCH] Bugfix: Fix inadvertent cost-climbing when recalculating the
 password hashing cost

---
 Changelog.md              |  5 +++++
 build/index.php           | 28 +++++++++++++++++++++-------
 module_index.json         |  2 +-
 modules/page-login.php    | 24 +++++++++++++++++++-----
 peppermint.guiconfig.json |  2 +-
 5 files changed, 47 insertions(+), 14 deletions(-)

diff --git a/Changelog.md b/Changelog.md
index f44bdf8..81a554e 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,6 +1,11 @@
 # 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.
 
+## v0.17-beta2
+
+### Fixed
+ - Fixed the cost-climbing bug in the last beta release
+
 ## v0.17-beta1
 
 ### Added
diff --git a/build/index.php b/build/index.php
index e024db9..449fdb2 100644
--- a/build/index.php
+++ b/build/index.php
@@ -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; " },
 	"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": { "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_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},
@@ -407,7 +407,7 @@ if($settings->sessionprefix == "auto")
 /////////////////////////////////////////////////////////////////////////////
 /** The version of Pepperminty Wiki currently running. */
 $version = "v0.17-beta1";
-$commit = "a45a3aa087752b4f26af0a02785efc8b08436ce2";
+$commit = "1a11c84e1dd0104e1cc9dc44eaaa737982165f4c";
 /// Environment ///
 /** Holds information about the current request environment. */
 $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.
 		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>");
@@ -8121,23 +8137,21 @@ function hash_password_compute_cost() {
 	$props = hash_password_properties();
 	if($props["algorithm"] == PASSWORD_ARGON2I)
 		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
 	
-	$start = microtime(true);
 	do {
 		$props["options"]["cost"]++;
 		$start_i = microtime(true);
 		password_hash("testing", $props["algorithm"], $props["options"]);
 		$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
 		// ....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 < $target_cost_time &&
-		$end_i - $start < $target_cost_time * 10);
+	} while($end_i - $start_i < $target_cost_time);
 	
 	return $props["options"]["cost"];
 }
diff --git a/module_index.json b/module_index.json
index 7674fb1..6014751 100755
--- a/module_index.json
+++ b/module_index.json
@@ -212,7 +212,7 @@
         "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",
-        "lastupdate": 1527246396,
+        "lastupdate": 1538314946,
         "optional": false
     },
     {
diff --git a/modules/page-login.php b/modules/page-login.php
index c0115f0..c61ea0c 100644
--- a/modules/page-login.php
+++ b/modules/page-login.php
@@ -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.
 		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>");
@@ -255,23 +271,21 @@ function hash_password_compute_cost() {
 	$props = hash_password_properties();
 	if($props["algorithm"] == PASSWORD_ARGON2I)
 		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
 	
-	$start = microtime(true);
 	do {
 		$props["options"]["cost"]++;
 		$start_i = microtime(true);
 		password_hash("testing", $props["algorithm"], $props["options"]);
 		$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
 		// ....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 < $target_cost_time &&
-		$end_i - $start < $target_cost_time * 10);
+	} while($end_i - $start_i < $target_cost_time);
 	
 	return $props["options"]["cost"];
 }
diff --git a/peppermint.guiconfig.json b/peppermint.guiconfig.json
index 0f8a0db..f8200f8 100644
--- a/peppermint.guiconfig.json
+++ b/peppermint.guiconfig.json
@@ -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; " },
 	"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": { "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_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},
