import { randomBytes } from "node:crypto"; import { argon2Verify, bcrypt, bcryptVerify, md5 } from "hash-wasm"; import { env } from "@/env"; export async function hashPassword(password: string): Promise { return await bcrypt({ password, salt: randomBytes(16), costFactor: env.BCRYPT_COST, outputType: "encoded", }); } export async function md5Hex(input: string): Promise { return await md5(input); } export async function isMd5Of( password: string, stored: string, ): Promise { return ( /^[a-f0-9]{32}$/i.test(stored) && (await md5Hex(password)) === stored.toLowerCase() ); } /** * Legacy argon2id verification — kept ONLY so accounts hashed before the * bcrypt switch can still sign in once and be migrated to bcrypt. No new * argon2 hashes are ever produced. */ export async function isArgon2idOf( password: string, stored: string, ): Promise { if (!/^\$argon2id\$/.test(stored)) return false; try { return await argon2Verify({ password, hash: stored }); } catch { return false; } } export async function isBcryptOf( password: string, stored: string, ): Promise { if (!/^\$2[aby]\$/.test(stored)) return false; try { return await bcryptVerify({ password, hash: stored }); } catch { return false; } } export async function verifyPassword( password: string, stored: string, ): Promise { return isBcryptOf(password, stored); } export interface LoginCheck { valid: boolean; upgradedHash?: string; } export async function checkLogin( password: string, stored: string, opts: { convertPasswords: boolean }, ): Promise { // Legacy argon2id — verify so existing users can sign in, then immediately // rehash to bcrypt so the hash format converges on bcrypt. if (/^\$argon2id\$/.test(stored)) { if (await isArgon2idOf(password, stored)) { return { valid: true, upgradedHash: await hashPassword(password) }; } return { valid: false }; } if (/^\$2[aby]\$/.test(stored)) { return { valid: await isBcryptOf(password, stored) }; } if (opts.convertPasswords && (await isMd5Of(password, stored))) { return { valid: true, upgradedHash: await hashPassword(password) }; } return { valid: await verifyPassword(password, stored) }; }