fix(auth): harden async login boundaries
This commit is contained in:
@@ -2,11 +2,11 @@ import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
const { argon2SyncSpy } = vi.hoisted(() => ({ argon2SyncSpy: vi.fn() }));
|
||||
const { argon2Spy } = vi.hoisted(() => ({ argon2Spy: vi.fn() }));
|
||||
vi.mock("node:crypto", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:crypto")>();
|
||||
argon2SyncSpy.mockImplementation(actual.argon2Sync);
|
||||
return { ...actual, argon2Sync: argon2SyncSpy };
|
||||
argon2Spy.mockImplementation(actual.argon2);
|
||||
return { ...actual, argon2: argon2Spy };
|
||||
});
|
||||
|
||||
import { verifyPassword } from "../src/auth/password.js";
|
||||
@@ -22,51 +22,51 @@ const vectors = JSON.parse(readFileSync(
|
||||
)) as Argon2Vector[];
|
||||
|
||||
describe("local Argon2id password verification", () => {
|
||||
test("accepts every committed Go-generated vector", () => {
|
||||
test("accepts every committed Go-generated vector", async () => {
|
||||
expect(vectors.length).toBeGreaterThan(0);
|
||||
for (const vector of vectors) {
|
||||
expect(verifyPassword(vector.password, vector.phc)).toBe(true);
|
||||
await expect(verifyPassword(vector.password, vector.phc)).resolves.toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects a one-byte password change", () => {
|
||||
test("rejects a one-byte password change", async () => {
|
||||
for (const vector of vectors) {
|
||||
expect(verifyPassword(`${vector.password}!`, vector.phc)).toBe(false);
|
||||
await expect(verifyPassword(`${vector.password}!`, vector.phc)).resolves.toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects ill-formed Unicode instead of authenticating as U+FFFD", () => {
|
||||
test("rejects ill-formed Unicode instead of authenticating as U+FFFD", async () => {
|
||||
const replacementPassword = "correct horse battery stap\uFFFD";
|
||||
const loneSurrogatePassword = "correct horse battery stap\uD800";
|
||||
const replacementPasswordHash = "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$+tAXzgaQVnNaonNvgevyG6UKlaKcwyRMi1mESNk0BvQ";
|
||||
|
||||
expect(verifyPassword(replacementPassword, replacementPasswordHash)).toBe(true);
|
||||
expect(verifyPassword(loneSurrogatePassword, replacementPasswordHash)).toBe(false);
|
||||
await expect(verifyPassword(replacementPassword, replacementPasswordHash)).resolves.toBe(true);
|
||||
await expect(verifyPassword(loneSurrogatePassword, replacementPasswordHash)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test("accepts a multibyte password at exactly the 1024-byte boundary", () => {
|
||||
test("accepts a multibyte password at exactly the 1024-byte boundary", async () => {
|
||||
const password = "é".repeat(512);
|
||||
const passwordHash = "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$LfO3M3JBKeXf1knenCjQ6m9z4B7nedb0As2uc522I1I";
|
||||
|
||||
expect(Buffer.byteLength(password, "utf8")).toBe(1024);
|
||||
expect(verifyPassword(password, passwordHash)).toBe(true);
|
||||
expect(verifyPassword(`${password}é`, passwordHash)).toBe(false);
|
||||
await expect(verifyPassword(password, passwordHash)).resolves.toBe(true);
|
||||
await expect(verifyPassword(`${password}é`, passwordHash)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test("rejects an over-limit password before converting it with Buffer.from", () => {
|
||||
test("rejects an over-limit password before converting it with Buffer.from", async () => {
|
||||
const password = "é".repeat(513);
|
||||
const fromSpy = vi.spyOn(Buffer, "from");
|
||||
|
||||
try {
|
||||
expect(Buffer.byteLength(password, "utf8")).toBe(1026);
|
||||
expect(verifyPassword(password, vectors[0].phc)).toBe(false);
|
||||
await expect(verifyPassword(password, vectors[0].phc)).resolves.toBe(false);
|
||||
expect(fromSpy.mock.calls.some(([value, encoding]) => value === password && encoding === "utf8")).toBe(false);
|
||||
} finally {
|
||||
fromSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects malformed and oversized PHC parameters before Argon2 allocation", () => {
|
||||
test("rejects malformed and oversized PHC parameters before Argon2 allocation", async () => {
|
||||
const password = vectors[0].password;
|
||||
const digest = vectors[0].phc.split("$")[5];
|
||||
const salt = vectors[0].phc.split("$")[4];
|
||||
@@ -79,9 +79,9 @@ describe("local Argon2id password verification", () => {
|
||||
];
|
||||
|
||||
for (const phc of cases) {
|
||||
argon2SyncSpy.mockClear();
|
||||
expect(verifyPassword(password, phc)).toBe(false);
|
||||
expect(argon2SyncSpy).not.toHaveBeenCalled();
|
||||
argon2Spy.mockClear();
|
||||
await expect(verifyPassword(password, phc)).resolves.toBe(false);
|
||||
expect(argon2Spy).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user