|
| 1 | +# Task: Chipotle V3 — Universal Encrypt/Decrypt Alignment |
| 2 | + |
| 3 | +**Task ID**: CHIPOTLE-V3-UNIVERSAL-ACTIONS |
| 4 | +**Created**: 2026-05-18 |
| 5 | +**Status**: InProgress (Phase 1 partially landed in `6bf1cddd6 "1st attempt"`) |
| 6 | +**Priority**: **P0 — Security + Architecture** — CEK must never appear in plaintext between components |
| 7 | +**Branch**: current HEAD (atop `6bf1cddd6`) |
| 8 | +**Owner**: Irzhy + Sasha |
| 9 | +**Related**: `SEC-2026-04-28-WAVE8-CHIPOTLE-HARDENING` (predecessor — C-02 kid binding), |
| 10 | +`LIT-ACTION-SIGNATURE-AUTH` (predecessor — session delegation), |
| 11 | +`docs/core/plans/chipotle_security_alignment.md` (specification) |
| 12 | + |
| 13 | +## TL;DR |
| 14 | + |
| 15 | +The Chipotle Lit Actions have been upgraded to a unified encrypt/decrypt model: |
| 16 | + |
| 17 | +- **Encryption** returns `{ ciphertext, hash, signature, issuer }` with a composite |
| 18 | + hash `SHA-256(CEK ‖ KID ‖ authority)` that replaces the old `KID = sha256(CEK)[0:16]` |
| 19 | + binding. |
| 20 | +- **Decryption** wraps the CEK in an ECDH envelope using the requester's |
| 21 | + `sessionPublicKey` instead of returning it as plaintext. The requester |
| 22 | + unwraps locally. |
| 23 | +- **Media and non-media use the same actions** — no more separate code paths. |
| 24 | + |
| 25 | +New CIDs: |
| 26 | +- Encrypt: `QmVEz3dDnQD1n96gMd2mFZWXdEDsRiPMumx86qMzhT35gY` |
| 27 | +- Decrypt: `QmPBjQD7V4aFTZPxUwZ9gDPFJtcJ4SvsJdTh3QexTyRBbj` |
| 28 | + |
| 29 | +The PC2 node must align with these changes. Commit `6bf1cddd6` landed partial |
| 30 | +work (Lit Action files + encrypt-path scaffolding) but the decrypt path, |
| 31 | +protection data format, CID wiring, and ECDH envelope unwrapping are incomplete. |
| 32 | + |
| 33 | +## Background |
| 34 | + |
| 35 | +### What changed in the Lit Actions |
| 36 | + |
| 37 | +1. **`universal-encrypt-chipotle.js`** (new, replaces `non-media-encrypt-chipotle.js`): |
| 38 | + - Accepts: `plaintext` (base64 CEK), `kid` (base64), `authority` (hex), `pkpId` |
| 39 | + - Computes composite hash: `SHA-256(cekBytes ‖ kidBytes ‖ authorityBytes)` |
| 40 | + - Signs the hash with the PKP's secp256k1 key |
| 41 | + - Encrypts CEK via `Lit.Actions.Encrypt({ pkpId, message: plaintext })` |
| 42 | + - Returns: `{ ciphertext, hash, signature, issuer }` (encoding via `outputFormat`) |
| 43 | + |
| 44 | +2. **`universal-decrypt-chipotle.js`** (new, replaces both `non-media-decrypt-chipotle.js` |
| 45 | + and the old media decrypt): |
| 46 | + - Full session-bundle validation (delegation + request, same as V1.2 sigauth) |
| 47 | + - Recomputes and verifies composite hash binding (CEK ↔ KID ↔ authority) |
| 48 | + - Optional issuer signature verification |
| 49 | + - **Does NOT return plaintext CEK** — wraps it in an ECDH envelope: |
| 50 | + - ECDH key agreement: `deriveKey(pkpPrivateKey, sessionPublicKey)` → AES-CBC-256 |
| 51 | + - IV = first 16 bytes of session public key |
| 52 | + - Plaintext layout: `metaLen | issuer(20B) | exp(8B) | audience(20B) | keyCount | cek` |
| 53 | + - Response includes metadata block (compressed PKP pubkey + signature + signer) |
| 54 | + - Supports `keyAlg: { name: "ECDH", namedCurve: "P-256" }` (required) or `{ name: "X25519" }` |
| 55 | + |
| 56 | +### What `6bf1cddd6` ("1st attempt") already changed |
| 57 | + |
| 58 | +| File | What landed | What's missing / broken | |
| 59 | +|---|---|---| |
| 60 | +| `data/lit-actions/universal-encrypt-chipotle.js` | ✅ Complete | — | |
| 61 | +| `data/lit-actions/universal-decrypt-chipotle.js` | ✅ Complete | — | |
| 62 | +| `chipotle-client.ts` | `EncryptParams` got `kid?`/`authority?`; `encryptWithLitAction` encodes raw bytes as base64 (was UTF-8), parses `hash`/`issuer`/`signature` from response | Still loads `non-media-encrypt-chipotle.js` not universal; doesn't pass `kid`/`authority` to jsParams; no unified decrypt function; no ECDH envelope unwrapping; no new CID constants | |
| 63 | +| `storage.ts` | Encrypt path passes raw CEK bytes; Datil path removed | Decrypt path untouched; default action CID still `bafkrei…` (V1.2 sigauth); no `kid`/`signature`/`issuer` in response | |
| 64 | +| `dashPackager.ts` | `encryptMediaCEK` passes raw CEK bytes (was UTF-8) | **REGRESSION**: `generateCEK` reverted to `kid = sha256(cek)[0:16]` (should be random UUID); `MEDIA_DECRYPT_ACTION_CID` not updated; `kid`/`authority` not passed to encrypt; protection data format not updated | |
| 65 | +| `media.ts` | — | Completely untouched | |
| 66 | + |
| 67 | +## Requirements |
| 68 | + |
| 69 | +### Must-have |
| 70 | + |
| 71 | +- [ ] **CEK never in plaintext** between Lit Action response and the consuming |
| 72 | + component. All decrypt paths produce an ECDH envelope; the server |
| 73 | + unwraps locally using its ephemeral P-256 private key. |
| 74 | +- [ ] **Unified encrypt**: `encryptWithLitAction` calls `universal-encrypt-chipotle.js` |
| 75 | + with `kid`, `authority`, `outputFormat: "hex"`. KID is generated independently |
| 76 | + from CEK. |
| 77 | +- [ ] **Unified decrypt**: single function `recoverCEKViaEnvelope` replaces both |
| 78 | + `recoverNonMediaCEK` and the Datil ECDH path in `media.ts`. Generates |
| 79 | + P-256 ephemeral keypair, sends `publicKey` + `keyAlg` to Lit Action, |
| 80 | + receives ECDH envelope, unwraps to raw CEK. |
| 81 | +- [ ] **Protection data format** aligned with keystore service: |
| 82 | + `protocolVersion: "3.0"`, `signature`, `issuer`, `format: "hex"`, |
| 83 | + `algorithm` field, no `ciphersuite`. |
| 84 | +- [ ] **Legacy asset compatibility**: if `actionIpfsId` is present in protection |
| 85 | + data → use that CID (legacy action); if response `.data` is ≤16 bytes |
| 86 | + raw → treat as legacy plaintext CEK. |
| 87 | +- [ ] **CID constants updated**: default decrypt = `QmPBjQD7V4aFTZPxUwZ9gDPFJtcJ4SvsJdTh3QexTyRBbj`, |
| 88 | + encrypt = `QmVEz3dDnQD1n96gMd2mFZWXdEDsRiPMumx86qMzhT35gY`. |
| 89 | +- [ ] `keyAlg: { name: "ECDH", namedCurve: "P-256" }` explicitly in all decrypt calls. |
| 90 | + |
| 91 | +### Should-have |
| 92 | + |
| 93 | +- [ ] Documentation: `docs/core/CHIPOTLE_V3_PROTOCOL.md` covering full protocol. |
| 94 | +- [ ] `non-media-encrypt-chipotle.js` and `non-media-decrypt-chipotle.js` retained |
| 95 | + on disk but no longer loaded by default (legacy fallback only). |
| 96 | + |
| 97 | +### Out of scope |
| 98 | + |
| 99 | +- Player-side (`media-player/`) changes — handled separately. |
| 100 | +- Chipotle allowlist registration for the new CIDs (operational, not code). |
| 101 | +- Supernode `ddrm-config.json` updates (deployment step). |
| 102 | + |
| 103 | +## Implementation Plan |
| 104 | + |
| 105 | +See per-phase task files for detailed implementation: |
| 106 | + |
| 107 | +- [x] **Phase 0**: Lit Action files landed — [`PHASE-0-LIT-ACTIONS.md`](./PHASE-0-LIT-ACTIONS.md) |
| 108 | +- [ ] **Phase 1**: Encryption path — [`PHASE-1-ENCRYPT.md`](./PHASE-1-ENCRYPT.md) |
| 109 | +- [ ] **Phase 2**: ECDH envelope unwrapping — [`PHASE-2-ECDH-UNWRAP.md`](./PHASE-2-ECDH-UNWRAP.md) |
| 110 | +- [ ] **Phase 3**: Unified decryption — [`PHASE-3-DECRYPT.md`](./PHASE-3-DECRYPT.md) |
| 111 | +- [ ] **Phase 4**: Protection data format — [`PHASE-4-PROTECTION-DATA.md`](./PHASE-4-PROTECTION-DATA.md) |
| 112 | +- [ ] **Phase 5**: Documentation — [`PHASE-5-DOCS.md`](./PHASE-5-DOCS.md) |
| 113 | + |
| 114 | +## Acceptance Criteria |
| 115 | + |
| 116 | +1. `cd pc2-node && npx tsc --noEmit` compiles cleanly. |
| 117 | +2. `POST /api/storage/lit/encrypt` returns `kid`, `signature`, `issuer`, |
| 118 | + `ciphertext`, `dataToEncryptHash` — all hex-encoded. |
| 119 | +3. `POST /api/storage/lit/secure-view` with a newly encrypted non-media asset |
| 120 | + recovers CEK via ECDH envelope (no plaintext CEK in Lit Action response). |
| 121 | +4. Media DASH packaging embeds new CIDs, `signature`, `issuer` in PSSH. |
| 122 | +5. Existing assets with old `actionIpfsId` still decrypt (legacy compatibility). |
| 123 | +6. No code path logs, caches, or transmits the raw CEK in plaintext between |
| 124 | + the Lit Action response and the AES-decrypt call. |
| 125 | + |
| 126 | +## Files Modified |
| 127 | + |
| 128 | +| File | Phase | Change | |
| 129 | +|---|---|---| |
| 130 | +| `pc2-node/src/api/chipotle-client.ts` | 1,2,3 | New CID constants, universal encrypt loader, `recoverCEKViaEnvelope`, ECDH unwrap | |
| 131 | +| `pc2-node/src/api/storage.ts` | 1,3,4 | Encrypt response, decrypt path, default CID, protection data | |
| 132 | +| `pc2-node/src/services/media/dashPackager.ts` | 1,4 | CID updates, independent KID, protection data format | |
| 133 | +| `pc2-node/src/api/media.ts` | 2,3 | Extract ECDH helpers, unified decrypt, remove Datil path | |
| 134 | + |
| 135 | +## Files Created |
| 136 | + |
| 137 | +| File | Phase | Purpose | |
| 138 | +|---|---|---| |
| 139 | +| `docs/core/CHIPOTLE_V3_PROTOCOL.md` | 5 | Full protocol documentation | |
| 140 | +| `.cursor/tasks/CHIPOTLE-V3-UNIVERSAL-ACTIONS/*.md` | — | This task + phase docs | |
| 141 | + |
| 142 | +## Hard Constraints |
| 143 | + |
| 144 | +- Do NOT delete `non-media-encrypt-chipotle.js` or `non-media-decrypt-chipotle.js` |
| 145 | + from disk — existing delegations may reference them. |
| 146 | +- Do NOT change the ECDH envelope wire format — the player-side decoder must |
| 147 | + stay compatible. |
| 148 | +- Do NOT log CEK bytes, CEK base64, or CEK hex at any log level. |
| 149 | +- Do NOT accept `rpc` from client/PSSH — always use `getBaseRpcUrl()` (M-01 fix). |
| 150 | + |
| 151 | +## Status History |
| 152 | + |
| 153 | +| Date | Status | Note | |
| 154 | +|---|---|---| |
| 155 | +| 2026-05-18 | InProgress | `6bf1cddd6` landed Lit Action files + partial encrypt scaffolding | |
| 156 | +| 2026-05-18 | InProgress | Task doc + phase breakdown created | |
0 commit comments