security: switch default hashing to Argon2id, fix tests
- hashPassword now uses Argon2id (memory-hard, GPU-resistant) via hash-wasm - verifyPassword checks both Argon2id and bcrypt - Legacy hashes (bcrypt, argon2, md5, sha1, sha256, sha512, combined, salted) auto-migrate to Argon2id on successful login - Updated all password tests to expect Argon2id format - Register validation: min 12 chars, max 128, upper+lower+digit+special required - Username restricted to [A-Za-z0-9_-], reserved names blocked - Disposable email domains blocked - Fixed parameter names for hash-wasm argon2id API (memorySize, iterations, parallelism, hashLength)
This commit is contained in:
1 parent
ac60a867d9
commit
b13b3a50ff
4 files changed
+117
-63
No files matched your search
@@ -40,9 +40,9 @@ describe("sha512Hex", () => {
|
||||
});
|
||||
|
||||
describe("hashPassword", () => {
|
||||
it("emits a bcrypt hash and round-trips", async () => {
|
||||
it("emits an Argon2id hash and round-trips", async () => {
|
||||
const h = await hashPassword("s3cret!");
|
||||
expect(h).toMatch(/^\$2[aby]\$/);
|
||||
expect(h).toMatch(/^\$argon2id\$/);
|
||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
||||
expect(await verifyPassword("wrong", h)).toBe(false);
|
||||
});
|
||||
@@ -209,31 +209,31 @@ describe("isSaltedDigestOf", () => {
|
||||
});
|
||||
|
||||
describe("verifyPassword", () => {
|
||||
it("verifies bcrypt hashes", async () => {
|
||||
it("verifies Argon2id hashes", async () => {
|
||||
const h = await hashPassword("hunter2");
|
||||
expect(h).toMatch(/^\$2[aby]\$/);
|
||||
expect(h).toMatch(/^\$argon2id\$/);
|
||||
expect(await verifyPassword("hunter2", h)).toBe(true);
|
||||
expect(await verifyPassword("nope", h)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkLogin", () => {
|
||||
it("upgrades a legacy md5 hash to bcrypt", async () => {
|
||||
it("upgrades a legacy md5 hash to Argon2id", async () => {
|
||||
const stored = await md5Hex("oldpass");
|
||||
const res = await checkLogin("oldpass", stored);
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$2[aby]\$/);
|
||||
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
|
||||
expect(await verifyPassword("oldpass", res.upgradedHash as string)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("migrates a legacy argon2id hash to bcrypt", async () => {
|
||||
it("migrates a legacy argon2id hash to Argon2id", async () => {
|
||||
const stored =
|
||||
"$argon2id$v=19$m=1024,t=1,p=1$pTCeoGfX788sH7Z3ju9rJw$4awmR4yciu2L+xDNQJ/NesWX3Kio+fwN8wSCtp4XUp0";
|
||||
const res = await checkLogin("test-password-123", stored);
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$2[aby]\$/);
|
||||
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
|
||||
});
|
||||
|
||||
for (const [name, hashThePassword] of [
|
||||
@@ -241,48 +241,48 @@ describe("checkLogin", () => {
|
||||
["sha256", sha256Hex],
|
||||
["sha512", sha512Hex],
|
||||
] as const) {
|
||||
it(`migrates a legacy ${name} hash to bcrypt`, async () => {
|
||||
it(`migrates a legacy ${name} hash to Argon2id`, async () => {
|
||||
const stored = await hashThePassword("oldpass");
|
||||
const res = await checkLogin("oldpass", stored);
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$2[aby]\$/);
|
||||
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
|
||||
expect(await verifyPassword("oldpass", res.upgradedHash as string)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it("migrates a combined digest hash to bcrypt (md5(md5(pass)))", async () => {
|
||||
it("migrates a combined digest hash to Argon2id (md5(md5(pass)))", async () => {
|
||||
const stored = await md5Hex(await md5Hex("oldpass"));
|
||||
const res = await checkLogin("oldpass", stored);
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$2[aby]\$/);
|
||||
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
|
||||
});
|
||||
|
||||
it("migrates a combined digest hash to bcrypt (sha1(md5(pass)))", async () => {
|
||||
it("migrates a combined digest hash to Argon2id (sha1(md5(pass)))", async () => {
|
||||
const stored = await sha1Hex(await md5Hex("oldpass"));
|
||||
const res = await checkLogin("oldpass", stored);
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$2[aby]\$/);
|
||||
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
|
||||
});
|
||||
|
||||
it("migrates a salted md5 hash to bcrypt (md5(salt+password))", async () => {
|
||||
it("migrates a salted md5 hash to Argon2id (md5(salt+password))", async () => {
|
||||
const salt = "pepper123";
|
||||
const stored = `${await md5Hex(`${salt}oldpass`)}:${salt}`;
|
||||
const res = await checkLogin("oldpass", stored);
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$2[aby]\$/);
|
||||
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
|
||||
expect(await verifyPassword("oldpass", res.upgradedHash as string)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("migrates a salted sha256 hash to bcrypt (sha256(salt+password))", async () => {
|
||||
it("migrates a salted sha256 hash to Argon2id (sha256(salt+password))", async () => {
|
||||
const salt = "abc123";
|
||||
const stored = `${salt}:${await sha256Hex(`${salt}oldpass`)}`;
|
||||
const res = await checkLogin("oldpass", stored);
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$2[aby]\$/);
|
||||
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
|
||||
});
|
||||
|
||||
it("rejects a wrong password for salted/combined hashes", async () => {
|
||||
@@ -292,10 +292,10 @@ describe("checkLogin", () => {
|
||||
expect((await checkLogin("wrongpass", combined)).valid).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts a raw plaintext password and upgrades it to bcrypt", async () => {
|
||||
it("accepts a raw plaintext password and upgrades it to Argon2id", async () => {
|
||||
const res = await checkLogin("hunter44", "hunter44");
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$2[aby]\$/);
|
||||
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
|
||||
expect(await verifyPassword("hunter44", res.upgradedHash as string)).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -14,10 +14,10 @@ import { env } from "@/env";
|
||||
|
||||
/** Argon2id parameters — memory-hard, GPU-resistant. */
|
||||
const ARGON2_CONFIG = {
|
||||
memoryCost: 19456, // ~19 MiB
|
||||
timeCost: 2,
|
||||
memorySize: 19456, // ~19 MiB
|
||||
iterations: 2,
|
||||
parallelism: 1,
|
||||
outputLen: 32,
|
||||
hashLength: 32,
|
||||
};
|
||||
|
||||
/** Hash new passwords with Argon2id (best practice 2024+). */
|
||||
@@ -210,6 +210,7 @@ export async function verifyPassword(
|
||||
password: string,
|
||||
stored: string,
|
||||
): Promise<boolean> {
|
||||
if (/^\$argon2/.test(stored)) return isArgon2Of(password, stored);
|
||||
return isBcryptOf(password, stored);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user