Files
EpicNext-Cms/src/lib/auth/password.ts
T
openhands 30ed2b8ce2
CI / check (push) Successful in 23s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m0s
refactor: switch password hashing from argon2 to bcrypt
- 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
2026-08-03 18:08:57 +02:00

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) };
}