import { describe, expect, it } from "vitest"; import { decryptSyncEntry, encryptSyncEntry, importSyncEntryKey, stripNonPortableSecrets, } from "./sync-entry-payload"; import type { VaultEntry } from "./types"; function entry(overrides: Partial = {}): VaultEntry { return { id: "entry-1", type: "login", title: "GitHub", username: "june", password: "secret", url: "https://github.com", createdAt: 1_000, modifiedAt: 1_000, ...overrides, }; } describe("sync entry payloads", () => { it("encrypts and decrypts vault entries through an AES-GCM envelope", async () => { const key = await importSyncEntryKey(new Uint8Array(32).fill(7)); const source = entry({ deletedAt: 2_000, notes: "sync me" }); const encrypted = await encryptSyncEntry(source, key); expect(encrypted).toMatchObject({ id: "entry-1", modifiedAt: 1_000, deletedAt: 2_000, ciphertext: expect.stringMatching(/^[A-Za-z0-9_-]+$/), nonce: expect.stringMatching(/^[A-Za-z0-9_-]+$/), tag: expect.stringMatching(/^[A-Za-z0-9_-]+$/), }); expect(await decryptSyncEntry(encrypted, key)).toEqual(source); }); it("strips passkey private keys before encryption", async () => { const key = await importSyncEntryKey(new Uint8Array(32).fill(9)); const source = entry({ passkey: { rpId: "github.com", rpName: "GitHub", credentialId: "credential", publicKeyCose: "public", privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "secret" }, userHandle: "user", userName: "june", userDisplayName: "June", algorithm: -7, signCount: 3, aaguid: "aaguid", backupEligible: true, backupState: true, browser: "chrome", createdAt: 1_000, }, privateKeyJwk: { kty: "EC", d: "legacy-secret" }, }); const encrypted = await encryptSyncEntry(source, key); const decrypted = await decryptSyncEntry(encrypted, key); expect(decrypted.passkey?.privateKeyJwk).toBeUndefined(); expect(decrypted.privateKeyJwk).toBeUndefined(); expect(stripNonPortableSecrets(source).passkey?.publicKeyCose).toBe("public"); }); it("rejects invalid key sizes", async () => { await expect(importSyncEntryKey(new Uint8Array(31))).rejects.toThrow( "sync entry key must be 32 bytes" ); }); it("rejects tampered ciphertext or mismatched envelope metadata", async () => { const key = await importSyncEntryKey(new Uint8Array(32).fill(11)); const encrypted = await encryptSyncEntry(entry(), key); const replacement = encrypted.ciphertext.endsWith("A") ? "B" : "A"; await expect( decryptSyncEntry( { ...encrypted, ciphertext: `${encrypted.ciphertext.slice(0, -1)}${replacement}` }, key ) ).rejects.toThrow(); await expect(decryptSyncEntry({ ...encrypted, id: "other" }, key)).rejects.toThrow( "invalid sync entry payload" ); }); });