Merge pull request #927 from Zankaria/better-hash

Better hashing
This commit is contained in:
Lorenzo Yario 2025-04-18 10:36:13 -07:00 committed by GitHub
commit dcbb985eab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 53 additions and 44 deletions

View File

@ -36,6 +36,7 @@
"inc/functions.php", "inc/functions.php",
"inc/functions/dice.php", "inc/functions/dice.php",
"inc/functions/format.php", "inc/functions/format.php",
"inc/functions/hide.php",
"inc/functions/net.php", "inc/functions/net.php",
"inc/functions/num.php", "inc/functions/num.php",
"inc/functions/theme.php", "inc/functions/theme.php",

View File

@ -2101,7 +2101,7 @@
// Password hashing method version // Password hashing method version
// If set to 0, it won't upgrade hashes using old password encryption schema, only create new. // 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. // 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? // Use CAPTCHA for reports?
$config['report_captcha'] = false; $config['report_captcha'] = false;

View File

@ -10,6 +10,8 @@ if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) {
exit; exit;
} }
use Vichan\Functions\Hide;
$microtime_start = microtime(true); $microtime_start = microtime(true);
// the user is not currently logged in as a moderator // 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 // Use SHA1 for the hash
$_hash = sha1($_hash . $extra_salt); $_hash = sha1($_hash . $extra_salt);
if ($hash != $_hash) if ($hash != $_hash) {
return true; return true;
}
$query = prepare('SELECT `passed` FROM ``antispam`` WHERE `hash` = :hash'); $query = prepare('SELECT `passed` FROM ``antispam`` WHERE `hash` = :hash');
$query->bindValue(':hash', $hash); $query->bindValue(':hash', $hash);
@ -2443,11 +2446,11 @@ function rrmdir($dir) {
function poster_id($ip, $thread) { function poster_id($ip, $thread) {
global $config; global $config;
if ($id = event('poster-id', $ip, $thread)) if ($id = event('poster-id', $ip, $thread)) {
return $id; return $id;
}
// Confusing, hard to brute-force, but simple algorithm return \substr(Hide\secure_hash($ip . $config['secure_trip_salt'] . $thread . $config['secure_trip_salt'], false), 0, $config['poster_id_length']);
return substr(sha1(sha1($ip . $config['secure_trip_salt'] . $thread) . $config['secure_trip_salt']), 0, $config['poster_id_length']);
} }
function generate_tripcode($name) { function generate_tripcode($name) {
@ -2475,7 +2478,7 @@ function generate_tripcode($name) {
if (isset($config['custom_tripcode']["##{$trip}"])) if (isset($config['custom_tripcode']["##{$trip}"]))
$trip = $config['custom_tripcode']["##{$trip}"]; $trip = $config['custom_tripcode']["##{$trip}"];
else 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 { } else {
if (isset($config['custom_tripcode']["#{$trip}"])) if (isset($config['custom_tripcode']["#{$trip}"]))
$trip = $config['custom_tripcode']["#{$trip}"]; $trip = $config['custom_tripcode']["#{$trip}"];

6
inc/functions/hide.php Normal file
View File

@ -0,0 +1,6 @@
<?php
namespace Vichan\Functions\Hide;
function secure_hash(string $data, bool $binary): string {
return \hash('tiger160,3', $data, $binary);
}

View File

@ -5,7 +5,7 @@
*/ */
use Vichan\Context; use Vichan\Context;
use Vichan\Functions\Net; use Vichan\Functions\{Hide, Net};
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
@ -14,27 +14,28 @@ function mkhash(string $username, ?string $password, mixed $salt = false): array
global $config; global $config;
if (!$salt) { if (!$salt) {
// create some sort of salt for the hash // Create some salt for the hash.
$salt = substr(base64_encode(sha1(rand() . time(), true) . $config['cookies']['salt']), 0, 15); $salt = \bin2hex(\random_bytes(15)); // 20 characters.
$generated_salt = true; $generated_salt = true;
} else {
$generated_salt = false;
} }
// generate hash (method is not important as long as it's strong) // generate hash (method is not important as long as it's strong)
$hash = substr( $hash = \substr(
base64_encode( Hide\secure_hash(
md5( $username . $config['cookies']['salt'] . Hide\secure_hash(
$username . $config['cookies']['salt'] . sha1( $username . $password . $salt . (
$username . $password . $salt . ( $config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : ''
$config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : '' ), true
), true ) . Hide\secure_hash($config['password_crypt_version'], true), // Log out users being logged in with older password encryption schema
) . sha1($config['password_crypt_version']) // Log out users being logged in with older password encryption schema false
, true ),
) 0,
), 0, 20 40
); );
if (isset($generated_salt)) { if ($generated_salt) {
return [ $hash, $salt ]; return [ $hash, $salt ];
} else { } else {
return $hash; return $hash;
@ -45,26 +46,24 @@ function crypt_password(string $password): array {
global $config; global $config;
// `salt` database field is reused as a version value. We don't want it to be 0. // `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; $version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1;
$new_salt = generate_salt(); $pre_hash = \hash('tiger160,3', $password, false); // Note that it's truncated to 72 in the next line.
$password = crypt($password, $config['password_crypt'] . $new_salt . "$"); $r = \password_hash($pre_hash, \PASSWORD_BCRYPT, [ 'cost' => 12 ]);
return [ $version, $password ]; if ($r === false) {
} throw new \RuntimeException("Could not hash password");
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));
} else {
$comp = crypt($test, $password);
} }
return [ $version, hash_equals($password, $comp) ];
return [ $version, $r ];
} }
function generate_salt(): string { function test_password(string $db_hash, string|int $version, string $input_password): bool {
return strtr(base64_encode(random_bytes(16)), '+', '.'); $version = (int)$version;
if ($version < 2) {
$ok = \hash_equals($db_hash, \crypt($input_password, $db_hash));
} else {
$pre_hash = \hash('tiger160,3', $input_password, false);
$ok = \password_verify($pre_hash, $db_hash);
}
return $ok;
} }
function calc_cookie_name(bool $is_https, bool $is_path_jailed, string $base_name): string { function calc_cookie_name(bool $is_https, bool $is_path_jailed, string $base_name): string {
@ -80,24 +79,24 @@ function calc_cookie_name(bool $is_https, bool $is_path_jailed, string $base_nam
} }
function login(string $username, string $password): array|false { 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 = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username");
$query->bindValue(':username', $username); $query->bindValue(':username', $username);
$query->execute() or error(db_error($query)); $query->execute();
if ($user = $query->fetch(PDO::FETCH_ASSOC)) { 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 ($ok) {
if ($config['password_crypt_version'] > $version) { if ((int)$user['version'] < 2) {
// It's time to upgrade the password hashing method! // It's time to upgrade the password hashing method!
list ($user['version'], $user['password']) = crypt_password($password); list ($user['version'], $user['password']) = crypt_password($password);
$query = prepare("UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id"); $query = prepare("UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id");
$query->bindValue(':password', $user['password']); $query->bindValue(':password', $user['password']);
$query->bindValue(':version', $user['version']); $query->bindValue(':version', $user['version']);
$query->bindValue(':id', $user['id']); $query->bindValue(':id', $user['id']);
$query->execute() or error(db_error($query)); $query->execute();
} }
return $mod = [ return $mod = [