- hashPassword now emits bcrypt (cost 12) instead of argon2id - checkLogin migrates legacy md5/argon2id hashes to bcrypt on sign-in - keep argon2id verification only as a one-time migration path - replace ARGON2_* env vars with BCRYPT_COST
94 lines
2.2 KiB
TypeScript
94 lines
2.2 KiB
TypeScript
import { randomBytes } from "node:crypto";
|
|
import { argon2Verify, bcrypt, bcryptVerify, md5 } from "hash-wasm";
|
|
|
|
import { env } from "@/env";
|
|
|
|
export async function hashPassword(password: string): Promise<string> {
|
|
return await bcrypt({
|
|
password,
|
|
salt: randomBytes(16),
|
|
costFactor: env.BCRYPT_COST,
|
|
outputType: "encoded",
|
|
});
|
|
}
|
|
|
|
export async function md5Hex(input: string): Promise<string> {
|
|
return await md5(input);
|
|
}
|
|
|
|
export async function isMd5Of(
|
|
password: string,
|
|
stored: string,
|
|
): Promise<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
return isBcryptOf(password, stored);
|
|
}
|
|
|
|
export interface LoginCheck {
|
|
valid: boolean;
|
|
upgradedHash?: string;
|
|
}
|
|
|
|
export async function checkLogin(
|
|
password: string,
|
|
stored: string,
|
|
opts: { convertPasswords: boolean },
|
|
): Promise<LoginCheck> {
|
|
// 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) };
|
|
}
|