import { describe, expect, test } from "bun:test";
import {
decryptSecret,
encryptSecret,
hashPassword,
isLegacySecret,
passwordNeedsRehash,
verifyPassword,
verifyPasswordAgainstNobody,
} from "./crypto";
const MASTER = "correct horse battery staple";
/** Produces a ciphertext the way the pre-AUTH-A10 code did: AES-GCM under SHA-256(master), no AAD, bare base64. */
async function legacyEncrypt(masterKey: string, plaintext: string): Promise<{ ciphertext: string; iv: string }> {
const encoder = new TextEncoder();
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(masterKey));
const key = await crypto.subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt"]);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = new Uint8Array(
await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder.encode(plaintext)),
);
return { ciphertext: btoa(String.fromCharCode(...encrypted)), iv: btoa(String.fromCharCode(...iv)) };
}
describe("encryptSecret / decryptSecret (AUTH-A10)", () => {
test("round-trips under a per-purpose key with AAD", async () => {
const context = { purpose: "totp", aad: "user:u1" };
const sealed = await encryptSecret(MASTER, "JBSWY3DPEHPK3PXP", context);
expect(isLegacySecret(sealed.ciphertext)).toBe(false);
expect(await decryptSecret(MASTER, sealed.ciphertext, sealed.iv, context)).toBe("JBSWY3DPEHPK3PXP");
});
test("a ciphertext moved to another row (different AAD) does not decrypt", async () => {
const sealed = await encryptSecret(MASTER, "seed", { purpose: "totp", aad: "user:u1" });
await expect(
decryptSecret(MASTER, sealed.ciphertext, sealed.iv, { purpose: "totp", aad: "user:u2" }),
).rejects.toThrow();
});