import { randomBytes } from "node:crypto"; import { describe, expect, it } from "vitest"; import { decryptRconPassword, encryptRconPassword, rconCommandDigest } from "./rcon-credentials"; const key = randomBytes(32).toString("base64"); const otherKey = randomBytes(32).toString("base64"); const connectionId = "11111111-1111-4111-8111-111111111111"; describe("RCON credential encryption", () => { it("round trips with randomized authenticated encryption", () => { const first = encryptRconPassword("super-secret", connectionId, key); const second = encryptRconPassword("super-secret", connectionId, key); expect(first).not.toBe(second); expect(first).not.toContain("super-secret"); expect(decryptRconPassword(first, connectionId, key)).toBe("super-secret"); expect(decryptRconPassword(second, connectionId, key)).toBe("super-secret"); }); it("fails closed for tampering, another connection, or another key", () => { const encrypted = encryptRconPassword("super-secret", connectionId, key); expect(() => decryptRconPassword(`${encrypted}x`, connectionId, key)).toThrow("RCON credential unavailable"); expect(() => decryptRconPassword(encrypted, "22222222-2222-4222-8222-222222222222", key)).toThrow("RCON credential unavailable"); expect(() => decryptRconPassword(encrypted, connectionId, otherKey)).toThrow("RCON credential unavailable"); }); it("requires an exact 32-byte deployment key", () => { expect(() => encryptRconPassword("secret", connectionId, "not-base64")).toThrow("RCON credential key is not configured"); }); it("creates a keyed, versioned command digest", () => { const digest = rconCommandDigest("say secret message", key); expect(digest).toMatch(/^hmac-sha256:v1:[a-f0-9]{64}$/u); expect(digest).not.toContain("secret message"); expect(rconCommandDigest("say secret message", key)).toBe(digest); expect(rconCommandDigest("say secret message", otherKey)).not.toBe(digest); }); });