feat: client-side envelope encryption of binaries, flow zips & env vars - #94
Open
riglar wants to merge 4 commits into
Open
feat: client-side envelope encryption of binaries, flow zips & env vars#94riglar wants to merge 4 commits into
riglar wants to merge 4 commits into
Conversation
…#1138) Encrypt half of the dcd#1138 contract (the platform api + simulators are the decrypt half). Opt-in via `--encrypt` or `DCD_ENCRYPT_BINARIES=1`; off by default, so uploads are unchanged unless requested. - src/utils/envelope.ts: per-upload DEK, chunked AES-256-GCM container (streamed, constant memory), X25519 sealed-box DEK wrap. Byte-compatible with api/src/common/crypto/envelope.ts (format in dcd/docs/binary-envelope-encryption.md). - src/config/environments.ts: pinned per-env KEK public key slot (null until provisioned; DCD_BINARY_KEK_PUBLIC override for testing). - src/methods.ts: encrypt source in place before hashing/upload so the SHA, dedup check, and both uploaders operate on ciphertext (binaries.sha = ciphertext hash); attach the envelope to binaries.metadata.enc at finalise. - src/types.ts: TAppMetadata.enc; binary.flags.ts: --encrypt; cloud/upload thread the flag. - test/unit/envelope.test.ts: round-trips (incl. the 15MB wikipedia.apk fixture) through a decrypt mirroring the platform, tamper detection, and DEK wrap/unwrap. Verified end-to-end: CLI-encrypted output + wrapped DEK decrypt byte-for-byte with the real dcd api envelope code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fill in the previously-null kekPublicKey slots (version 1) now that the key-encryption keypairs are provisioned. These are X25519 *public* keys and are safe to embed in a public release, exactly like the Supabase anon keys already checked in here; the matching private halves live only on the platform API and are never shipped. Update the resolver test to assert each environment resolves to its pinned version and key rather than null. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the binary envelope scheme to the two remaining sensitive client-side
inputs — the Maestro flow zip and the injected --env KEY=VALUE secrets — so the
platform stores only ciphertext at rest. Enabling encryption now covers the
binary, the flow zip, and the env vars, each with its OWN per-upload DEK (all
wrapped under the same pinned per-environment KEK public key).
- utils/envelope.ts: encryptToContainer (in-memory DCDE twin of the streaming
file encryptor), encryptFlowBuffer, and encryptEnv (a single-segment inline
blob for results.env.enc), plus a shared isEncryptionEnabled(). Byte-compatible
with the platform decrypt half.
- test-submission.service.ts is the single seam: when encrypting, the flow zip
becomes a DCDE container (the sha sent to the API is the CIPHERTEXT hash), the
flow envelope rides fields.enc as a JSON string (so both the JSON and legacy
multipart submission paths carry it identically), and the env map becomes an
{ enc } envelope. Both the cloud command and the MCP tool get this for free
via buildTestPayload.
- --encrypt (and DCD_ENCRYPT=1) now gate all three; DCD_ENCRYPT_BINARIES=1 stays
as an alias for the binary-only behaviour.
- Round-trips flow + env ciphertext through a mirror of the platform decrypt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
The two KEK public keys pinned in bfa2b4c tripped gitleaks' default generic-api-key rule on entropy alone (5.02 / 4.89), failing the secret-scan job. They are base64 of the raw 32-byte X25519 *public* halves — encrypt-only, with the private halves living solely in API env config, never in this repo. Allowlisted by exact value, matching the convention already used for the Supabase anon keys and deliberately not by file path or by the whole rule: a KEK private key is byte-identical in shape to its public half, so a path allowlist would blind the scanner to a genuine leak in precisely the file most likely to contain one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
riglar
added a commit
that referenced
this pull request
Jul 26, 2026
`fetch-depth: 0` fetches refs/heads/* — every branch — and `gitleaks git` scans all reachable commits, not just the checked-out ref. So a branch that legitimately commits a high-entropy value together with its own .gitleaks.toml allowlist entry fails the secret-scan of every OTHER branch, because those are judged against the allowlist at their own tip. That is currently a hard deadlock. feat/binary-envelope-encryption (#94) pins two KEK public keys and allowlists them in the same branch, so any PR branched from dev fails secret-scan on commit bfa2b4c, while #94 itself cannot merge until it picks up this branch's `pnpm audit` fix. --log-opts=HEAD limits the scan to commits reachable from what is checked out: the full history of the branch, or of the PR merge commit (base + PR commits). Coverage is unchanged in the way that matters — every commit of the branch under test is still scanned, each branch is fully scanned by its own PR, and pushes to dev/production scan their full history. Verified with the pinned gitleaks 8.30.1: scoped to this branch, 61 commits, no leaks; scoped to feat/binary-envelope-encryption with this branch's allowlist, the same 2 findings still fire — so the scoping narrows which commits are in scope without weakening detection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
riglar
added a commit
that referenced
this pull request
Jul 27, 2026
* deps: patch js-yaml and brace-expansion DoS advisories `pnpm audit --audit-level moderate` was failing CI on three high advisories: - js-yaml 5.2.1 -> 5.2.2 (GHSA-pm4m-ph32-ghv5): exponential parsing time in flow collections. This one is reachable at runtime — js-yaml parses user Maestro flow files. Bumped the declared range and added a transitive override guard alongside the existing 3.x/4.x entries. - brace-expansion 5.0.7 -> 5.0.8 (GHSA-mh99-v99m-4gvg): unbounded expansion length causing an uncatchable OOM crash. Widened the existing override range. - brace-expansion 1.1.16, reached via eslint-plugin-import -> minimatch@3. Upstream published no 1.x or 2.x backport (maintenance-v1 is still 1.1.16, maintenance-v2 2.1.2 — both inside the vulnerable <=5.0.7 range), and 5.x cannot be forced into that path: v5 exports `{ expand }` while minimatch@3 requires a callable default. Overriding minimatch@3 -> 10.x breaks differently, since its CJS build has no callable default for eslint-plugin-import's interop require. So eslint-plugin-import is dropped instead. eslint.config.cjs registered it with no rules enabled — purely so legacy `import/...` disable comments resolve — and exactly one such comment remained (src/methods.ts, itself already a dead directive since neither `import/namespace` nor `new-cap` is enabled). Removing the plugin deletes the whole minimatch@3 -> brace-expansion@1.x subtree and avoids adding the GHSA to `ignoreGhsas`, which would also have silenced the fixable 5.x path. No other dependency resolutions change. The remaining ignored moderate (@hono/node-server, GHSA-frvp-7c67-39w9) is unchanged — the MCP SDK is already at its latest release, so there is no fix to take. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: scope the gitleaks scan to the branch under test `fetch-depth: 0` fetches refs/heads/* — every branch — and `gitleaks git` scans all reachable commits, not just the checked-out ref. So a branch that legitimately commits a high-entropy value together with its own .gitleaks.toml allowlist entry fails the secret-scan of every OTHER branch, because those are judged against the allowlist at their own tip. That is currently a hard deadlock. feat/binary-envelope-encryption (#94) pins two KEK public keys and allowlists them in the same branch, so any PR branched from dev fails secret-scan on commit bfa2b4c, while #94 itself cannot merge until it picks up this branch's `pnpm audit` fix. --log-opts=HEAD limits the scan to commits reachable from what is checked out: the full history of the branch, or of the PR merge commit (base + PR commits). Coverage is unchanged in the way that matters — every commit of the branch under test is still scanned, each branch is fully scanned by its own PR, and pushes to dev/production scan their full history. Verified with the pinned gitleaks 8.30.1: scoped to this branch, 61 commits, no leaks; scoped to feat/binary-envelope-encryption with this branch's allowlist, the same 2 findings still fire — so the scoping narrows which commits are in scope without weakening detection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Client-side envelope encryption of every sensitive input the CLI uploads — the app binary (APK/IPA), the Maestro flow zip, and the injected
--envsecrets — so the platform only ever stores ciphertext at rest. Opt-in and off by default: uploads are byte-for-byte unchanged unless you pass--encrypt(or setDCD_ENCRYPT=1), so this is safe to ship dormant.Companion to the platform (API + simulator) decrypt halves, which are backward-compatible — anything without an
encmarker is treated as plaintext and passes through untouched.How it works
binaries.metadata.enc = { v, kek, wrapped_key }uploads.metadata.enc(same shape; its own DEK)results.env.enc = { v, kek, wrapped_key, ciphertext }(ciphertext inline)shasent to the API is the ciphertext hash for binary and flow, so dedup / caching / range logic operate on ciphertext unchanged.Pinned public keys
src/config/environments.tspins thedevandprodKEK public keys (version 1). These are X25519 public keys — safe to embed in a public release, like the Supabase anon keys already here. Rotation is a versioned map: a new keypair gets a new version + pinned public key; old uploads keep decrypting under their original version with no re-encryption.DCD_BINARY_KEK_PUBLIC=<version>:<base64>overrides the pin for testing.Implementation
src/utils/envelope.ts—encryptToContainer(in-memory DCDE twin of the streaming file encryptor),encryptFlowBuffer,encryptEnv, and a sharedisEncryptionEnabled(). Byte-compatible with the platform decrypt half.src/services/test-submission.service.tsis the single seam for flow + env: when encrypting, the flow zip becomes ciphertext (fields.sha= ciphertext hash), the flow envelope ridesfields.encas a JSON string (so both the JSONsubmitFlowTestbody and the legacy multipart form carry it identically), and the env map becomes an{ enc }envelope. Both thecloudcommand and the MCP tool get this viabuildTestPayload.--encrypt/DCD_ENCRYPT=1now gate all three;DCD_ENCRYPT_BINARIES=1stays as an alias.Verification
pnpm lint(0 errors) ·pnpm buildclean · unit suite green (178 passing).Rollout
Ships dormant. Enabling
--encryptfor an environment requires that environment's KEK private key to be provisioned on the platform API, and the flow/env decrypt halves deployed (the platform-side flow submit must accept the flowencfield). Until then, leave the flag off; legacy plaintext uploads are unaffected.🤖 Generated with Claude Code