Skip to main content

Three layers: a short note at the top, the key lines with our take in the middle, the full source at the bottom.

Test

c2-encrypt-encoding.test.ts

Encryption-at-rest encoding tests. Each ciphertext blob must round-trip exactly.

Repo path apps/api/tests/c2-encrypt-encoding.test.tsLanguage TypeScript

What this is

A test that pins the exact byte layout of the encrypted blob shared between your device and the server. If the server-side encoding ever drifts from the client-side encoding, the test fails. The server and the client must agree byte for byte.

What it proves

Backs the promise that a locked scan can be reopened on any of your devices. Both sides of the wire are pinned to one canonical layout. Read the promise →

What to look for in the source below

  • A fixture with a known input and a known output — both committed to the file.
  • A round-trip assertion that the encoded bytes match the expected output exactly.
  • A version byte at the start of every blob, so a future change can be detected without breaking old documents.
Show the full file (107 lines)

106 lines

// Wave C2.3 — the wire-encoding contract between the server
// encrypt seam (queue.ts) and the future client decrypt path
// (C2.4). A mismatch here = permanently undecryptable documents,
// so it is pinned end-to-end against the real crypto core.

import { describe, it, expect } from "vitest";
import { b64urlEncode, b64urlDecode } from "../src/lib/queue";
import {
  generateIdentityKeyPair,
  wrapToPublicKey,
  unwrapWithSecretKey,
  aesEncrypt,
  aesDecrypt,
} from "@muntin/recovery-crypto";

describe("b64url codec (queue.ts) round-trips", () => {
  it("encode→decode is identity for random byte lengths", () => {
    for (const n of [0, 1, 12, 16, 31, 32, 48, 80, 1000]) {
      const b = new Uint8Array(n);
      for (let i = 0; i < n; i++) b[i] = (i * 37 + 11) & 0xff;
      expect([...b64urlDecode(b64urlEncode(b))]).toEqual([...b]);
    }
  });

  it("emits url-safe, unpadded alphabet only", () => {
    const b = new Uint8Array([251, 252, 253, 254, 255, 0, 62, 63]);
    const s = b64urlEncode(b);
    expect(s).toMatch(/^[A-Za-z0-9_-]+$/);
    expect(s).not.toContain("=");
  });
});

describe("server wire format → client reversal (the catastrophic path)", () => {
  it("epk||ciphertext packing + b64url survives a full encrypt→decrypt", async () => {
    // Mirror EXACTLY what queue.ts writes to document_deks.
    const { publicKey, secretKey } = generateIdentityKeyPair();
    const dek = new Uint8Array(32);
    crypto.getRandomValues(dek);
    const docId = "doc_round_trip";
    const aad = new TextEncoder().encode(docId);
    const plaintext = new TextEncoder().encode(
      "the original invoice bytes — only the owner may read this",
    );

    // --- server side (queue.ts encrypt seam) ---
    const body = await aesEncrypt(dek, plaintext, aad);
    const sealed = await wrapToPublicKey(publicKey, dek, aad);
    const packed = new Uint8Array(sealed.epk.length + sealed.ciphertext.length);
    packed.set(sealed.epk, 0);
    packed.set(sealed.ciphertext, sealed.epk.length);
    const wire = {
      wrapped_dek: b64urlEncode(packed),
      wrap_nonce: b64urlEncode(sealed.nonce),
      document_nonce: b64urlEncode(body.nonce),
      ciphertext: b64urlEncode(body.ciphertext), // (R2 object body)
      wrap_aad: docId,
    };

    // --- client side (C2.4 will do exactly this) ---
    const packedBack = b64urlDecode(wire.wrapped_dek);
    const epk = packedBack.slice(0, 32); // fixed 32-byte X25519 prefix
    const ct = packedBack.slice(32);
    const dekBack = await unwrapWithSecretKey(
      secretKey,
      { epk, ciphertext: ct, nonce: b64urlDecode(wire.wrap_nonce) },
      new TextEncoder().encode(wire.wrap_aad),
    );
    expect([...dekBack]).toEqual([...dek]);

    const recovered = await aesDecrypt(
      dekBack,
      {
        ciphertext: b64urlDecode(wire.ciphertext),
        nonce: b64urlDecode(wire.document_nonce),
      },
      new TextEncoder().encode(wire.wrap_aad),
    );
    expect(new TextDecoder().decode(recovered)).toBe(
      "the original invoice bytes — only the owner may read this",
    );
  });

  it("a different recipient secret key cannot unwrap (confidentiality)", async () => {
    const alice = generateIdentityKeyPair();
    const mallory = generateIdentityKeyPair();
    const dek = new Uint8Array(32);
    crypto.getRandomValues(dek);
    const aad = new TextEncoder().encode("doc_x");
    const sealed = await wrapToPublicKey(alice.publicKey, dek, aad);
    const packed = new Uint8Array(32 + sealed.ciphertext.length);
    packed.set(sealed.epk, 0);
    packed.set(sealed.ciphertext, 32);
    const back = b64urlDecode(b64urlEncode(packed));
    await expect(
      unwrapWithSecretKey(
        mallory.secretKey,
        {
          epk: back.slice(0, 32),
          ciphertext: back.slice(32),
          nonce: sealed.nonce,
        },
        aad,
      ),
    ).rejects.toThrow();
  });
});

See also

This is the file as it lives at the moment of this build. The canonical history lives in git. If you want the full history or a specific commit, write to hello@muntin.digital.

c2-encrypt-encoding.test.ts · Verify · Muntin Ledger · Muntin