Skip to content
File

Blob: tests/worker/argon2.test.ts

typescript77 lines
1import { describe, expect, it } from "vitest";
2 
3import { hashArgon2id, verifyArgon2id } from "@/worker/auth/argon2";
4 
5const PHC_PATTERN = /^\$argon2id\$v=19\$m=19456,t=2,p=1\$[A-Za-z0-9+/]+\$[A-Za-z0-9+/]+$/;
6 
7// Argon2id at m=19456,t=2 takes ~1–3s per hash on dev hardware (pure-JS noble
8// path on workerd). Keep these tests well past Vitest's 5s default.
9const HASH_TIMEOUT = 30_000;
10 
11describe("Argon2id hash + verify", () => {
12 it(
13 "emits a PHC string with the expected shape",
14 async () => {
15 const hash = await hashArgon2id("hunter2");
16 expect(hash).toMatch(PHC_PATTERN);
17 },
18 HASH_TIMEOUT,
19 );
20 
21 it(
22 "verifies the correct password",
23 async () => {
24 const password = "correct-horse-battery-staple";
25 const hash = await hashArgon2id(password);
26 expect(await verifyArgon2id({ hash, password })).toBe(true);
27 },
28 HASH_TIMEOUT,
29 );
30 
31 it(
32 "rejects the wrong password",
33 async () => {
34 const hash = await hashArgon2id("right-password");
35 expect(await verifyArgon2id({ hash, password: "wrong-password" })).toBe(false);
36 },
37 HASH_TIMEOUT,
38 );
39 
40 it("rejects malformed PHC strings", async () => {
41 expect(await verifyArgon2id({ hash: "not-a-phc-string", password: "x" })).toBe(false);
42 expect(await verifyArgon2id({ hash: "$argon2id$v=19$m=19456,t=2,p=1$tooshort", password: "x" })).toBe(false);
43 });
44 
45 // tessera always emits dkLen=32 PHC strings. Anything else is by
46 // definition not produced by hashArgon2id; rejecting non-canonical
47 // lengths removes the trivial truncation collision surface
48 // (1/256 brute-force at dkLen=1) that an attacker with arbitrary D1
49 // write could otherwise exploit. Non-canonical lengths short-circuit
50 // before invoking argon2id, so this is a fast assertion.
51 it("rejects PHC strings with non-canonical stored-digest lengths", async () => {
52 const encodeBase64Phc = (bytes: Uint8Array): string => {
53 let binary = "";
54 for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
55 return btoa(binary).replace(/=+$/, "");
56 };
57 const salt = new Uint8Array(16);
58 const make = (storedLen: number): string => {
59 const stored = new Uint8Array(storedLen);
60 return `$argon2id$v=19$m=19456,t=2,p=1$${encodeBase64Phc(salt)}$${encodeBase64Phc(stored)}`;
61 };
62 expect(await verifyArgon2id({ hash: make(16), password: "x" })).toBe(false);
63 expect(await verifyArgon2id({ hash: make(40), password: "x" })).toBe(false);
64 expect(await verifyArgon2id({ hash: make(1), password: "x" })).toBe(false);
65 });
66 
67 it(
68 "produces unique salts so two hashes of the same password differ",
69 async () => {
70 const a = await hashArgon2id("same");
71 const b = await hashArgon2id("same");
72 expect(a).not.toBe(b);
73 },
74 HASH_TIMEOUT,
75 );
76});