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
This commit is contained in:
1 parent
d39738eb0d
commit
c601ffbb76
6 files changed
+121
-28
No files matched your search
+40
-11
@@ -1,16 +1,18 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { bcrypt, bcryptVerify, md5 } from "hash-wasm";
|
||||
import { argon2id, argon2Verify, bcryptVerify, md5 } from "hash-wasm";
|
||||
|
||||
import { env } from "@/env";
|
||||
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const h = await bcrypt({
|
||||
return await argon2id({
|
||||
password,
|
||||
salt: randomBytes(16),
|
||||
costFactor: env.BCRYPT_ROUNDS,
|
||||
parallelism: env.ARGON2_PARALLELISM,
|
||||
iterations: env.ARGON2_ITERATIONS,
|
||||
memorySize: env.ARGON2_MEMORY_KB,
|
||||
hashLength: 32,
|
||||
outputType: "encoded",
|
||||
});
|
||||
return h.replace(/^\$2[ab]\$/, "$2y$");
|
||||
}
|
||||
|
||||
export async function md5Hex(input: string): Promise<string> {
|
||||
@@ -27,18 +29,39 @@ export async function isMd5Of(
|
||||
);
|
||||
}
|
||||
|
||||
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 (/^\$2[aby]\$/.test(stored)) {
|
||||
try {
|
||||
return await bcryptVerify({ password, hash: stored });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (/^\$argon2id\$/.test(stored)) {
|
||||
return isArgon2idOf(password, stored);
|
||||
}
|
||||
return false;
|
||||
return isBcryptOf(password, stored);
|
||||
}
|
||||
|
||||
export interface LoginCheck {
|
||||
@@ -54,5 +77,11 @@ export async function checkLogin(
|
||||
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) };
|
||||
}
|
||||
Reference in new issue
Block a user