import { sql } from "@umt/db"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { base32Decode, base32Encode, hashPassword, totpAt, verifyPassword, verifyTotp, } from "../../src/modules/auth-settings/crypto.ts"; import { createSecretStore, SecretError } from "../../src/modules/auth-settings/provided.ts"; import { createHarness, type Harness, MASTER_KEY, pgAvailable } from "./harness.ts"; describe("密码学原语", () => { it("argon2id(hash-wasm)输出标准 PHC 串,校验正反", async () => { const h = await hashPassword("pässwörd-123"); expect(h).toMatch(/^\$argon2id\$v=19\$m=19456,t=2,p=1\$/); expect(await verifyPassword("pässwörd-123", h)).toBe(true); expect(await verifyPassword("password-123", h)).toBe(false); expect(await verifyPassword("x", "not-a-hash")).toBe(false); }); it("TOTP:RFC 6238 测试向量(SHA1)+ 防重放", () => { const secret = base32Encode(Buffer.from("12345678901234567890")); expect(base32Decode(secret).toString()).toBe("12345678901234567890"); // RFC 6238 附录 B:T=59s → 94287082(8 位),6 位取后 6 位 expect(totpAt(secret, 1)).toBe("287082"); expect(totpAt(secret, Math.floor(1111111109 / 30))).toBe("081804"); const now = 1_700_000_000_000; const c = Math.floor(now / 30_000); expect(verifyTotp(secret, totpAt(secret, c), null, now)).toBe(c); expect(verifyTotp(secret, totpAt(secret, c - 1), null, now)).toBe(c - 1); expect(verifyTotp(secret, totpAt(secret, c), c, now)).toBeNull(); expect(verifyTotp(secret, totpAt(secret, c + 3), null, now)).toBeNull(); }); }); const hasPg = await pgAvailable(); describe.skipIf(!hasPg)("SecretStore(真实 PG)", () => { let h: Harness; beforeAll(async () => { h = await createHarness(); }, 60_000); afterAll(async () => { await h?.close(); }); it("create/resolve/replace(rotate)/remove 往返;库里无明文;每次加密盐与 nonce 不同", async () => { const s = h.ctx.secrets; const ref = await s.create({ purpose: "site_cookie", plaintext: "uid=1; pass=topsecret" }); expect(await s.resolve(ref)).toBe("uid=1; pass=topsecret"); const raw = await sql<{ ciphertext: Buffer; nonce: Buffer; kdf_params: { salt: string } }>` select ciphertext, nonce, kdf_params from secret where id = ${ref}`.execute(h.db); const before = raw.rows[0]!; expect(before.ciphertext.toString("utf8")).not.toContain("topsecret"); await s.replace(ref, "uid=1; pass=rotated"); expect(await s.resolve(ref)).toBe("uid=1; pass=rotated"); const after = ( await sql<{ nonce: Buffer; kdf_params: { salt: string }; rotated_at: Date | null }>` select nonce, kdf_params, rotated_at from secret where id = ${ref}`.execute(h.db) ).rows[0]!; expect(after.nonce.equals(before.nonce)).toBe(false); expect(after.kdf_params.salt).not.toBe(before.kdf_params.salt); expect(after.rotated_at).toBeTruthy(); await s.remove(ref); await expect(s.resolve(ref)).rejects.toBeInstanceOf(SecretError); }); it("主密钥不对 / 密文挪到别的行 / 改用途 → 解密失败,错误信息不含明文", async () => { const s = h.ctx.secrets; const a = await s.create({ purpose: "site_api_key", plaintext: "alpha-plain" }); const b = await s.create({ purpose: "site_api_key", plaintext: "bravo-plain" }); const other = createSecretStore({ db: h.db, masterKey: `${MASTER_KEY}-other` }); await expect(other.resolve(a)).rejects.toThrow(/解密失败/); await sql`update secret set ciphertext = (select ciphertext from secret where id = ${a}), nonce = (select nonce from secret where id = ${a}), auth_tag = (select auth_tag from secret where id = ${a}), kdf_params = (select kdf_params from secret where id = ${a}) where id = ${b}`.execute(h.db); const err = await s.resolve(b).catch((e: Error) => e); expect(err).toBeInstanceOf(SecretError); expect(String((err as Error).message)).not.toMatch(/alpha|bravo/); await sql`update secret set purpose = 'other' where id = ${a}`.execute(h.db); await expect(s.resolve(a)).rejects.toThrow(/解密失败/); await expect(s.resolve("not-a-uuid")).rejects.toThrow(/不存在/); }); it("resolve 出的明文经 logger 记录时也不会以敏感字段名泄漏;SecretError 进日志只有 ref", async () => { const ref = await h.ctx.secrets.create({ purpose: "downloader_password", plaintext: "qb-pass-777" }); const password = await h.ctx.secrets.resolve(ref); h.ctx.logger.info({ password, secretRef: ref }, "连接下载器"); const err = await h.ctx.secrets.resolve("00000000-0000-4000-8000-000000000000").catch((e: Error) => e); h.ctx.logger.error({ err }, "取凭证失败"); const out = h.lines.join("\n"); expect(out).not.toContain("qb-pass-777"); expect(out).toContain("[REDACTED]"); }); });