From f39e1230c5a413343ce33c979fad7e6430d8a339 Mon Sep 17 00:00:00 2001
From: flashwave <me@flash.moe>
Date: Sun, 9 Feb 2025 00:26:12 +0000
Subject: [PATCH] Moved passwords out of the users table.

---
 ..._moved_passwords_out_of_the_user_table.php | 33 ++++++++
 public-legacy/auth/login.php                  | 23 ++++--
 public-legacy/auth/password.php               |  8 +-
 public-legacy/auth/register.php               | 10 +--
 public-legacy/manage/users/user.php           |  8 +-
 public-legacy/settings/account.php            | 12 +--
 public-legacy/settings/data.php               |  5 +-
 src/Users/UserInfo.php                        | 36 +++------
 src/Users/UserPasswordInfo.php                | 33 ++++++++
 src/Users/UserPasswordsData.php               | 80 +++++++++++++++++++
 src/Users/UserTotpsData.php                   |  2 +-
 src/Users/UsersContext.php                    |  2 +
 src/Users/UsersData.php                       | 47 ++---------
 13 files changed, 202 insertions(+), 97 deletions(-)
 create mode 100644 database/2025_02_08_235046_moved_passwords_out_of_the_user_table.php
 create mode 100644 src/Users/UserPasswordInfo.php
 create mode 100644 src/Users/UserPasswordsData.php

diff --git a/database/2025_02_08_235046_moved_passwords_out_of_the_user_table.php b/database/2025_02_08_235046_moved_passwords_out_of_the_user_table.php
new file mode 100644
index 00000000..4e900324
--- /dev/null
+++ b/database/2025_02_08_235046_moved_passwords_out_of_the_user_table.php
@@ -0,0 +1,33 @@
+<?php
+use Index\Db\DbConnection;
+use Index\Db\Migration\DbMigration;
+
+final class MovedPasswordsOutOfTheUserTable_20250208_235046 implements DbMigration {
+    public function migrate(DbConnection $conn): void {
+        $conn->execute(<<<SQL
+            CREATE TABLE msz_users_passwords (
+                user_id          INT(10) UNSIGNED NOT NULL,
+                password_hash    VARCHAR(255)     NOT NULL COLLATE 'ascii_bin',
+                password_created TIMESTAMP        NOT NULL DEFAULT current_timestamp(),
+                PRIMARY KEY (user_id),
+                CONSTRAINT users_passwords_users_foreign
+                    FOREIGN KEY (user_id)
+                    REFERENCES msz_users (user_id)
+                    ON UPDATE CASCADE
+                    ON DELETE CASCADE
+            ) COLLATE='utf8mb4_bin';
+        SQL);
+
+        $conn->execute(<<<SQL
+            INSERT msz_users_passwords
+            SELECT user_id, user_password, NOW()
+            FROM msz_users
+            WHERE user_password IS NOT NULL AND TRIM(user_password) <> ''
+        SQL);
+
+        $conn->execute(<<<SQL
+            ALTER TABLE msz_users
+                DROP COLUMN user_password;
+        SQL);
+    }
+}
diff --git a/public-legacy/auth/login.php b/public-legacy/auth/login.php
index 613b914d..6c0f4eca 100644
--- a/public-legacy/auth/login.php
+++ b/public-legacy/auth/login.php
@@ -98,19 +98,26 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
         break;
     }
 
-    if(!$userInfo->hasPasswordHash) {
-        $notices[] = 'Your password has been invalidated, please reset it.';
-        break;
-    }
-
-    if($userInfo->deleted || !$userInfo->verifyPassword($_POST['login']['password'])) {
+    if($userInfo->deleted) {
         $msz->authCtx->loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
         $notices[] = $loginFailedError;
         break;
     }
 
-    if($userInfo->passwordNeedsRehash)
-        $msz->usersCtx->users->updateUser($userInfo, password: $_POST['login']['password']);
+    $pwInfo = $msz->usersCtx->passwords->getUserPassword($userInfo);
+    if($pwInfo === null) {
+        $notices[] = 'Your password has been invalidated, please reset it.';
+        break;
+    }
+
+    if(!$pwInfo->verifyPassword($_POST['login']['password'])) {
+        $msz->authCtx->loginAttempts->recordAttempt(false, $ipAddress, $countryCode, $userAgent, $clientInfo, $userInfo);
+        $notices[] = $loginFailedError;
+        break;
+    }
+
+    if($pwInfo->needsRehash)
+        $msz->usersCtx->passwords->updateUserPassword($userInfo, $_POST['login']['password']);
 
     if(!empty($loginPermCat) && $loginPermVal > 0 && !$msz->perms->checkPermissions($loginPermCat, $loginPermVal, $userInfo)) {
         $notices[] = "Login succeeded, but you're not allowed to browse the site right now.";
diff --git a/public-legacy/auth/password.php b/public-legacy/auth/password.php
index 2d4c55f1..2c3936ff 100644
--- a/public-legacy/auth/password.php
+++ b/public-legacy/auth/password.php
@@ -2,7 +2,7 @@
 namespace Misuzu;
 
 use RuntimeException;
-use Misuzu\Users\User;
+use Misuzu\Users\{User,UserPasswordsData};
 
 if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext))
     die('Script must be called through the Misuzu route dispatcher.');
@@ -63,15 +63,15 @@ while($canResetPassword) {
             break;
         }
 
-        $passwordValidation = $msz->usersCtx->users->validatePassword($passwordNew);
+        $passwordValidation = UserPasswordsData::validateUserPassword($passwordNew);
         if($passwordValidation !== '') {
-            $notices[] = $msz->usersCtx->users->validatePasswordText($passwordValidation);
+            $notices[] = UserPasswordsData::validateUserPasswordText($passwordValidation);
             break;
         }
 
         // also disables two factor auth to prevent getting locked out of account entirely
         // this behaviour should really be replaced with recovery keys...
-        $msz->usersCtx->users->updateUser($userInfo, password: $passwordNew);
+        $msz->usersCtx->passwords->updateUserPassword($userInfo, $passwordNew);
         $msz->usersCtx->totps->deleteUserTotp($userInfo);
 
         $msz->createAuditLog('PASSWORD_RESET', [], $userInfo);
diff --git a/public-legacy/auth/register.php b/public-legacy/auth/register.php
index b273db39..0b58127c 100644
--- a/public-legacy/auth/register.php
+++ b/public-legacy/auth/register.php
@@ -2,7 +2,7 @@
 namespace Misuzu;
 
 use RuntimeException;
-use Misuzu\Users\User;
+use Misuzu\Users\{User,UserPasswordsData};
 
 if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext))
     die('Script must be called through the Misuzu route dispatcher.');
@@ -61,11 +61,11 @@ while(!empty($register)) {
         $notices[] = $msz->usersCtx->users->validateEMailAddressText($emailValidation);
 
     if($register['password_confirm'] !== $register['password'])
-        $notices[] = 'The given passwords don\'t match.';
+        $notices[] = "The given passwords don't match.";
 
-    $passwordValidation = $msz->usersCtx->users->validatePassword($register['password']);
+    $passwordValidation = UserPasswordsData::validateUserPassword($register['password']);
     if($passwordValidation !== '')
-        $notices[] = $msz->usersCtx->users->validatePasswordText($passwordValidation);
+        $notices[] = UserPasswordsData::validateUserPasswordText($passwordValidation);
 
     if(!empty($notices))
         break;
@@ -75,12 +75,12 @@ while(!empty($register)) {
     try {
         $userInfo = $msz->usersCtx->users->createUser(
             $register['username'],
-            $register['password'],
             $register['email'],
             $ipAddress,
             $countryCode,
             $defaultRoleInfo
         );
+        $msz->usersCtx->passwords->updateUserPassword($userInfo, $register['password']);
     } catch(RuntimeException $ex) {
         $notices[] = 'Something went wrong while creating your account, please alert an administrator or a developer about this!';
         break;
diff --git a/public-legacy/manage/users/user.php b/public-legacy/manage/users/user.php
index 4d13272d..5e837bb7 100644
--- a/public-legacy/manage/users/user.php
+++ b/public-legacy/manage/users/user.php
@@ -5,7 +5,7 @@ use RuntimeException;
 use Index\Colour\Colour;
 use Misuzu\Perm;
 use Misuzu\Auth\AuthTokenCookie;
-use Misuzu\Users\User;
+use Misuzu\Users\{User,UserPasswordsData};
 
 if(!isset($msz) || !($msz instanceof \Misuzu\MisuzuContext))
     die('Script must be called through the Misuzu route dispatcher.');
@@ -190,13 +190,13 @@ if(CSRF::validateRequest() && $canEdit) {
             if($passwordNewValue !== $passwordConfirmValue)
                 $notices[] = 'Confirm password does not match.';
             else {
-                $passwordValidation = $msz->usersCtx->users->validatePassword($passwordNewValue);
+                $passwordValidation = UserPasswordsData::validateUserPassword($passwordNewValue);
                 if($passwordValidation !== '')
-                   $notices[] = $msz->usersCtx->users->validatePasswordText($passwordValidation);
+                   $notices[] = UserPasswordsData::validateUserPasswordText($passwordValidation);
             }
 
             if(empty($notices))
-                $msz->usersCtx->users->updateUser(userInfo: $userInfo, password: $passwordNewValue);
+                $msz->usersCtx->passwords->updateUserPassword($userInfo, $passwordNewValue);
         }
     }
 
diff --git a/public-legacy/settings/account.php b/public-legacy/settings/account.php
index 77fab736..98a4e7d7 100644
--- a/public-legacy/settings/account.php
+++ b/public-legacy/settings/account.php
@@ -2,7 +2,7 @@
 namespace Misuzu;
 
 use RuntimeException;
-use Misuzu\Users\User;
+use Misuzu\Users\{User,UserPasswordsData};
 use chillerlan\QRCode\QRCode;
 use chillerlan\QRCode\QROptions;
 
@@ -50,7 +50,7 @@ if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
 
 if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $msz->usersCtx->totps->hasUserTotp($userInfo) !== (bool)$_POST['tfa']['enable']) {
     if((bool)$_POST['tfa']['enable']) {
-        $totpInfo = $msz->usersCtx->totps->createUserTotp($userInfo);
+        $totpInfo = $msz->usersCtx->totps->updateUserTotp($userInfo);
         $totpSecret = $totpInfo->encodedSecret;
         $totpIssuer = $msz->siteInfo->name;
         $totpQrcode = (new QRCode(new QROptions([
@@ -74,7 +74,7 @@ if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $msz->usersCtx->totps
 }
 
 if($isVerifiedRequest && !empty($_POST['current_password'])) {
-    if(!$userInfo->verifyPassword($_POST['current_password'] ?? '')) {
+    if(!$msz->usersCtx->passwords->getUserPassword($userInfo)?->verifyPassword($_POST['current_password'] ?? '')) {
         $errors[] = 'Your password was incorrect.';
     } else {
         // Changing e-mail
@@ -100,12 +100,12 @@ if($isVerifiedRequest && !empty($_POST['current_password'])) {
             if(empty($_POST['password']['confirm']) || $_POST['password']['new'] !== $_POST['password']['confirm']) {
                 $errors[] = 'The new passwords you entered did not match each other.';
             } else {
-                $checkPassword = $msz->usersCtx->users->validatePassword($_POST['password']['new']);
+                $checkPassword = UserPasswordsData::validateUserPassword($_POST['password']['new']);
 
                 if($checkPassword !== '') {
-                    $errors[] = $msz->usersCtx->users->validatePasswordText($checkPassword);
+                    $errors[] = UserPasswordsData::validateUserPasswordText($checkPassword);
                 } else {
-                    $msz->usersCtx->users->updateUser(userInfo: $userInfo, password: $_POST['password']['new']);
+                    $msz->usersCtx->passwords->updateUserPassword($userInfo, $_POST['password']['new']);
                     $msz->createAuditLog('PERSONAL_PASSWORD_CHANGE');
                 }
             }
diff --git a/public-legacy/settings/data.php b/public-legacy/settings/data.php
index 94bf64d5..50f7efc0 100644
--- a/public-legacy/settings/data.php
+++ b/public-legacy/settings/data.php
@@ -111,7 +111,7 @@ $userInfo = $msz->authInfo->userInfo;
 
 if(isset($_POST['action']) && is_string($_POST['action'])) {
     if(isset($_POST['password']) && is_string($_POST['password'])
-        && ($userInfo->verifyPassword($_POST['password'] ?? ''))) {
+        && ($msz->usersCtx->passwords->getUserPassword($userInfo)?->verifyPassword($_POST['password'] ?? ''))) {
         switch($_POST['action']) {
             case 'data':
                 $msz->createAuditLog('PERSONAL_DATA_DOWNLOAD');
@@ -151,9 +151,10 @@ if(isset($_POST['action']) && is_string($_POST['action'])) {
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'profile_backgrounds',    ['user_id:s', 'bg_attach:s', 'bg_blend:i', 'bg_slide:i']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'profile_fields_values',  ['field_id:s', 'user_id:s', 'format_id:s', 'field_value:s']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'sessions',               ['session_id:s', 'user_id:s', 'session_key:n', 'session_remote_addr_first:a', 'session_remote_addr_last:a:n', 'session_user_agent:s', 'session_country:s', 'session_expires:t', 'session_expires_bump:b', 'session_created:t', 'session_active:t:n']);
-                            $tmpFiles[] = db_to_zip($archive, $userInfo, 'users',                  ['user_id:s', 'user_name:s', 'user_password:n', 'user_email:s', 'user_remote_addr_first:a', 'user_remote_addr_last:a', 'user_super:b', 'user_country:s', 'user_colour:i:n', 'user_title:s:n', 'user_display_role_id:s:n', 'user_created:t', 'user_active:t:n', 'user_deleted:t:n']);
+                            $tmpFiles[] = db_to_zip($archive, $userInfo, 'users',                  ['user_id:s', 'user_name:s', 'user_email:s', 'user_remote_addr_first:a', 'user_remote_addr_last:a', 'user_super:b', 'user_country:s', 'user_colour:i:n', 'user_title:s:n', 'user_display_role_id:s:n', 'user_created:t', 'user_active:t:n', 'user_deleted:t:n']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_bans',             ['ban_id:s', 'user_id:s', 'mod_id:n', 'ban_severity:i', 'ban_reason_public:s', 'ban_reason_private:s', 'ban_created:t', 'ban_expires:t:n']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_birthdates',       ['user_id:s', 'birth_year:i:n', 'birth_month:i', 'birth_day:i']);
+                            $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_passwords',        ['user_id:s', 'password_hash:n', 'password_created:t']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_password_resets',  ['reset_id:s', 'user_id:s', 'reset_remote_addr:a', 'reset_requested:t', 'reset_code:n']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_roles',            ['user_id:s', 'role_id:s']);
                             $tmpFiles[] = db_to_zip($archive, $userInfo, 'users_totp',             ['user_id:s', 'totp_secret:n', 'totp_created:t']);
diff --git a/src/Users/UserInfo.php b/src/Users/UserInfo.php
index 19fcfaff..d9769ae9 100644
--- a/src/Users/UserInfo.php
+++ b/src/Users/UserInfo.php
@@ -9,7 +9,6 @@ class UserInfo {
     public function __construct(
         public private(set) string $id,
         public private(set) string $name,
-        #[\SensitiveParameter] private ?string $passwordHash,
         #[\SensitiveParameter] public private(set) string $emailAddress,
         public private(set) string $registerRemoteAddress,
         public private(set) string $lastRemoteAddress,
@@ -27,33 +26,20 @@ class UserInfo {
         return new UserInfo(
             id: $result->getString(0),
             name: $result->getString(1),
-            passwordHash: $result->getStringOrNull(2),
-            emailAddress: $result->getString(3),
-            registerRemoteAddress: $result->getString(4),
-            lastRemoteAddress: $result->getStringOrNull(5),
-            super: $result->getBoolean(6),
-            countryCode: $result->getString(7),
-            colourRaw: $result->getIntegerOrNull(8),
-            title: $result->getString(9),
-            displayRoleId: $result->getStringOrNull(10),
-            createdTime: $result->getInteger(11),
-            lastActiveTime: $result->getIntegerOrNull(12),
-            deletedTime: $result->getIntegerOrNull(13),
+            emailAddress: $result->getString(2),
+            registerRemoteAddress: $result->getString(3),
+            lastRemoteAddress: $result->getStringOrNull(4),
+            super: $result->getBoolean(5),
+            countryCode: $result->getString(6),
+            colourRaw: $result->getIntegerOrNull(7),
+            title: $result->getString(8),
+            displayRoleId: $result->getStringOrNull(9),
+            createdTime: $result->getInteger(10),
+            lastActiveTime: $result->getIntegerOrNull(11),
+            deletedTime: $result->getIntegerOrNull(12),
         );
     }
 
-    public bool $hasPasswordHash {
-        get => $this->passwordHash !== null && $this->passwordHash !== '';
-    }
-
-    public bool $passwordNeedsRehash {
-        get => $this->hasPasswordHash && UsersData::passwordNeedsRehash($this->passwordHash);
-    }
-
-    public function verifyPassword(string $password): bool {
-        return $this->hasPasswordHash && password_verify($password, $this->passwordHash);
-    }
-
     public bool $hasColour {
         get => $this->colourRaw !== null && ($this->colourRaw & 0x40000000) === 0;
     }
diff --git a/src/Users/UserPasswordInfo.php b/src/Users/UserPasswordInfo.php
new file mode 100644
index 00000000..8af63326
--- /dev/null
+++ b/src/Users/UserPasswordInfo.php
@@ -0,0 +1,33 @@
+<?php
+namespace Misuzu\Users;
+
+use Carbon\CarbonImmutable;
+use Index\Db\DbResult;
+
+class UserPasswordInfo {
+    public function __construct(
+        public private(set) string $userId,
+        #[\SensitiveParameter] private string $hash,
+        public private(set) int $createdTime
+    ) {}
+
+    public static function fromResult(DbResult $result): UserPasswordInfo {
+        return new UserPasswordInfo(
+            userId: $result->getString(0),
+            hash: $result->getString(1),
+            createdTime: $result->getInteger(2),
+        );
+    }
+
+    public bool $needsRehash {
+        get => UserPasswordsData::passwordNeedsRehash($this->hash);
+    }
+
+    public CarbonImmutable $createdAt {
+        get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
+    }
+
+    public function verifyPassword(#[\SensitiveParameter] string $password): bool {
+        return password_verify($password, $this->hash);
+    }
+}
diff --git a/src/Users/UserPasswordsData.php b/src/Users/UserPasswordsData.php
new file mode 100644
index 00000000..2eda9e73
--- /dev/null
+++ b/src/Users/UserPasswordsData.php
@@ -0,0 +1,80 @@
+<?php
+namespace Misuzu\Users;
+
+use RuntimeException;
+use Index\XString;
+use Index\Db\{DbConnection,DbStatementCache};
+
+class UserPasswordsData {
+    public const string PASSWORD_ALGO = PASSWORD_ARGON2ID;
+    public const array PASSWORD_OPTS = [];
+    public const int PASSWORD_UNIQUE = 6;
+
+    private DbStatementCache $cache;
+
+    public function __construct(DbConnection $dbConn) {
+        $this->cache = new DbStatementCache($dbConn);
+    }
+
+    public static function passwordHash(#[\SensitiveParameter] string $password): string {
+        return password_hash($password, self::PASSWORD_ALGO, self::PASSWORD_OPTS);
+    }
+
+    public static function passwordNeedsRehash(#[\SensitiveParameter] string $passwordHash): bool {
+        return password_needs_rehash($passwordHash, self::PASSWORD_ALGO, self::PASSWORD_OPTS);
+    }
+
+    public function getUserPassword(UserInfo|string $userInfo): ?UserPasswordInfo {
+        $stmt = $this->cache->get(<<<SQL
+            SELECT user_id, password_hash, UNIX_TIMESTAMP(password_created)
+            FROM msz_users_passwords
+            WHERE user_id = ?
+        SQL);
+        $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
+        $stmt->execute();
+
+        $result = $stmt->getResult();
+        return $result->next() ? UserPasswordInfo::fromResult($result) : null;
+    }
+
+    public function deleteUserPassword(UserInfo|string $userInfo): void {
+        $stmt = $this->cache->get(<<<SQL
+            DELETE FROM msz_users_passwords
+            WHERE user_id = ?
+        SQL);
+        $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
+        $stmt->execute();
+    }
+
+    public function updateUserPassword(UserInfo|string $userInfo, #[\SensitiveParameter] string $password): UserPasswordInfo {
+        $stmt = $this->cache->get(<<<SQL
+            REPLACE INTO msz_users_passwords (
+                user_id, password_hash
+            ) VALUES (?, ?)
+        SQL);
+        $stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
+        $stmt->nextParameter(self::passwordHash($password));
+        $stmt->execute();
+
+        $pwInfo = $this->getUserPassword($userInfo);
+        if($pwInfo === null)
+            throw new RuntimeException('failed to create password');
+
+        return $pwInfo;
+    }
+
+    public static function validateUserPassword(string $password): string {
+        if(XString::countUnique($password) < self::PASSWORD_UNIQUE)
+            return 'weak';
+
+        return '';
+    }
+
+    public static function validateUserPasswordText(string $error): string {
+        return match($error) {
+            'weak' => sprintf("Your password is too weak, it must contain at least %d unique characters.", self::PASSWORD_UNIQUE),
+            '' => 'Your password is strong enough, why are you seeing this?',
+            default => 'Your password is not acceptable.',
+        };
+    }
+}
diff --git a/src/Users/UserTotpsData.php b/src/Users/UserTotpsData.php
index 9d232e95..5e42c2f1 100644
--- a/src/Users/UserTotpsData.php
+++ b/src/Users/UserTotpsData.php
@@ -46,7 +46,7 @@ class UserTotpsData {
         $stmt->execute();
     }
 
-    public function createUserTotp(UserInfo|string $userInfo): UserTotpInfo {
+    public function updateUserTotp(UserInfo|string $userInfo): UserTotpInfo {
         $stmt = $this->cache->get(<<<SQL
             REPLACE INTO msz_users_totp (
                 user_id, totp_secret
diff --git a/src/Users/UsersContext.php b/src/Users/UsersContext.php
index 69cbfc14..a0cb4071 100644
--- a/src/Users/UsersContext.php
+++ b/src/Users/UsersContext.php
@@ -10,6 +10,7 @@ class UsersContext {
     public private(set) BansData $bans;
     public private(set) WarningsData $warnings;
     public private(set) ModNotesData $modNotes;
+    public private(set) UserPasswordsData $passwords;
     public private(set) UserTotpsData $totps;
     public private(set) UserBirthdatesData $birthdates;
 
@@ -31,6 +32,7 @@ class UsersContext {
         $this->bans = new BansData($dbConn);
         $this->warnings = new WarningsData($dbConn);
         $this->modNotes = new ModNotesData($dbConn);
+        $this->passwords = new UserPasswordsData($dbConn);
         $this->totps = new UserTotpsData($dbConn);
         $this->birthdates = new UserBirthdatesData($dbConn);
     }
diff --git a/src/Users/UsersData.php b/src/Users/UsersData.php
index 25196139..fc436743 100644
--- a/src/Users/UsersData.php
+++ b/src/Users/UsersData.php
@@ -16,20 +16,8 @@ class UsersData {
         $this->cache = new DbStatementCache($dbConn);
     }
 
-    public const NAME_MIN_LENGTH = 3;
-    public const NAME_MAX_LENGTH = 16;
-
-    public const PASSWORD_ALGO = PASSWORD_ARGON2ID;
-    public const PASSWORD_OPTS = [];
-    public const PASSWORD_UNIQUE = 6;
-
-    public static function passwordHash(string $password): string {
-        return password_hash($password, self::PASSWORD_ALGO, self::PASSWORD_OPTS);
-    }
-
-    public static function passwordNeedsRehash(string $passwordHash): bool {
-        return password_needs_rehash($passwordHash, self::PASSWORD_ALGO, self::PASSWORD_OPTS);
-    }
+    public const int NAME_MIN_LENGTH = 3;
+    public const int NAME_MAX_LENGTH = 16;
 
     public function countUsers(
         RoleInfo|string|null $roleInfo = null,
@@ -165,7 +153,7 @@ class UsersData {
 
         $args = 0;
         $query = <<<SQL
-            SELECT u.user_id, u.user_name, u.user_password, u.user_email,
+            SELECT u.user_id, u.user_name, u.user_email,
                 INET6_NTOA(u.user_remote_addr_first), INET6_NTOA(u.user_remote_addr_last),
                 u.user_super, u.user_country, u.user_colour,
                  u.user_title, u.user_display_role_id,
@@ -271,7 +259,7 @@ class UsersData {
 
         $args = 0;
         $query = <<<SQL
-            SELECT user_id, user_name, user_password, user_email,
+            SELECT user_id, user_name, user_email,
                 INET6_NTOA(user_remote_addr_first), INET6_NTOA(user_remote_addr_last),
                 user_super, user_country, user_colour,
                 user_title, user_display_role_id,
@@ -307,7 +295,6 @@ class UsersData {
 
     public function createUser(
         string $name,
-        string $password,
         string $email,
         string $remoteAddr,
         string $countryCode,
@@ -318,16 +305,13 @@ class UsersData {
         elseif($displayRoleInfo === null)
             $displayRoleInfo = RolesData::DEFAULT_ROLE;
 
-        $password = self::passwordHash($password);
-
         if(self::validateName($name, true) !== '')
             throw new InvalidArgumentException('$name is not a valid user name.');
         if(self::validateEMailAddress($email, true) !== '')
             throw new InvalidArgumentException('$email is not a valid e-mail address.');
 
-        $stmt = $this->cache->get('INSERT INTO msz_users (user_name, user_password, user_email, user_remote_addr_first, user_remote_addr_last, user_country, user_display_role_id) VALUES (?, ?, ?, INET6_ATON(?), INET6_ATON(?), ?, ?)');
+        $stmt = $this->cache->get('INSERT INTO msz_users (user_name, user_email, user_remote_addr_first, user_remote_addr_last, user_country, user_display_role_id) VALUES (?, ?, INET6_ATON(?), INET6_ATON(?), ?, ?)');
         $stmt->nextParameter($name);
-        $stmt->nextParameter($password);
         $stmt->nextParameter($email);
         $stmt->nextParameter($remoteAddr);
         $stmt->nextParameter($remoteAddr);
@@ -342,7 +326,6 @@ class UsersData {
         UserInfo|string $userInfo,
         ?string $name = null,
         ?string $emailAddr = null,
-        ?string $password = null,
         ?string $countryCode = null,
         ?Colour $colour = null,
         RoleInfo|string|null $displayRoleInfo = null,
@@ -372,11 +355,6 @@ class UsersData {
             $values[] = $emailAddr;
         }
 
-        if($password !== null) {
-            $fields[] = 'password = ?';
-            $values[] = $password === '' ? null : self::passwordHash($password);
-        }
-
         if($countryCode !== null) {
             $fields[] = 'user_country = ?';
             $values[] = $countryCode;
@@ -643,19 +621,4 @@ class UsersData {
             default => 'Your e-mail address is not correctly formatted.',
         };
     }
-
-    public static function validatePassword(string $password): string {
-        if(XString::countUnique($password) < self::PASSWORD_UNIQUE)
-            return 'weak';
-
-        return '';
-    }
-
-    public static function validatePasswordText(string $error): string {
-        return match($error) {
-            'weak' => sprintf("Your password is too weak, it must contain at least %d unique characters.", self::PASSWORD_UNIQUE),
-            '' => 'Your password is strong enough, why are you seeing this?',
-            default => 'Your password is not acceptable.',
-        };
-    }
 }