import { describe, expect, it } from "vitest"; import { sharedKey } from "@stablelib/x25519"; import { computeVaultId, hexToPubkey, pubkeyToHex } from "./sync-identity"; import { deriveSyncIdentityKeypair } from "./legacy-identity-derivation.test-support"; const MASTER = Uint8Array.from(Array.from({ length: 32 }, (_, i) => (i + 1) & 0xff)); const OTHER_MASTER = Uint8Array.from(Array.from({ length: 32 }, (_, i) => (i * 3 + 5) & 0xff)); describe("deriveSyncIdentityKeypair", () => { it("is deterministic across calls", () => { const a = deriveSyncIdentityKeypair(MASTER, "abc-123"); const b = deriveSyncIdentityKeypair(MASTER, "abc-123"); expect(a.publicKey).toEqual(b.publicKey); expect(a.secretKey).toEqual(b.secretKey); }); it("differs across device_id (same master_key)", () => { const a = deriveSyncIdentityKeypair(MASTER, "device-A"); const b = deriveSyncIdentityKeypair(MASTER, "device-B"); expect(a.publicKey).not.toEqual(b.publicKey); }); it("differs across master_key (same device_id)", () => { const a = deriveSyncIdentityKeypair(MASTER, "device-A"); const b = deriveSyncIdentityKeypair(OTHER_MASTER, "device-A"); expect(a.publicKey).not.toEqual(b.publicKey); }); it("produces 32-byte keys", () => { const kp = deriveSyncIdentityKeypair(MASTER, "x"); expect(kp.publicKey.length).toBe(32); expect(kp.secretKey.length).toBe(32); }); it("yields a working X25519 keypair (DH commutes)", () => { const alice = deriveSyncIdentityKeypair(MASTER, "alice"); const bob = deriveSyncIdentityKeypair(MASTER, "bob"); const ab = sharedKey(alice.secretKey, bob.publicKey); const ba = sharedKey(bob.secretKey, alice.publicKey); expect(ab).toEqual(ba); }); it("rejects empty master key", () => { expect(() => deriveSyncIdentityKeypair(new Uint8Array(), "x")).toThrow(); }); it("rejects empty device id", () => { expect(() => deriveSyncIdentityKeypair(MASTER, "")).toThrow(); }); }); describe("computeVaultId", () => { it("is deterministic for the same master key", async () => { const a = await computeVaultId(MASTER); const b = await computeVaultId(MASTER); expect(a).toBe(b); }); it("differs across master keys", async () => { const a = await computeVaultId(MASTER); const b = await computeVaultId(OTHER_MASTER); expect(a).not.toBe(b); }); it("returns 64 hex chars (full 32-byte BLAKE3 digest)", async () => { const id = await computeVaultId(MASTER); expect(id).toMatch(/^[0-9a-f]{64}$/); }); }); describe("pubkeyToHex / hexToPubkey", () => { it("round-trips", () => { const pk = Uint8Array.from(Array.from({ length: 32 }, (_, i) => i)); const hex = pubkeyToHex(pk); expect(hexToPubkey(hex)).toEqual(pk); }); it("rejects keys that are not exactly 32 bytes", () => { expect(() => hexToPubkey("abc")).toThrow(); expect(() => hexToPubkey("00".repeat(31))).toThrow(); expect(() => hexToPubkey("00".repeat(33))).toThrow(); }); it("rejects invalid hex chars", () => { expect(() => hexToPubkey("zz".repeat(32))).toThrow(); }); });