# Export Center KMS Envelope Pattern (age + AES-GCM) Status: Adopted for Sprint 0164-0001-0001 (ExportCenter III) Scope: Defines deterministic envelope handling for mirror bundle encryption (`EXPORT-SVC-37-002`) and general export signing. Applies to worker path and verification docs. ## Key hierarchy - **Content key (DEK):** 32-byte random generated per export run. Used for AES-256-GCM over encrypted payloads (`/data` subtree for mirror; optional for others). - **Nonce:** 12-byte random per file; stored alongside ciphertext; derive Additional Authenticated Data (AAD) as `{runId}:{relativePath}` to bind file path and run. - **Wrapping keys:** - **age recipients** (preferred for offline): each tenant can list one or more age public keys. DEK is wrapped once per recipient using age X25519. Store `recipient`, `wrappedKey` (base64), and optional `keyId` in provenance. - **KMS envelope** (Authority/HSM): DEK wrapped with tenant-scoped KMS key alias `export/envelope`. Store `kmsKeyId` (authority URI or external ARN) and `wrappedKey` (base64) plus KMS-provided `algorithm`. ## Write path (worker) 1) Generate DEK (32 bytes) per run; zeroize after use. 2) For each encrypted file, derive AAD = `{runId}:{relativePath}`; encrypt with AES-256-GCM (nonce per file). Store `nonce` and `ciphertext`. 3) Wrap DEK for all configured recipients: - age: `age --encrypt --recipient ` over DEK bytes → base64. - KMS: `Encrypt`/`WrapKey` with `KeyId=export/envelope` and `EncryptionContext={runId,tenant}` → base64. 4) Record wrapping metadata in `provenance.json` under `environment.encryption.recipients[]` preserving deterministic order (age recipients lexicographically by `recipient`, then KMS entries by `kmsKeyId`). 5) Include `encryption.mode` (`age` or `aes-gcm+kms`), `aadFormat`, and `nonceFormat` in provenance for verification tooling. ## Read/verification path 1) Select a recipient entry that matches available keys (age private key or KMS key). 2) Unwrap DEK: - age: `age --decrypt` → DEK bytes. - KMS: `Decrypt`/`UnwrapKey` with same encryption context. 3) For each encrypted file, recompute AAD from `{runId}:{relativePath}`, decrypt with AES-256-GCM using stored `nonce`, verify tag. 4) Recompute SHA-256 of decrypted payload and compare with `export.json` entries. ## Determinism & offline posture - Recipient lists and wrapped keys are ordered deterministically to keep `provenance.json` hashes stable across retries. - age path works fully offline; KMS path requires Authority/HSM availability but stores all metadata to allow later decryption once KMS is reachable. - Use fixed casing and field names: `mode`, `recipients[] {type, recipient|kmsKeyId, wrappedKey, keyId?}` and `aadFormat`. ## Testing notes - Add regression cases that encrypt/decrypt fixtures with both age and KMS paths, asserting identical manifest/provenance hashes across reruns. - Ensure decryption fails when AAD does not match expected `{runId}:{relativePath}` (prevents path swapping). - Keep tests air-gap friendly: mock KMS wrapper with deterministic stub keys. ## Rollout guidance - Default to age recipients for Offline Kit deployments; enable KMS wrapping where Authority/HSM is reachable. - Configuration knobs: - `ExportCenter:Encryption:Mode` = `age` | `kms` - `ExportCenter:Encryption:Recipients` = list of age public keys - `ExportCenter:Encryption:KmsKeyId` = tenant-specific key alias (when using KMS) - Documented verification commands should reference this pattern (update CLI/Console guides when payloads change).