Skip to content

feat: client-side envelope encryption of binaries, flow zips & env vars - #94

Open
riglar wants to merge 4 commits into
devfrom
feat/binary-envelope-encryption
Open

feat: client-side envelope encryption of binaries, flow zips & env vars#94
riglar wants to merge 4 commits into
devfrom
feat/binary-envelope-encryption

Conversation

@riglar

@riglar riglar commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Client-side envelope encryption of every sensitive input the CLI uploads — the app binary (APK/IPA), the Maestro flow zip, and the injected --env secrets — 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 set DCD_ENCRYPT=1), so this is safe to ship dormant.

Companion to the platform (API + simulator) decrypt halves, which are backward-compatible — anything without an enc marker is treated as plaintext and passes through untouched.

How it works

  • Per-upload DEK — each artifact gets its own random 256-bit key. The binary and flow zip are chunked AES-256-GCM containers (streamed / constant memory, per-segment tags); the env map is a single-segment inline blob.
  • KEK — one long-lived X25519 keypair per environment. The public half is pinned in this CLI; the private half lives only on the platform API and is never shipped here. Each DEK is wrapped to the public key with an X25519 sealed box.
  • Where the envelope rides:
    • binary → binaries.metadata.enc = { v, kek, wrapped_key }
    • flow zip → uploads.metadata.enc (same shape; its own DEK)
    • env vars → results.env.enc = { v, kek, wrapped_key, ciphertext } (ciphertext inline)
  • The sha sent 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.ts pins the dev and prod KEK 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.tsencryptToContainer (in-memory DCDE twin of the streaming file encryptor), encryptFlowBuffer, encryptEnv, and a shared isEncryptionEnabled(). Byte-compatible with the platform decrypt half.
  • src/services/test-submission.service.ts is the single seam for flow + env: when encrypting, the flow zip becomes ciphertext (fields.sha = ciphertext hash), the flow envelope rides fields.enc as a JSON string (so both the JSON submitFlowTest body and the legacy multipart form carry it identically), and the env map becomes an { enc } envelope. Both the cloud command and the MCP tool get this via buildTestPayload.
  • --encrypt / DCD_ENCRYPT=1 now gate all three; DCD_ENCRYPT_BINARIES=1 stays as an alias.

Verification

  • Round-trip tests decrypt CLI-produced ciphertext (binary, flow, env) through a mirror of the platform decrypt path — incl. a 15 MB fixture, tamper/truncation rejection, DEK wrap/unwrap, and that flow + env get distinct DEKs. Pinned keys asserted to resolve to the expected version + key.
  • pnpm lint (0 errors) · pnpm build clean · unit suite green (178 passing).
  • Cross-checked byte-for-byte against the platform API envelope implementation.

Rollout

Ships dormant. Enabling --encrypt for 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 flow enc field). Until then, leave the flag off; legacy plaintext uploads are unaffected.

🤖 Generated with Claude Code

riglar and others added 3 commits July 24, 2026 11:54
…#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>
@riglar riglar changed the title feat: client-side envelope encryption of app binaries feat: client-side envelope encryption of binaries, flow zips & env vars Jul 24, 2026
@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Code review

No 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant