import { describe, it, expect } from "vitest"; import { generateKeyPair, deviceIdFromKeys, serializeKeyPair, deserializeKeyPair, deriveSharedKey, encrypt, decrypt, sign, verify, buildSignablePayload, } from "../src/crypto/index.js"; describe("Key generation", () => { it("generates unique keypairs", () => { const kp1 = generateKeyPair(); const kp2 = generateKeyPair(); expect(kp1.signPub).not.toEqual(kp2.signPub); expect(kp1.encPub).not.toEqual(kp2.encPub); }); it("derives deviceId from signing public key", () => { const kp = generateKeyPair(); const id = deviceIdFromKeys(kp); expect(id).toBe(kp.signPub.toString("hex")); expect(id).toHaveLength(64); // 32 bytes hex }); it("serializes and deserializes keypair", () => { const kp = generateKeyPair(); const serialized = serializeKeyPair(kp); const restored = deserializeKeyPair(serialized); expect(restored.signPub).toEqual(kp.signPub); expect(restored.signSec).toEqual(kp.signSec); expect(restored.encPub).toEqual(kp.encPub); expect(restored.encSec).toEqual(kp.encSec); }); }); describe("Encryption", () => { it("encrypts and decrypts with shared key", () => { const alice = generateKeyPair(); const bob = generateKeyPair(); const aliceShared = deriveSharedKey(alice.encSec, bob.encPub); const bobShared = deriveSharedKey(bob.encSec, alice.encPub); // Both sides derive the same shared key expect(aliceShared).toEqual(bobShared); const plaintext = Buffer.from("hello cookies"); const { nonce, ciphertext } = encrypt(plaintext, aliceShared); const decrypted = decrypt(ciphertext, nonce, bobShared); expect(decrypted.toString()).toBe("hello cookies"); }); it("fails to decrypt with wrong key", () => { const alice = generateKeyPair(); const bob = generateKeyPair(); const eve = generateKeyPair(); const sharedKey = deriveSharedKey(alice.encSec, bob.encPub); const wrongKey = deriveSharedKey(eve.encSec, bob.encPub); const plaintext = Buffer.from("secret"); const { nonce, ciphertext } = encrypt(plaintext, sharedKey); expect(() => decrypt(ciphertext, nonce, wrongKey)).toThrow(); }); it("produces different ciphertexts for same plaintext (random nonce)", () => { const alice = generateKeyPair(); const bob = generateKeyPair(); const shared = deriveSharedKey(alice.encSec, bob.encPub); const plaintext = Buffer.from("same message"); const r1 = encrypt(plaintext, shared); const r2 = encrypt(plaintext, shared); expect(r1.nonce).not.toEqual(r2.nonce); expect(r1.ciphertext).not.toEqual(r2.ciphertext); }); }); describe("Signing", () => { it("signs and verifies", () => { const kp = generateKeyPair(); const msg = Buffer.from("test message"); const sig = sign(msg, kp.signSec); expect(verify(msg, sig, kp.signPub)).toBe(true); }); it("rejects tampered message", () => { const kp = generateKeyPair(); const msg = Buffer.from("original"); const sig = sign(msg, kp.signSec); const tampered = Buffer.from("tampered"); expect(verify(tampered, sig, kp.signPub)).toBe(false); }); it("rejects wrong signer", () => { const alice = generateKeyPair(); const bob = generateKeyPair(); const msg = Buffer.from("from alice"); const sig = sign(msg, alice.signSec); expect(verify(msg, sig, bob.signPub)).toBe(false); }); it("builds deterministic signable payload", () => { const fields = { type: "cookie_sync", from: "aaa", to: "bbb", nonce: "ccc", payload: "ddd", timestamp: "2024-01-01T00:00:00.000Z", }; const p1 = buildSignablePayload(fields); const p2 = buildSignablePayload(fields); expect(p1).toEqual(p2); }); });