From aa9c102a5c775f35d9a9cd9e19747885ccbf1106 Mon Sep 17 00:00:00 2001 From: Arena Agent Date: Sun, 11 Oct 2026 16:46:18 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20CloakJS=201.3.0=20=E2=80=94=20real?= =?UTF-8?q?=20extreme=20mode=20and=20much=20stronger=20obfuscation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extreme is no longer a copy of high (the 1.2.0 presets were byte-identical after defaults were applied). It now adds identifier escaping, codepoint-safe string splitting, decoy string-array entries, maximum flattening/dead-code amounts and a recurring debugger trap. Library changes: - scope-aware identifier mangling: parameters (defaults, rest), catch bindings, class names, named function expressions and template substitutions; a spelling is renamed only when every occurrence lies inside a scope that declares it, so globals owned by other scripts stay intact - property names, object keys/shorthand, method and field names, labels, destructuring patterns, module specifiers and string keys are never touched - self-contained string decoder: own Base64 + alphabet substitution + RC4 + UTF-8 handling, no atob/Uint8Array/TextDecoder required by generated code - control-flow flattening inside function bodies (nested bodies first), with a shuffled case layout, a sequence array that fixes execution order and decoy cases that can never be selected; hoisting/scoping-sensitive statements and the final top-level statement are kept out of dispatchers - opaque predicates verified at obfuscation time, richer unreachable decoy blocks, static template literals encrypted like strings - self-defending prelude hashes generated functions (whitespace-insensitive) and throws Error("Unexpected token") when they are patched - typed option validation, seeded reproducible output, per-run stats that report the techniques actually applied, clearer warnings Docs and build: - README rewritten for 1.3.0: preset ladder, full option table, technique notes, behaviour caveats (selfDefending vs. re-minification, module syntax, debug protection) and the npm note — the `cloakjs` package on npm is an unrelated AOP/Proxy library, so the README points at the repo and jsDelivr and never tells anyone to install it - test suite expanded to 40 behavioural tests (every preset, option matrix, generators/classes/private fields/destructuring/ASI, guard tampering) - cloakjs.min.js regenerated with Terser 5.44.0; the workflow runs the tests, rebuilds and verifies the minified file before committing it Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .github/workflows/minify.yml | 90 +- README.md | 210 +++- cloakjs.js | 2122 ++++++++++++++++++++++++++++------ cloakjs.min.js | 12 +- test/cloakjs.test.js | 883 ++++++++++++-- 5 files changed, 2846 insertions(+), 471 deletions(-) diff --git a/.github/workflows/minify.yml b/.github/workflows/minify.yml index e316084..580d29c 100644 --- a/.github/workflows/minify.yml +++ b/.github/workflows/minify.yml @@ -1,4 +1,4 @@ -name: Update minified CloakJS +name: Tests and minified build on: push: @@ -6,6 +6,13 @@ on: - '**' paths: - cloakjs.js + - test/cloakjs.test.js + - .github/workflows/minify.yml + pull_request: + paths: + - cloakjs.js + - test/cloakjs.test.js + workflow_dispatch: permissions: contents: write @@ -18,20 +25,87 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '22' - - name: Minify and verify + + - name: Run the test suite + run: node --test + + - name: Minify run: | - node --test npm exec --yes --package=terser@5.44.0 -- terser cloakjs.js --compress --mangle --comments '/^!/' --output cloakjs.min.js node --check cloakjs.min.js - node -e 'const c = require("./cloakjs.min.js"); const source = require("./cloakjs.js"); if (c.VERSION !== source.VERSION || typeof c.obfuscate !== "function") process.exit(1); const vm = require("node:vm"); const logs = []; vm.runInNewContext(c.obfuscate("console.log(\"ok\")", "high"), { console: { log: x => logs.push(x) }, atob, TextDecoder }); if (logs[0] !== "ok") process.exit(1)' - - name: Commit minified file if changed + + - name: Verify the minified build + run: | + cat > "$RUNNER_TEMP/verify-cloakjs.js" <<'VERIFY' + const assert = require("node:assert/strict"); + const vm = require("node:vm"); + const path = require("node:path"); + const source = require(path.join(process.cwd(), "cloakjs.js")); + const minified = require(path.join(process.cwd(), "cloakjs.min.js")); + + // metadata and API surface + assert.equal(minified.VERSION, source.VERSION); + assert.equal(minified.NAME, source.NAME); + assert.deepEqual(Object.keys(minified.presets), Object.keys(source.presets)); + assert.equal(minified.techniques.length, source.techniques.length); + for (const key of ["obfuscate", "obfuscateWithStats", "measure", "resolveOptions"]) { + assert.equal(typeof minified[key], "function", `${key} is missing`); + } + assert.deepEqual(Object.keys(minified.resolveOptions("extreme")).sort(), Object.keys(source.defaultOptions).sort()); + + function evaluate(code, sandbox) { + const logs = []; + const scope = Object.assign({ console: { log: (value) => logs.push(String(value)) } }, sandbox); + vm.runInNewContext(code, scope, { timeout: 5000 }); + return logs; + } + + const program = [ + 'function label(value) { var suffix = "!"; return value + suffix; }', + 'var list = [1, 2, 3].map(function (v) { return v * 2; });', + 'console.log(label("ok"), "emoji \u{1F680}", list.join("-"), 21 * 2);' + ].join("\n"); + const expected = evaluate(program, { TextDecoder, atob, Uint8Array }); + + for (const preset of ["off", "low", "medium", "high", "extreme"]) { + for (let attempt = 0; attempt < 4; attempt++) { + const result = minified.obfuscateWithStats(program, { seed: `${preset}-${attempt}` }); + assert.equal(result.stats.seed, `${preset}-${attempt}`); + assert.deepEqual(result.warnings, [], `${preset}: ${result.warnings.join("; ")}`); + // with the fast decoder path available + assert.deepEqual(evaluate(result.code, { TextDecoder, atob, Uint8Array }), expected, preset); + // and with no globals at all: the decoder must be self-contained + assert.deepEqual(evaluate(result.code, {}), expected, `${preset} (bare sandbox)`); + } + } + + // the readable and the minified build must generate identical output + const options = { + seed: "determinism", stringEncryption: true, stringSplitting: true, + stringArrayDecoys: 4, controlFlowFlattening: true, identifierEscaping: true + }; + assert.equal(minified.obfuscate(program, options), source.obfuscate(program, options)); + + // self-defending output runs, and rejects a patched generated function + const guarded = minified.obfuscate(program, { seed: 7 }); + assert.deepEqual(evaluate(guarded, {}), expected, "guarded build produces the right output"); + const patched = guarded.replace("String.fromCharCode", "String.fromCharCode2"); + assert.notEqual(patched, guarded, "the decoder did not contain String.fromCharCode, the tamper check is meaningless"); + assert.throws(() => evaluate(patched, {}), /Unexpected token/); + + console.log("minified build verified:", minified.VERSION); + VERIFY + node "$RUNNER_TEMP/verify-cloakjs.js" + + - name: Commit the minified file if it changed + if: github.event_name == 'push' && github.ref_type == 'branch' run: | git add -- cloakjs.min.js if git diff --cached --quiet; then - echo 'Minified file is already up to date.' + echo 'cloakjs.min.js is already up to date.' exit 0 fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git commit -m 'chore: update minified CloakJS' - git push origin "HEAD:${GITHUB_REF}" + git commit -m 'chore: rebuild cloakjs.min.js' + git push origin "HEAD:${{ github.ref_name }}" diff --git a/README.md b/README.md index ebe4f53..43f0c4d 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,213 @@ -# CloakJS 1.2.0 +# CloakJS 1.3.0 [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![GitHub stars](https://img.shields.io/github/stars/Infinitode/CloakJS.svg)](https://github.com/Infinitode/CloakJS/stargazers) [![GitHub issues](https://img.shields.io/github/issues/Infinitode/CloakJS.svg)](https://github.com/Infinitode/CloakJS/issues) [![jsDelivr monthly downloads](https://data.jsdelivr.com/v1/package/gh/Infinitode/CloakJS/badge?style=rounded)](https://www.jsdelivr.com/package/gh/Infinitode/CloakJS) -CloakJS is a single-file, dependency-free JavaScript obfuscator for the browser and Node.js. It can rename some identifiers, encode strings and numbers, and optionally add a dispatcher and decoy statements. It returns JavaScript source code; it does **not** run the code for you. +CloakJS is a single-file, dependency-free JavaScript obfuscator for the browser and Node.js. It renames bindings, hides strings behind a generated decoder, rewrites numeric literals, flattens control flow inside function bodies, injects opaque predicates and unreachable decoy code, escapes identifiers, and can add runtime guards. It returns JavaScript source **as a string** — it never executes the code for you. + +```html + + +``` > [!IMPORTANT] -> Obfuscation makes source less readable, **not secure**. Never put secrets, credentials or security-critical checks in client-side JavaScript. CloakJS uses a small lexer rather than a full JavaScript parser; test transformed output in the environment where it will run. It is intended for classic scripts, not ES modules, JSX or TypeScript. +> **CloakJS is not distributed through npm.** There *is* a package named `cloakjs` on the npm registry, but it is an unrelated AOP/Proxy wrapper library maintained by someone else — it is not this project and it does not contain this obfuscator. Do not install that package expecting CloakJS. Grab [`cloakjs.js`](cloakjs.js) or [`cloakjs.min.js`](cloakjs.min.js) from this repository (or from jsDelivr) instead; the library has no dependencies and no package manifest, so there is nothing to resolve and nothing to build. + +> [!WARNING] +> Obfuscation makes source **harder to read, not secure**. It cannot protect secrets, credentials, licence keys or security-critical checks that ship to the client, and it does not resist an analyst who can instrument the running page. Everything CloakJS does is reversible by hand or by tooling. Treat it as a speed bump and a deterrent — never as a security boundary. + +## What is new in 1.3.0 + +- **`extreme` is a real mode now.** It no longer just repeats `high`: it adds identifier escaping, string splitting, decoy string-array entries, maximum flattening and decoy amounts, and a recurring `debugger` trap. +- **Control-flow flattening reaches inside function bodies**, not just the top level, and can flatten `var`, `if`, `for`, `try`, `switch`, `return` and `throw` statements — not only bare calls. +- **A self-contained decoder.** Obfuscated output no longer requires `atob`, `Uint8Array` or `TextDecoder`; it implements Base64, a custom alphabet, RC4 and UTF-8 decoding itself, using `TextDecoder` only as a fast path when the host provides one. +- **Template literals are first class.** Identifiers inside `` `${…}` `` are analysed and renamed, while static templates are encrypted like ordinary strings. +- **Much broader mangling.** Parameters (including defaults and rest), `catch` bindings, class names, named function expressions and template substitutions are renamed — but only when every occurrence of a spelling provably stays inside a scope that declares it. +- **New options:** `stringSplitting`, `stringArrayDecoys`, `stringArrayShuffle`, `stringAlphabet`, `stringEncoding`, `identifierEscaping`, `preserveFunctionNames`, `controlFlowThreshold`, `deadCodeAmount`, `debugProtection`(+`Interval`), `wrapInIife`, `disableConsoleOutput`, `seed`. +- **Typed, validated options.** Unknown names and wrong types throw with a clear message instead of being silently ignored. +- **Reproducible builds** with `seed`, richer `stats` (including which `techniques` actually ran) and clearer `warnings`. ## Getting started -Download [`cloakjs.js`](cloakjs.js) (readable) or [`cloakjs.min.js`](cloakjs.min.js) (minified), or load either from [jsDelivr](https://www.jsdelivr.com/package/gh/Infinitode/CloakJS) after the files are available on the repository's `main` branch: +Download [`cloakjs.js`](cloakjs.js) (readable) or [`cloakjs.min.js`](cloakjs.min.js) (minified), or load either from [jsDelivr](https://www.jsdelivr.com/package/gh/Infinitode/CloakJS): ```html - + - - + ``` -For reproducible deployments, replace `@main` with a published tag or commit hash (once available). The minified file is generated from `cloakjs.js` with Terser; there is no npm release to install. The badge above displays jsDelivr's live download information rather than a hard-coded count and may not show data until the files have been published on `main`. +For reproducible deployments, replace `@main` with a tag or commit hash from this repository. The minified file is generated from `cloakjs.js` with Terser; there is no npm release. The badge above shows jsDelivr's live download information rather than a hard-coded count. -In Node.js (modern Node with `atob`, `TextEncoder` and `TextDecoder`): +In Node.js (any modern Node, no extra packages): ```js -const CloakJS = require('./cloakjs.js'); -const result = CloakJS.obfuscateWithStats('console.log("hello")', 'high'); -console.log(result.code); -console.log(result.stats, result.warnings); +const CloakJS = require("./cloakjs.js"); + +const result = CloakJS.obfuscateWithStats('const who = "world"; console.log(`hi ${who}`);', "extreme"); +console.log(result.code); // obfuscated source, as a string +console.log(result.stats); // what the pipeline actually applied +console.log(result.warnings); // anything it refused to do — check these + +// reproducible output while you are building a pipeline +const first = CloakJS.obfuscate("var a = 1; console.log(a);", { seed: "build-42" }); ``` -The file is UMD: it also works as an AMD module, or as the global `CloakJS` in a normal `