From 624b629ff36da7afd84163c6aaa53b16e214fe0a Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 16 Apr 2025 14:32:14 +0200 Subject: [PATCH 01/11] hide.php: add hide.php to the functions --- inc/functions/hide.php | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 inc/functions/hide.php diff --git a/inc/functions/hide.php b/inc/functions/hide.php new file mode 100644 index 00000000..bf972751 --- /dev/null +++ b/inc/functions/hide.php @@ -0,0 +1,6 @@ + Date: Wed, 16 Apr 2025 14:32:54 +0200 Subject: [PATCH 02/11] composer: add hide.php --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 4661b165..48dec807 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "inc/functions.php", "inc/functions/dice.php", "inc/functions/format.php", + "inc/functions/hide.php", "inc/functions/net.php", "inc/functions/num.php", "inc/functions/theme.php", From 3ea2fb4cd4b6c838f0f9e67750a7078cebc61eb7 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 16 Apr 2025 14:38:07 +0200 Subject: [PATCH 03/11] functions.php: use secure_hash where appropriate --- inc/functions.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/inc/functions.php b/inc/functions.php index 8379fcc2..1787b440 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -10,6 +10,8 @@ if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { exit; } +use Vichan\Functions\Hide; + $microtime_start = microtime(true); // the user is not currently logged in as a moderator @@ -1605,8 +1607,9 @@ function checkSpam(array $extra_salt = array()) { // Use SHA1 for the hash $_hash = sha1($_hash . $extra_salt); - if ($hash != $_hash) + if ($hash != $_hash) { return true; + } $query = prepare('SELECT `passed` FROM ``antispam`` WHERE `hash` = :hash'); $query->bindValue(':hash', $hash); @@ -2443,11 +2446,11 @@ function rrmdir($dir) { function poster_id($ip, $thread) { global $config; - if ($id = event('poster-id', $ip, $thread)) + if ($id = event('poster-id', $ip, $thread)) { return $id; + } - // Confusing, hard to brute-force, but simple algorithm - return substr(sha1(sha1($ip . $config['secure_trip_salt'] . $thread) . $config['secure_trip_salt']), 0, $config['poster_id_length']); + return \substr(Hide\secure_hash($ip . $config['secure_trip_salt'] . $thread . $config['secure_trip_salt'], false), 0, $config['poster_id_length']); } function generate_tripcode($name) { @@ -2475,7 +2478,7 @@ function generate_tripcode($name) { if (isset($config['custom_tripcode']["##{$trip}"])) $trip = $config['custom_tripcode']["##{$trip}"]; else - $trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(base64_encode(sha1($trip . $config['secure_trip_salt'], true)), 0, 4))), -10); + $trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(Hide\secure_hash($trip . $config['secure_trip_salt'], false), 0, 4))), -10); } else { if (isset($config['custom_tripcode']["#{$trip}"])) $trip = $config['custom_tripcode']["#{$trip}"]; From d91ae6e3e53dd8e5fa93791abfc743544d7a4576 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 16 Apr 2025 14:26:41 +0200 Subject: [PATCH 04/11] auth.php: use secure salt source, use a cryptographically secure hashing algorithm for login tokens --- inc/mod/auth.php | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index f6a65db2..83d6e769 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -5,7 +5,7 @@ */ use Vichan\Context; -use Vichan\Functions\Net; +use Vichan\Functions\{Hide, Net}; defined('TINYBOARD') or exit; @@ -14,27 +14,28 @@ function mkhash(string $username, ?string $password, mixed $salt = false): array global $config; if (!$salt) { - // create some sort of salt for the hash - $salt = substr(base64_encode(sha1(rand() . time(), true) . $config['cookies']['salt']), 0, 15); - + // Create some salt for the hash. + $salt = \bin2hex(\random_bytes(15)); // 20 characters. $generated_salt = true; + } else { + $generated_salt = false; } // generate hash (method is not important as long as it's strong) - $hash = substr( - base64_encode( - md5( - $username . $config['cookies']['salt'] . sha1( - $username . $password . $salt . ( - $config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : '' - ), true - ) . sha1($config['password_crypt_version']) // Log out users being logged in with older password encryption schema - , true - ) - ), 0, 20 + $hash = \substr( + Hide\secure_hash( + $username . $config['cookies']['salt'] . Hide\secure_hash( + $username . $password . $salt . ( + $config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : '' + ), true + ) . Hide\secure_hash($config['password_crypt_version'], true), // Log out users being logged in with older password encryption schema + false + ), + 0, + 40 ); - if (isset($generated_salt)) { + if ($generated_salt) { return [ $hash, $salt ]; } else { return $hash; From 0b560b235361cf6f27193fc92d0f5b975944006c Mon Sep 17 00:00:00 2001 From: fowr <89118232+perdedora@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:31:18 -0300 Subject: [PATCH 05/11] auth.php: use password_hash with bcrypt and password_verify for login --- inc/mod/auth.php | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 83d6e769..54dce356 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -46,22 +46,18 @@ function crypt_password(string $password): array { global $config; // `salt` database field is reused as a version value. We don't want it to be 0. $version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1; - $new_salt = generate_salt(); - $password = crypt($password, $config['password_crypt'] . $new_salt . "$"); - return [ $version, $password ]; + $hash = \password_hash($password, \PASSWORD_BCRYPT); + return [$version, $hash]; } -function test_password(string $password, string $salt, string $test): array { - // Version = 0 denotes an old password hashing schema. In the same column, the - // password hash was kept previously - $version = strlen($salt) <= 8 ? (int)$salt : 0; - - if ($version == 0) { - $comp = hash('sha256', $salt . sha1($test)); +function test_password(string $db_hash, string|int $version, string $input_password): array { + $version = (int)$version; + if ($version < 2) { + $ok = \hash_equals($db_hash, \crypt($input_password, $db_hash)); } else { - $comp = crypt($test, $password); + $ok = \password_verify($input_password, $db_hash); } - return [ $version, hash_equals($password, $comp) ]; + return [$version, $ok]; } function generate_salt(): string { @@ -91,7 +87,7 @@ function login(string $username, string $password): array|false { list($version, $ok) = test_password($user['password'], $user['version'], $password); if ($ok) { - if ($config['password_crypt_version'] > $version) { + if ((int)$user['version'] < 2) { // It's time to upgrade the password hashing method! list ($user['version'], $user['password']) = crypt_password($password); $query = prepare("UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id"); From 88c863a43a3d5c2f09256e8d3a4ff693aa103417 Mon Sep 17 00:00:00 2001 From: fowr <89118232+perdedora@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:31:35 -0300 Subject: [PATCH 06/11] config.php: bump password crypt version --- inc/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/config.php b/inc/config.php index cb6aee95..a1044906 100644 --- a/inc/config.php +++ b/inc/config.php @@ -2101,7 +2101,7 @@ // Password hashing method version // If set to 0, it won't upgrade hashes using old password encryption schema, only create new. // You can set it to a higher value, to further migrate to other password hashing function. - $config['password_crypt_version'] = 1; + $config['password_crypt_version'] = 2; // Use CAPTCHA for reports? $config['report_captcha'] = false; From 69144087be60ce0ae930d3598586cce8a1a0358a Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 16 Apr 2025 19:18:50 +0200 Subject: [PATCH 07/11] auth.php: use pre-hashing for BCRYPT --- inc/mod/auth.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 54dce356..6927d5b3 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -46,8 +46,13 @@ function crypt_password(string $password): array { global $config; // `salt` database field is reused as a version value. We don't want it to be 0. $version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1; - $hash = \password_hash($password, \PASSWORD_BCRYPT); - return [$version, $hash]; + $pre_hash = \hash('tiger160,3', $password, false); // Note that it's truncated to 72 in the next line. + $r = \password_hash($pre_hash, \PASSWORD_BCRYPT); + if ($r === false) { + throw new \RuntimeException("Could not hash password"); + } + + return [ $version, $r ]; } function test_password(string $db_hash, string|int $version, string $input_password): array { @@ -55,9 +60,10 @@ function test_password(string $db_hash, string|int $version, string $input_passw if ($version < 2) { $ok = \hash_equals($db_hash, \crypt($input_password, $db_hash)); } else { - $ok = \password_verify($input_password, $db_hash); + $pre_hash = \hash('tiger160,3', $input_password, false); + $ok = \password_verify($pre_hash, $db_hash); } - return [$version, $ok]; + return [ $version, $ok ]; } function generate_salt(): string { From 92ea93cec5bff4db3e3fab65b664405d4030c787 Mon Sep 17 00:00:00 2001 From: fowr <89118232+perdedora@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:44:37 -0300 Subject: [PATCH 08/11] auth.php: no need to repass version anymore in test_password --- inc/mod/auth.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 6927d5b3..13b0f8b5 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -55,7 +55,7 @@ function crypt_password(string $password): array { return [ $version, $r ]; } -function test_password(string $db_hash, string|int $version, string $input_password): array { +function test_password(string $db_hash, string|int $version, string $input_password): bool { $version = (int)$version; if ($version < 2) { $ok = \hash_equals($db_hash, \crypt($input_password, $db_hash)); @@ -63,7 +63,7 @@ function test_password(string $db_hash, string|int $version, string $input_passw $pre_hash = \hash('tiger160,3', $input_password, false); $ok = \password_verify($pre_hash, $db_hash); } - return [ $version, $ok ]; + return $ok; } function generate_salt(): string { @@ -90,7 +90,7 @@ function login(string $username, string $password): array|false { $query->execute() or error(db_error($query)); if ($user = $query->fetch(PDO::FETCH_ASSOC)) { - list($version, $ok) = test_password($user['password'], $user['version'], $password); + $ok = test_password($user['password'], $user['version'], $password); if ($ok) { if ((int)$user['version'] < 2) { From a16f02ce51a5bad8dc2e08ad0d97de4a97916039 Mon Sep 17 00:00:00 2001 From: fowr <89118232+perdedora@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:46:11 -0300 Subject: [PATCH 09/11] auth.php: cleanup ununsed function --- inc/mod/auth.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 13b0f8b5..55ac64f8 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -66,10 +66,6 @@ function test_password(string $db_hash, string|int $version, string $input_passw return $ok; } -function generate_salt(): string { - return strtr(base64_encode(random_bytes(16)), '+', '.'); -} - function calc_cookie_name(bool $is_https, bool $is_path_jailed, string $base_name): string { if ($is_https) { if ($is_path_jailed) { From 93fc8f713b1432eafcf5f2692d1a035c8c23e3f2 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 16 Apr 2025 21:06:03 +0200 Subject: [PATCH 10/11] auth.php: remove unused global --- inc/mod/auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 55ac64f8..4e5d27a5 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -79,7 +79,7 @@ function calc_cookie_name(bool $is_https, bool $is_path_jailed, string $base_nam } function login(string $username, string $password): array|false { - global $mod, $config; + global $mod; $query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username"); $query->bindValue(':username', $username); From e3fdb8f3fed89afbe65b0ca4574f52e300170f0d Mon Sep 17 00:00:00 2001 From: Zankaria Date: Wed, 16 Apr 2025 21:18:30 +0200 Subject: [PATCH 11/11] auth.php: use php 8.4 cost for bcrypt --- inc/mod/auth.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 4e5d27a5..69256e03 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -47,7 +47,7 @@ function crypt_password(string $password): array { // `salt` database field is reused as a version value. We don't want it to be 0. $version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1; $pre_hash = \hash('tiger160,3', $password, false); // Note that it's truncated to 72 in the next line. - $r = \password_hash($pre_hash, \PASSWORD_BCRYPT); + $r = \password_hash($pre_hash, \PASSWORD_BCRYPT, [ 'cost' => 12 ]); if ($r === false) { throw new \RuntimeException("Could not hash password"); } @@ -83,7 +83,7 @@ function login(string $username, string $password): array|false { $query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username"); $query->bindValue(':username', $username); - $query->execute() or error(db_error($query)); + $query->execute(); if ($user = $query->fetch(PDO::FETCH_ASSOC)) { $ok = test_password($user['password'], $user['version'], $password); @@ -96,7 +96,7 @@ function login(string $username, string $password): array|false { $query->bindValue(':password', $user['password']); $query->bindValue(':version', $user['version']); $query->bindValue(':id', $user['id']); - $query->execute() or error(db_error($query)); + $query->execute(); } return $mod = [