Adds docs/decisions/encryption.md — the ADR for how scrubbed bug reports are encrypted at rest in R2 and how the key is held. Decision/ADR ticket only (no implementation code); it unblocks #9.
Decision
Worker-side AES-256-GCM authenticated encryption, applied in the Worker before the object is written to R2, with the key held as a versioned keyring in Cloudflare Secrets Store.
Why this over the alternatives: R2 default at-rest encryption (Cloudflare-managed keys) leaves plaintext readable by anyone with bucket read access — fails the requirement. R2 SSE-C encrypts at rest under a customer key but still sends plaintext + key to the R2 service on every request and gives us no integrity/rotation control. Worker-side AEAD means R2 only ever stores opaque ciphertext (never plaintext or the key), gives us GCM integrity/tamper-evidence, and makes rotation data-loss-free. Reports can carry residual PII after best-effort scrubbing, so confidentiality against the storage boundary matters.
Key custody: versioned JSON keyring ({active, keys{ver: base64-32B}}) in Cloudflare Secrets Store, bound to both Workers; read at runtime via await env.BUGREPORT_ENC_KEYRING.get(); never logged/returned.
Weekly publish: recommended as a Cron-Trigger Worker sharing the same binding so the key never leaves Cloudflare (GitHub Actions decryption explicitly discouraged — it would widen custody).
Rotation: add a new key version + bump active; the key_id in each object header selects the right version, so old objects keep decrypting; never drop a version still referenced.
TinyGo's crypto/aes and crypto/cipher currently fail their test suites, so the ADR recommends doing AES-GCM via the Workers host Web Crypto API (SubtleCrypto) rather than pure-Go crypto. The wire format is identical either way.
Open questions for the maintainer (see ADR "Open questions")
Weekly publisher = Cron Worker (assumed) or GitHub Action?
Crypto provider = host SubtleCrypto (recommended) or pure-Go once TinyGo is verified?
Keyring shape/binding name confirmation.
Are R2 objects deleted after publication (bounds rotation burden)?
Notes
Per ticket scope this PR does not edit the README (to avoid conflicting with the parallel bootstrap PR). The ticket's acceptance criteria mention linking the ADR from the README — suggest adding a one-line link under the README "Status" section once the bootstrap PR lands (e.g. Encryption/key-custody decision: docs/decisions/encryption.md).
## Summary
Adds `docs/decisions/encryption.md` — the ADR for how scrubbed bug reports are encrypted at rest in R2 and how the key is held. Decision/ADR ticket only (no implementation code); it unblocks #9.
## Decision
**Worker-side AES-256-GCM authenticated encryption, applied in the Worker before the object is written to R2**, with the key held as a versioned keyring in **Cloudflare Secrets Store**.
- **Why this over the alternatives:** R2 default at-rest encryption (Cloudflare-managed keys) leaves plaintext readable by anyone with bucket read access — fails the requirement. R2 SSE-C encrypts at rest under a customer key but still sends plaintext + key to the R2 service on every request and gives us no integrity/rotation control. Worker-side AEAD means **R2 only ever stores opaque ciphertext (never plaintext or the key)**, gives us GCM integrity/tamper-evidence, and makes rotation data-loss-free. Reports can carry residual PII after best-effort scrubbing, so confidentiality against the storage boundary matters.
## What #9 can code against
- **Scheme:** AES-256-GCM, 12-byte random nonce per object, 128-bit tag, header authenticated as AAD.
- **Object layout (R2 body):** `magic "LMB1"(4) || format_version(1) || key_id(2, BE) || nonce(12) || ciphertext(N) || tag(16)`.
- **Key custody:** versioned JSON keyring (`{active, keys{ver: base64-32B}}`) in Cloudflare Secrets Store, bound to both Workers; read at runtime via `await env.BUGREPORT_ENC_KEYRING.get()`; never logged/returned.
- **Weekly publish:** recommended as a Cron-Trigger Worker sharing the same binding so the key never leaves Cloudflare (GitHub Actions decryption explicitly discouraged — it would widen custody).
- **Rotation:** add a new key version + bump `active`; the `key_id` in each object header selects the right version, so old objects keep decrypting; never drop a version still referenced.
## Implementation note flagged for #9
TinyGo's `crypto/aes` and `crypto/cipher` currently fail their test suites, so the ADR recommends doing AES-GCM via the Workers host **Web Crypto API (SubtleCrypto)** rather than pure-Go crypto. The wire format is identical either way.
## Open questions for the maintainer (see ADR "Open questions")
1. Weekly publisher = Cron Worker (assumed) or GitHub Action?
2. Crypto provider = host SubtleCrypto (recommended) or pure-Go once TinyGo is verified?
3. Keyring shape/binding name confirmation.
4. Are R2 objects deleted after publication (bounds rotation burden)?
## Notes
- Per ticket scope this PR does **not** edit the README (to avoid conflicting with the parallel bootstrap PR). The ticket's acceptance criteria mention linking the ADR from the README — suggest adding a one-line link under the README "Status" section once the bootstrap PR lands (e.g. `Encryption/key-custody decision: docs/decisions/encryption.md`).
Closes #5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Adds
docs/decisions/encryption.md— the ADR for how scrubbed bug reports are encrypted at rest in R2 and how the key is held. Decision/ADR ticket only (no implementation code); it unblocks #9.Decision
Worker-side AES-256-GCM authenticated encryption, applied in the Worker before the object is written to R2, with the key held as a versioned keyring in Cloudflare Secrets Store.
What #9 can code against
magic "LMB1"(4) || format_version(1) || key_id(2, BE) || nonce(12) || ciphertext(N) || tag(16).{active, keys{ver: base64-32B}}) in Cloudflare Secrets Store, bound to both Workers; read at runtime viaawait env.BUGREPORT_ENC_KEYRING.get(); never logged/returned.active; thekey_idin each object header selects the right version, so old objects keep decrypting; never drop a version still referenced.Implementation note flagged for #9
TinyGo's
crypto/aesandcrypto/ciphercurrently fail their test suites, so the ADR recommends doing AES-GCM via the Workers host Web Crypto API (SubtleCrypto) rather than pure-Go crypto. The wire format is identical either way.Open questions for the maintainer (see ADR "Open questions")
Notes
Encryption/key-custody decision: docs/decisions/encryption.md).Closes #5