Files
LibreMail-Bug-Report-Ingest/wrangler.jsonc
T
JMR-devandClaude Opus 4.8 bd7fe21d97 #9 Encrypted-at-rest R2 storage for scrubbed reports
Implement the storage path: for each accepted report, scrub PII (#8),
encrypt with AES-256-GCM (ADR #5), and write only ciphertext to R2, wired
in as the real ingest Sink replacing NopSink.

- internal/crypto: AES-256-GCM in the exact ADR #5 wire format
  (magic "LMB1" || version || key_id BE16 || nonce(12) || ct || tag(16);
  the 7-byte header is the GCM AAD). Provider-independent framing shared by
  a host crypto/aes+crypto/cipher impl (tests, devserver) and a Wasm
  SubtleCrypto impl (syscall/js, //go:build js && wasm) per the TinyGo
  constraint; both produce byte-identical frames. Versioned keyring with
  key_id rotation; ParseKeyring reads the Secrets Store JSON secret.
- internal/storage: ObjectStore interface with an in-memory fake (tests,
  devserver) and a Wasm R2Store (syumai/workers R2 binding). Sink ties
  scrub -> Seal -> Put under a unique reports/<ts>-<rand> key. WorkerSink
  loads the keyring from Secrets Store (BUGREPORT_ENC_KEYRING), cached for
  the isolate lifetime.
- handler.New now takes an injectable ingest.Sink; the Worker uses the real
  R2/Secrets-Store sink, the devserver a memory + throwaway-key sink.
- wrangler.jsonc: add REPORTS_BUCKET (R2) and BUGREPORT_ENC_KEYRING
  (Secrets Store) bindings.

Tests (host, no TinyGo): encrypt/decrypt roundtrip; ciphertext != plaintext;
wrong key + tamper (ct/tag/nonce/header-AAD) fail; exact wire layout plus a
known-answer vector; key_id rotation with retained keys; full sink path (PII
scrubbed then encrypted, readback requires the key and yields the scrubbed
content). Existing ingest/handler behavior preserved (202 on valid POST).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:08:21 -05:00

46 lines
1.8 KiB
JSON

{
// Cloudflare Worker config for local dev and deploy.
// Docs: https://developers.cloudflare.com/workers/wrangler/configuration/
"name": "libremail-bug-report-ingest",
// Entry module. The TinyGo build (see package.json "build") produces the Wasm
// binary at ./build/app.wasm and workers-assets-gen produces this shim, which
// instantiates the Wasm module. The ./build/ artifacts are git-ignored and are
// created by `pnpm run build` (which requires TinyGo).
"main": "./build/worker.mjs",
"compatibility_date": "2025-06-01",
// Wrangler runs this before dev/deploy to (re)build the Wasm + shim.
// Requires TinyGo locally; TinyGo is installed in CI.
"build": {
"command": "pnpm run build"
},
// R2 bucket for the encrypted-at-rest bug-report objects (ADR #5 / issue #9).
// The Worker encrypts each scrubbed report with AES-256-GCM before writing, so
// only ciphertext is ever stored here. "binding" is the JS var the Worker code
// reads (internal/storage.BucketBinding); "bucket_name" matches the bucket
// provisioned by infra/ (defaultR2BucketName = "libremail-bug-reports").
"r2_buckets": [
{
"binding": "REPORTS_BUCKET",
"bucket_name": "libremail-bug-reports"
}
],
// Cloudflare Secrets Store secret holding the versioned encryption keyring
// (ADR #5, Key custody): a single JSON secret {active, keys{ver: base64-32B}}.
// "binding" is read at runtime via env.BUGREPORT_ENC_KEYRING.get()
// (internal/storage.KeyringBinding). Replace "<store-id>" with the account's
// Secrets Store id at deploy time; it is not needed for `pnpm run build`
// (Wasm compile) or the devserver, so CI does not require it.
"secrets_store_secrets": [
{
"binding": "BUGREPORT_ENC_KEYRING",
"store_id": "<store-id>",
"secret_name": "bugreport-enc-keyring"
}
]
}