- hashPassword now emits argon2id (same params as the legacy AtomCMS Laravel setup: memory 64MB, iterations 4, parallelism 1) - legacy md5 and bcrypt hashes are verified and auto-upgraded to argon2id on successful login (CONVERT_PASSWORDS=true) - replace BCRYPT_ROUNDS env with ARGON2_MEMORY_KB / ARGON2_ITERATIONS / ARGON2_PARALLELISM - update README and add tests for argon2id and bcrypt upgrade paths
88 lines
2.1 KiB
TypeScript
88 lines
2.1 KiB
TypeScript
import { randomBytes } from "node:crypto";
|
|
import { argon2id, argon2Verify, bcryptVerify, md5 } from "hash-wasm";
|
|
|
|
import { env } from "@/env";
|
|
|
|
export async function hashPassword(password: string): Promise<string> {
|
|
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<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()
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/** Legacy bcrypt support — only kept to verify & auto-upgrade old accounts. */
|
|
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> {
|
|
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<LoginCheck> {
|
|
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) };
|
|
}
|