1import { describe, expect, it } from "vitest";23import {4 decryptSyncEntry,5 encryptSyncEntry,6 importSyncEntryKey,7 stripNonPortableSecrets,8} from "./sync-entry-payload";9import type { VaultEntry } from "./types";1011function entry(overrides: Partial<VaultEntry> = {}): VaultEntry {12 return {13 id: "entry-1",14 type: "login",15 title: "GitHub",16 username: "june",17 password: "secret",18 url: "https://github.com",19 createdAt: 1_000,20 modifiedAt: 1_000,21 ...overrides,22 };23}2425describe("sync entry payloads", () => {26 it("encrypts and decrypts vault entries through an AES-GCM envelope", async () => {27 const key = await importSyncEntryKey(new Uint8Array(32).fill(7));28 const source = entry({ deletedAt: 2_000, notes: "sync me" });2930 const encrypted = await encryptSyncEntry(source, key);3132 expect(encrypted).toMatchObject({33 id: "entry-1",34 modifiedAt: 1_000,35 deletedAt: 2_000,36 ciphertext: expect.stringMatching(/^[A-Za-z0-9_-]+$/),37 nonce: expect.stringMatching(/^[A-Za-z0-9_-]+$/),38 tag: expect.stringMatching(/^[A-Za-z0-9_-]+$/),39 });40 expect(await decryptSyncEntry(encrypted, key)).toEqual(source);41 });4243 it("strips passkey private keys before encryption", async () => {44 const key = await importSyncEntryKey(new Uint8Array(32).fill(9));45 const source = entry({46 passkey: {47 rpId: "github.com",48 rpName: "GitHub",49 credentialId: "credential",50 publicKeyCose: "public",51 privateKeyJwk: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "secret" },52 userHandle: "user",53 userName: "june",54 userDisplayName: "June",55 algorithm: -7,56 signCount: 3,57 aaguid: "aaguid",58 backupEligible: true,59 backupState: true,60 browser: "chrome",61 createdAt: 1_000,62 },63 privateKeyJwk: { kty: "EC", d: "legacy-secret" },64 });6566 const encrypted = await encryptSyncEntry(source, key);67 const decrypted = await decryptSyncEntry(encrypted, key);6869 expect(decrypted.passkey?.privateKeyJwk).toBeUndefined();70 expect(decrypted.privateKeyJwk).toBeUndefined();71 expect(stripNonPortableSecrets(source).passkey?.publicKeyCose).toBe("public");72 });7374 it("rejects invalid key sizes", async () => {75 await expect(importSyncEntryKey(new Uint8Array(31))).rejects.toThrow(76 "sync entry key must be 32 bytes"77 );78 });7980 it("rejects tampered ciphertext or mismatched envelope metadata", async () => {81 const key = await importSyncEntryKey(new Uint8Array(32).fill(11));82 const encrypted = await encryptSyncEntry(entry(), key);8384 const replacement = encrypted.ciphertext.endsWith("A") ? "B" : "A";85 await expect(86 decryptSyncEntry(87 { ...encrypted, ciphertext: `${encrypted.ciphertext.slice(0, -1)}${replacement}` },88 key89 )90 ).rejects.toThrow();91 await expect(decryptSyncEntry({ ...encrypted, id: "other" }, key)).rejects.toThrow(92 "invalid sync entry payload"93 );94 });95});