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
This commit is contained in:
1 parent
6fc14b9b84
commit
30ed2b8ce2
4 files changed
+42
-53
No files matched your search
@@ -1,9 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockEnv = vi.hoisted(() => ({
|
||||
ARGON2_MEMORY_KB: 65_536,
|
||||
ARGON2_ITERATIONS: 4,
|
||||
ARGON2_PARALLELISM: 1,
|
||||
BCRYPT_COST: 12,
|
||||
}));
|
||||
|
||||
vi.mock("@/env", () => ({
|
||||
@@ -28,16 +26,16 @@ describe("md5Hex", () => {
|
||||
});
|
||||
|
||||
describe("hashPassword", () => {
|
||||
it("emits an argon2id hash and round-trips", async () => {
|
||||
it("emits a bcrypt hash and round-trips", async () => {
|
||||
const h = await hashPassword("s3cret!");
|
||||
expect(h).toMatch(/^\$argon2id\$/);
|
||||
expect(h).toMatch(/^\$2[aby]\$/);
|
||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
||||
expect(await verifyPassword("wrong", h)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isArgon2idOf", () => {
|
||||
it("verifies an argon2id hash (AtomCMS/Laravel)", async () => {
|
||||
it("verifies a legacy argon2id hash (pre-migration accounts)", async () => {
|
||||
const stored =
|
||||
"$argon2id$v=19$m=1024,t=1,p=1$pTCeoGfX788sH7Z3ju9rJw$4awmR4yciu2L+xDNQJ/NesWX3Kio+fwN8wSCtp4XUp0";
|
||||
expect(await isArgon2idOf("test-password-123", stored)).toBe(true);
|
||||
@@ -47,7 +45,7 @@ describe("isArgon2idOf", () => {
|
||||
});
|
||||
|
||||
describe("isBcryptOf", () => {
|
||||
it("verifies a legacy bcrypt hash", async () => {
|
||||
it("verifies a bcrypt hash", async () => {
|
||||
const { bcrypt } = await import("hash-wasm");
|
||||
const { randomBytes } = await import("node:crypto");
|
||||
const stored = await bcrypt({
|
||||
@@ -71,20 +69,20 @@ describe("isMd5Of", () => {
|
||||
});
|
||||
|
||||
describe("verifyPassword", () => {
|
||||
it("verifies argon2id hashes", async () => {
|
||||
it("verifies bcrypt hashes", async () => {
|
||||
const h = await hashPassword("hunter2");
|
||||
expect(h).toMatch(/^\$argon2id\$/);
|
||||
expect(h).toMatch(/^\$2[aby]\$/);
|
||||
expect(await verifyPassword("hunter2", h)).toBe(true);
|
||||
expect(await verifyPassword("nope", h)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkLogin", () => {
|
||||
it("upgrades a legacy md5 hash to argon2id when conversion is enabled", async () => {
|
||||
it("upgrades a legacy md5 hash to bcrypt when conversion is enabled", async () => {
|
||||
const stored = await md5Hex("oldpass");
|
||||
const res = await checkLogin("oldpass", stored, { convertPasswords: true });
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
|
||||
expect(res.upgradedHash).toMatch(/^\$2[aby]\$/);
|
||||
expect(await verifyPassword("oldpass", res.upgradedHash as string)).toBe(
|
||||
true,
|
||||
);
|
||||
@@ -99,37 +97,25 @@ describe("checkLogin", () => {
|
||||
expect(res.upgradedHash).toBeUndefined();
|
||||
});
|
||||
|
||||
it("accepts an argon2id hash with no rehash", async () => {
|
||||
it("migrates a legacy argon2id hash to bcrypt", 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, {
|
||||
convertPasswords: true,
|
||||
});
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toBeUndefined();
|
||||
expect(res.upgradedHash).toMatch(/^\$2[aby]\$/);
|
||||
});
|
||||
|
||||
it("upgrades a legacy bcrypt hash to argon2id when conversion is enabled", async () => {
|
||||
// Generate a real bcrypt hash via hash-wasm and verify the upgrade path.
|
||||
it("accepts an existing bcrypt hash with no rehash", async () => {
|
||||
const { bcrypt } = await import("hash-wasm");
|
||||
const { randomBytes } = await import("node:crypto");
|
||||
const stored = await bcrypt({
|
||||
password: "oldbcrypt",
|
||||
salt: await import("node:crypto").then((c) => c.randomBytes(16)),
|
||||
password: "modern",
|
||||
salt: randomBytes(16),
|
||||
costFactor: 10,
|
||||
outputType: "encoded",
|
||||
});
|
||||
const res = await checkLogin("oldbcrypt", stored, {
|
||||
convertPasswords: true,
|
||||
});
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toMatch(/^\$argon2id\$/);
|
||||
expect(await verifyPassword("oldbcrypt", res.upgradedHash as string)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("validates an existing modern hash with no upgrade", async () => {
|
||||
const stored = await hashPassword("modern");
|
||||
const res = await checkLogin("modern", stored, { convertPasswords: true });
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toBeUndefined();
|
||||
|
||||
Reference in new issue
Block a user