feat(auth): auto-upgrade every legacy password format to bcrypt on login
CI / check (push) Failing after 25s
CI / preflight (push) Skipped
CI / deploy (push) Skipped

checkLogin now verifies and migrates all known password formats without
configuration: bcrypt, argon2id/argon2i/argon2d, unsalted md5/sha1/sha256/
sha512, double-md5 (UberCMS/Butterfly), salted md5 with embedded salt
(hash:salt, salt:hash, hash$salt), and a guarded plaintext fallback.

Every successful legacy login rewrites the stored hash to bcrypt, so the
CONVERT_PASSWORDS flag is no longer required (kept for deploy compatibility).
This commit is contained in:
openhands committed 2026-09-17 14:56:31 +02:00
1 parent 905573e627
commit e153300da0
5 files changed
+326 -47

No files matched your search

+3 -2
View File
@@ -36,8 +36,9 @@ BADGE_URL=/swf/c_images/album1584
# --- SECURITY & HASHING --- # --- SECURITY & HASHING ---
AUTH_SECRET=your-super-secret-auth-key-change-this-min-32-chars AUTH_SECRET=your-super-secret-auth-key-change-this-min-32-chars
APP_KEY=base64:your-app-key-here= APP_KEY=base64:your-app-key-here=
CONVERT_PASSWORDS=true # Deprecated: legacy md5/argon2id hashes are ALWAYS upgraded to bcrypt on
# CONVERT_PASSWORDS: enables legacy md5/argon2id -> bcrypt upgrade on login. # login now. Kept only for config compatibility with existing deploys.
# CONVERT_PASSWORDS=true
BCRYPT_COST=12 BCRYPT_COST=12
# --- PATHS --- # --- PATHS ---
+2 -2
View File
@@ -69,8 +69,8 @@ const schema = z
// Laravel APP_KEY (base64:...) — needed to read existing 2FA secrets. // Laravel APP_KEY (base64:...) — needed to read existing 2FA secrets.
APP_KEY: z.string().optional(), APP_KEY: z.string().optional(),
// Mirrors Laravel config('habbo.site.convert_passwords') — enables // Deprecated: legacy md5/argon2id hashes are ALWAYS upgraded to bcrypt
// legacy md5/argon2id hashes to be upgraded to bcrypt on login. // on login now (no flag required). Kept for config compatibility.
CONVERT_PASSWORDS: z CONVERT_PASSWORDS: z
.string() .string()
.optional() .optional()
+2 -5
View File
@@ -1,5 +1,4 @@
import { sql } from "drizzle-orm"; import { sql } from "drizzle-orm";
import { env } from "@/env";
import { checkLogin } from "@/lib/auth/password"; import { checkLogin } from "@/lib/auth/password";
import { cachedQuery, invalidateKey } from "@/lib/cached-db"; import { cachedQuery, invalidateKey } from "@/lib/cached-db";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
@@ -98,7 +97,7 @@ export async function invalidateLoginCache(username: string): Promise<void> {
/** Runs a dummy hash check so missing-user responses stay timing-constant. */ /** Runs a dummy hash check so missing-user responses stay timing-constant. */
export async function runDummyHashCheck(password: string): Promise<void> { export async function runDummyHashCheck(password: string): Promise<void> {
await checkLogin(password, DUMMY_BCRYPT_HASH, { convertPasswords: false }); await checkLogin(password, DUMMY_BCRYPT_HASH);
} }
/** Verifies the password against the stored hash and reports a possible upgrade. */ /** Verifies the password against the stored hash and reports a possible upgrade. */
@@ -107,9 +106,7 @@ export async function verifyLoginPassword(
password: string, password: string,
): Promise<{ valid: boolean; upgradedHash?: string }> { ): Promise<{ valid: boolean; upgradedHash?: string }> {
if (!user.password) return { valid: false }; if (!user.password) return { valid: false };
return checkLogin(password, user.password, { return checkLogin(password, user.password);
convertPasswords: env.CONVERT_PASSWORDS,
});
} }
/** True when email verification is required but this account hasn't verified yet. */ /** True when email verification is required but this account hasn't verified yet. */
File diff suppressed because it is too large. Load diff
+139 -18
View File
File diff suppressed because it is too large. Load diff