Files
EpicNext-Cms/src/lib/auth/password.ts
T
openhands c601ffbb76
CI / check (push) Failing after 10s
CI / release (push) Skipped
CI / deploy (push) Skipped
feat(auth): switch password hashing to argon2id with legacy auto-upgrade
- 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
2026-08-01 17:09:29 +02:00

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