Anti-AI-decompilation hardening — complete slate (cohort + runtime link + off-device secret + oracle/impurity/per-unit-salt) - #5
Open
owengregson wants to merge 13 commits into
Open
Conversation
Root-cause (Ruam ships the decryptor with the ciphertext; four attacker primitives P1-P4) + honest ceiling, derived from a frontier-model deobfuscation transcript and a 47-agent adversarial swarm. Slate W0-W5: source-map gate, internal-oracle annihilation, per-unit heterogeneity, gated decode-path impurity, mandated cross-file cohort + opt-in runtime link, opt-in off-device external secret. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Directory mode copies the whole input tree, so .js.map files (carrying sourcesContent = original source) shipped verbatim next to obfuscated output. New centralized source-map-gate deletes *.map and strips any surviving sourceMappingURL annotation. On by default; --keep-source-maps opts out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Directory/bundle obfuscation now forms a cohort: an order-independent digest of every file's source, folded with a per-build cohort seed, is XOR-folded into each file's key anchor via the same seam integrityHash uses (build keyAnchor ^= term; runtime _ka ^= term). Mirrors exactly => build==runtime symmetric across all seeds (verified 50 random-seed round-trips). New: compiler/cohort.ts (CohortContext/createCohort), obfuscateBundle() public API, optional cohort param threaded through obfuscateCode/transform. runVmObfuscation + CLI directory mode + index batch now route through the cohort and the source-map gate. Per-file hermeticity preserved otherwise. Honest scope: work-factor (raises static-correlation unit to the whole bundle; identical source diverges per bundle/build). Not cryptographic against a whole-bundle attacker — that is W4-L2 (runtime link) + W5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The one lever that crosses from work-factor to cryptographic hardness: fold fnv1a(secret) into the key anchor at build, recompute fnv1a over a host-provided accessor at runtime and fold identically. The secret VALUE is never embedded (only the dotted accessor path), so the artifact cannot derive its key without the correct runtime secret => offline AI reconstruction is denied. Opt-in only (no preset), auto-enables rollingCipher; reuses the integrity-hash/_ka fold seam (build==runtime symmetric). Accessor emitted as plain member access (no eval, CSP-safe), read wrapped in try/catch so an absent secret yields garbage not a crash. Verified across random seeds: correct secret => correct output (25/25), wrong/absent secret => denied (25/25), secret never appears in output, invalid-accessor build-time dev gate. CLI: --external-key-value/-accessor. Trade-off (documented): breaks offline use of the protected asset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tell) Internal-oracle annihilation (W1, safe slice). The transcript's step 15 oracle: a wrong rolling-cipher key yields out-of-range physical opcodes, so _ht[PH] is undefined and dispatch throws / lands outside the handler map => the attacker KNOWS the key is wrong and self-corrects. Slow-path resolution is now _ht[PH] | 0: an out-of-range/hole index (only reachable under a wrong key) coerces undefined -> handler 0 and dispatches a plausible-but-wrong handler instead of revealing the error. This matches the decode-cache path, where Int32Array storage already coerces undefined -> 0. Provably safe: | 0 is identity on every valid handler index, so legitimate execution is byte-for-byte unchanged. Verified across 4 slow-path configs (max / opcodeMutation / observationResistance / incrementalCipher) x 4 programs x 12 seeds, plus the full 2365-test suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The literal 'a file cannot run without the others' lever. A declared
provider file writes a per-cohort secret to a shared global at load; each
consumer folds fnv1a(that secret) into its key (reusing the externalKey
fold). STRICT — no fallback: a consumer cannot decrypt without its
provider present-and-earlier in the same realm (verified 20/20 runs with
provider, 20/20 denied without).
API: obfuscateBundle(files, options, link?) where link = {provider,
consumers}; paths matched by exact-or-basename. CLI: --link-provider /
--link-consumers. Provider write is char-code (CSP-safe, no plaintext,
no eval), prepended on every return path incl. shielded/no-unit files.
Caller owns the co-residence contract (prove-or-don't-link by explicit
declaration); mis-declaring bricks the consumer by design.
Honest scope: still work-factor vs an attacker holding the whole bundle
(co-acquired = co-possessed), but raises the unit and delivers the
literal cross-file dependency. Default-OFF, opt-in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…key reuse) Two units with identical metadata (instr/reg/param/const counts) derived the SAME implicit key => same position keystream => an attacker who recovered one unit's keystream could decrypt another's same-position instructions. Each unit now carries a per-unit salt (deriveSeed(seed, 'unitSalt:'+id)) in its serialized header, folded into the key anchor at build and mirrored by rcDeriveKey's 'k ^= u.us' at runtime. Salts both the instruction keystream AND the string-constant key (both derive from the single rcDeriveKey(U) path), so symmetry is by construction. New serialized u32 header field + _dsk temp + U.us. Verified: full suite 2370 pass; duplicate-metadata functions round-trip across rolling/medium/ max/incremental x seeds; per-unit salt distinctness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(W3) The strongest on-device anti-agentic lever. Each instruction's decryption keystream folds in an accumulator chained from every prior decrypted instruction (linear decode order), removing the position cipher's random-access property: an attacker cannot decrypt instruction K without first decrypting 0..K-1, forcing full sequential per-unit simulation and defeating cheap transferable automation. Build==runtime symmetric because the decode cache materializes the stream in a single linear forward pass at BOTH build and runtime (independent of execution path), so the accumulator evolves identically. Gated to cache-active builds; INCOMPATIBLE with incrementalCipher / opcodeMutation / observationResistance / vmShielding (each disables/bypasses the cache) — combining throws at build time. Every encoded unit passes a MANDATORY build-time self-equality gate (assertChainedDecryptInverts) — a divergence is a loud build failure, never a silent miscompile (honoring all-seeds / no-silent-garbage). Opt-in, no preset, auto-enables rollingCipher. CLI: --decode-impurity. Verified: 60 build==runtime round-trips across programs x seeds; gate fires on tampered ciphertext; full suite 2381 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Pull request overview
This PR introduces a multi-layer “anti-AI decompilation hardening” set of features for Ruam, centered around (a) preventing cleartext leaks in directory/bundle output, (b) increasing per-bundle/per-unit key diversity, and (c) adding optional runtime-bound keying mechanisms (cross-file linking + off-device secret binding), with test coverage for each layer.
Changes:
- Add default-on source-map gating for directory outputs (delete
*.mapand stripsourceMappingURLcomments), with--keep-source-maps/keepSourceMapsopt-out. - Add cross-file cohort support (
createCohort,obfuscateBundle) and optional strict cross-file runtime linking (provider writes secret; consumers require it). - Add rolling-cipher hardening: per-unit key salt (wire format change), optional decode-impurity chained keystream with a mandatory build-time self-equality gate, plus optional
externalKeyBindingfolding a host-provided secret into the key anchor.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ruam/test/security/source-map-gate.test.ts | Tests source-map stripping and .map deletion across a directory tree. |
| packages/ruam/test/security/per-unit-salt.test.ts | Tests per-unit salt behavior across duplicate-metadata units and seeds. |
| packages/ruam/test/security/oracle-annihilation.test.ts | Tests hole-tolerant slow-path dispatch invariants across configs/seeds. |
| packages/ruam/test/security/external-key-binding.test.ts | Tests externalKeyBinding secret non-embedding + runtime correctness/denial. |
| packages/ruam/test/security/decode-impurity.test.ts | Tests chained rolling cipher + build-time self-equality gate + incompatibilities. |
| packages/ruam/test/security/cross-file-linking.test.ts | Tests strict provider/consumer runtime link behavior for bundles. |
| packages/ruam/test/security/cohort.test.ts | Tests cohort digest determinism + bundle build/runtime symmetry. |
| packages/ruam/test/helpers.ts | Adds helpers for evaluating bundle output in isolated/shared contexts. |
| packages/ruam/src/types.ts | Adds externalKeyBinding and decodeImpurity options + docs. |
| packages/ruam/src/transform.ts | Threads cohort/link/external key terms into key anchor; adds obfuscateBundle; adds per-unit salt plumbing; supports decode impurity. |
| packages/ruam/src/source-map-gate.ts | Implements .map deletion + sourceMappingURL stripping across output trees. |
| packages/ruam/src/ruamvm/builders/rolling-cipher.ts | Runtime derive-key folds in per-unit salt (u.us). |
| packages/ruam/src/ruamvm/builders/interpreter.ts | Implements decode-impurity chained keystream in cache materialization; adds _ht[PH] | 0 slow-path hole tolerance. |
| packages/ruam/src/ruamvm/builders/external-key.ts | Emits runtime fold reading secret via accessor + FNV-1a into key anchor. |
| packages/ruam/src/ruamvm/builders/deserializer.ts | Updates wire format to read/store per-unit salt field (us). |
| packages/ruam/src/ruamvm/assembler.ts | Wires cohort term + external key accessor folds into runtime key anchor init; plumbs decodeImpurity option. |
| packages/ruam/src/presets.ts | Excludes new options from presets typing; auto-enables rollingCipher for new features. |
| packages/ruam/src/naming/claims.ts | Reserves _dsk temp key. |
| packages/ruam/src/index.ts | Exposes obfuscateBundle/cohort APIs; routes directory obfuscation through bundling; runs source-map gate. |
| packages/ruam/src/compiler/rolling-cipher.ts | Implements chained encryption + mandatory self-equality gate helpers. |
| packages/ruam/src/compiler/encode.ts | Adds per-unit salt to unit header and folds into key derivation; supports chained rolling encryption option. |
| packages/ruam/src/compiler/cohort.ts | Implements cohort digesting and resolved provider/consumer link material. |
| packages/ruam/src/cli.ts | Adds CLI flags for keep-source-maps, decode impurity, external key binding, and runtime linking; changes directory mode to pre-read sources/cohort + gate maps. |
| docs/superpowers/specs/2026-06-30-anti-ai-decompilation-design.md | Design spec describing threat model, layers, scope, and invariants. |
| docs/superpowers/plans/2026-06-30-anti-ai-hardening.md | Implementation plan and wiring/testing strategy for W0–W5 layers. |
| CLAUDE.md | Architecture notes documenting new hardening layers and their constraints. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ider guard Copilot review (PR #5): - external-key.ts: generate the try/catch binding via nameGen() instead of the hard-coded "_eke" (NameRegistry invariant). The catch body is empty so there is no behavior change — this only removes the parallel-naming outlier. - cli.ts directory mode: track per-file read success. A pre-read failure is now surfaced as an error and the file is SKIPPED in the write loop, so the copied original is never overwritten with output generated from empty input (the data-loss path Copilot flagged). - cli.ts: surface a source-map gate failure as a visible warning instead of silently swallowing it — that gate is what prevents shipping original source via .map files, so a swallowed throw could leak source unnoticed. Adversarial self-review (confirmed real defect in the touched path): - The read-skip above interacts with the strict cross-file link. If the declared --link-provider is missing from the obfuscated set or fails to read, its runtime secret write is never emitted while consumers still bake in the strict (no- fallback) fold — so the consumers ship NON-RUNNABLE yet are reported as successes. Now fail loudly: abort before writing any consumer when the provider is missing or unreadable, and name the blast radius when the provider reads OK but fails to obfuscate. Normal (no --link) runs are unaffected (guard is gated on args.link). All 2381 tests pass; typecheck clean; lib builds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-contained prompt for a fresh agent to run the full ideation -> adversarial critique -> spec -> plan -> TDD -> PR loop for a superior anti-AI-decompilation layer. Centers invention on AI-agent-specific weaknesses (lift-and-run, oracle-seeking, pattern-matching, confident hallucination, fan-out, adversarial-input), keeps gen-1's hard constraints (all-seeds build==runtime symmetry, CSP-safe, NameRegistry, no size-bloat), and adds a hard product boundary: server-free / no off-device secret. Reframes the gen-1 "honest ceiling" as a floor to beat while keeping honesty strict on claims and tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines
+472
to
487
| export function obfuscateBundle( | ||
| files: BundleFile[], | ||
| options: VmObfuscationOptions = {}, | ||
| link?: CohortLink | ||
| ): BundleFile[] { | ||
| // A cohort is created when there are 2+ files OR a runtime link is declared | ||
| // (a link needs a shared cohort even if the provider/consumer set is small). | ||
| const cohort: CohortContext | undefined = | ||
| files.length >= 2 || link | ||
| ? createCohort(files as CohortFile[], generateCryptoSeed(), link) | ||
| : undefined; | ||
| return files.map((f) => ({ | ||
| path: f.path, | ||
| code: obfuscateCode(f.code, options, cohort, f.path), | ||
| })); | ||
| } |
Comment on lines
+66
to
+73
| const { globby } = await import("globby"); | ||
| const all = await globby(["**/*"], { | ||
| cwd: outputDir, | ||
| ignore: ["**/node_modules/**"], | ||
| absolute: true, | ||
| dot: true, | ||
| onlyFiles: true, | ||
| }); |
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.
Anti-AI-decompilation hardening — complete slate (8 layers)
Attacks the root capability a generalized AI decompiler uses, not the specific steps any one transcript took. Derived from a frontier-model deobfuscation transcript of a
max-preset artifact + a 47-agent adversarial swarm.Root cause (validated)
Ruam ships the decryptor with the ciphertext. A complete, deterministic, secret-free machine that must self-decode on the attacker's hardware — so the attack reduces to four primitives: P1 recognize, P2 lift-and-run, P3 observe-at-a-tap, P4 oracle (P4a internal = killable; P4b terminal I/O = unkillable).
Honest ceiling
A single self-contained file — or a co-resident bundle — cannot be made impossible to reconstruct (client-executable code is client-observable; passive reads are undetectable in JS). On-device levers RAISE WORK FACTOR and DESTROY automatability/transferability; the only cryptographic lever is an off-device secret. This PR is framed accordingly — no over-claims.
Every layer (each tested across many random seeds; full suite 2381 pass / 0 fail; typecheck clean; lib builds)
*.map(= original source) + stripssourceMappingURL. Default-on;--keep-source-maps._ht[PH] | 0: a wrong-key decode → handler 0 instead of an "undefined opcode" tell.| 0identity on valid indices ⇒ legit execution unchanged.rcDeriveKey). Closes same-metadata key reuse (shared position keystream).obfuscateBundle()— a per-bundle digest folded into each file's key anchor.crossFileLinking: a consumer cannot run without its declared provider present in the same realm (strict, no fallback).externalKeyBindingLocked product decisions (honored)
On the three previously-deferred items (now built, per request)
Invariants respected
All-seeds correctness (no flaky tests),
deriveSeed-only, NameRegistry for all identifiers, noeval/CSP-safe, no size-bloat, build==runtime fold symmetry, watermark untouched, additive API (obfuscateCode/obfuscateFilesignatures unchanged).Docs
docs/superpowers/specs/2026-06-30-anti-ai-decompilation-design.mddocs/superpowers/plans/2026-06-30-anti-ai-hardening.md🤖 Generated with Claude Code