From f0398c1b8835022ce0489f0cfb5283d4178c48f6 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Tue, 2 Jun 2026 12:46:03 +0000 Subject: feat(security): add AES-GCM feed-token helper (M5) --- src/lib/Utility/feedToken.test.ts | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/lib/Utility/feedToken.test.ts (limited to 'src/lib/Utility/feedToken.test.ts') diff --git a/src/lib/Utility/feedToken.test.ts b/src/lib/Utility/feedToken.test.ts new file mode 100644 index 00000000..ffcb5fd7 --- /dev/null +++ b/src/lib/Utility/feedToken.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { decryptFeedToken, encryptFeedToken } from "./feedToken"; + +// Fixed 32-byte keys so the cipher is exercised without the FEED_TOKEN_KEY env +// var (production omits the key argument and reads it from env). +const key = new Uint8Array(32).fill(7); +const otherKey = new Uint8Array(32).fill(9); + +describe("feed token", () => { + // Behaviour gate: the feed must still resolve to the same refresh token it + // was minted from, and the cleartext must never appear in the URL blob. + it("round-trips the refresh token without leaking it", async () => { + const refreshToken = "anilist-refresh-token-abc123"; + const sealed = await encryptFeedToken(refreshToken, key); + + expect(sealed).not.toContain(refreshToken); + expect(await decryptFeedToken(sealed, key)).toBe(refreshToken); + }); + + it("produces a fresh ciphertext each time (random IV)", async () => { + expect(await encryptFeedToken("same-token", key)).not.toBe( + await encryptFeedToken("same-token", key), + ); + }); + + it("rejects a token sealed with a different key", async () => { + const sealed = await encryptFeedToken("secret", otherKey); + + expect(await decryptFeedToken(sealed, key)).toBeNull(); + }); + + it("rejects a tampered ciphertext", async () => { + const sealed = await encryptFeedToken("secret", key); + const tampered = `${sealed[0] === "A" ? "B" : "A"}${sealed.slice(1)}`; + + expect(await decryptFeedToken(tampered, key)).toBeNull(); + }); + + it("rejects malformed input", async () => { + expect(await decryptFeedToken("", key)).toBeNull(); + expect(await decryptFeedToken("short", key)).toBeNull(); + expect(await decryptFeedToken("not valid base64url!!", key)).toBeNull(); + }); +}); -- cgit v1.2.3