sync-entry-payload.test.ts

3.0 KB · raw

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});