import { randomBytes } from "node:crypto"; import { argon2id, argon2Verify, bcryptVerify, md5 } from "hash-wasm"; import { env } from "@/env"; export async function hashPassword(password: string): Promise { return await argon2id({ password, salt: randomBytes(16), parallelism: env.ARGON2_PARALLELISM, iterations: env.ARGON2_ITERATIONS, memorySize: env.ARGON2_MEMORY_KB, hashLength: 32, 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() ); } 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; } } /** Legacy bcrypt support — only kept to verify & auto-upgrade old accounts. */ 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 { if (/^\$argon2id\$/.test(stored)) { return isArgon2idOf(password, stored); } return isBcryptOf(password, stored); } export interface LoginCheck { valid: boolean; upgradedHash?: string; } export async function checkLogin( password: string, stored: string, opts: { convertPasswords: boolean }, ): Promise { if (opts.convertPasswords && (await isMd5Of(password, stored))) { return { valid: true, upgradedHash: await hashPassword(password) }; } if (opts.convertPasswords && (await isArgon2idOf(password, stored))) { return { valid: true }; } if (opts.convertPasswords && (await isBcryptOf(password, stored))) { return { valid: true, upgradedHash: await hashPassword(password) }; } return { valid: await verifyPassword(password, stored) }; }