@dacely/toildefender 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,380 @@
1
+ <div align="center">
2
+
3
+ <img src="./images/toildefender2.svg" alt="ToilDefender" width="600" />
4
+
5
+
6
+ ### JavaScript code protection for the Toil stack.
7
+
8
+ <sub>Randomized control flow, literal protection, object packing, BigInt-backed VM bytecode, and hash-mesh bytecode unlock for browser and Node bundles.</sub>
9
+
10
+ <br/>
11
+
12
+ [![npm](https://img.shields.io/npm/v/@dacely/toildefender.svg?color=2563ff&label=npm&labelColor=0e1520)](https://www.npmjs.com/package/@dacely/toildefender)
13
+ [![node](https://img.shields.io/badge/node-%3E%3D24-22e3ab.svg?labelColor=0e1520)](https://nodejs.org/)
14
+ [![vm](https://img.shields.io/badge/VM-BigInt_bytecode-7c3aed.svg?labelColor=0e1520)](#virtual-machine-protection)
15
+ [![license](https://img.shields.io/badge/license-AGPL--3.0-8b9ab4.svg?labelColor=0e1520)](./LICENSE)
16
+
17
+ </div>
18
+
19
+ ---
20
+
21
+ ToilDefender is Dacely's maintained JavaScript protection layer for the Toil
22
+ technology stack. It started from the original `defendjs` project, but this
23
+ fork is now maintained as its own package: `@dacely/toildefender`.
24
+
25
+ The goal is not to make client-side JavaScript impossible to analyze. That is
26
+ not a real promise. The goal is to raise reverse-engineering cost by removing
27
+ source-level structure, splitting logic across generated helpers, packing
28
+ constants, and optionally compiling selected functions into randomized numeric
29
+ VM programs.
30
+
31
+ ```bash
32
+ npm install @dacely/toildefender
33
+ ```
34
+
35
+ ```js
36
+ const toildefender = require("@dacely/toildefender");
37
+
38
+ const result = toildefender.do({
39
+ code: source,
40
+ modulesCode: {},
41
+ logLevel: "error",
42
+ features: {
43
+ dead_code: true,
44
+ scope: true,
45
+ control_flow: true,
46
+ identifiers: true,
47
+ numeric_vm: true,
48
+ object_packing: true,
49
+ literals: true,
50
+ mangle: true,
51
+ compress: true
52
+ },
53
+ protections: {
54
+ virtualMachine: {
55
+ enabled: true,
56
+ mode: "aggressive",
57
+ bigintBytecode: true,
58
+ randomizedOpcodes: true,
59
+ encodeConstants: true,
60
+ perFunctionDialect: true,
61
+ virtualize: "heuristic"
62
+ },
63
+ hashMesh: {
64
+ enabled: true,
65
+ mode: "aggressive",
66
+ unlock: "per-function",
67
+ deriveDialectFromMesh: true,
68
+ bindToVmState: true,
69
+ encodeChaff: true,
70
+ chaffRatio: 0.55
71
+ }
72
+ }
73
+ });
74
+
75
+ console.log(result.code);
76
+ ```
77
+
78
+ ## What It Does
79
+
80
+ | Protection | Purpose |
81
+ | --- | --- |
82
+ | `control_flow` | Rewrites structured control flow into dispatcher-style execution. |
83
+ | `scope` | Flattens function/scope structure into generated runtime frames. |
84
+ | `identifiers` | Renames and rewrites identifiers, object references, and property access. |
85
+ | `object_packing` | Packs object literal keys into numeric schemas instead of readable key/value arrays. |
86
+ | `literals` | Encodes strings and numeric constants. |
87
+ | `dead_code` | Inserts unreachable or low-value code paths to add noise. |
88
+ | `mangle` | Shortens generated identifiers. |
89
+ | `compress` | Emits compact output. |
90
+ | `numeric_vm` | Virtualizes supported functions into BigInt-packed bytecode. |
91
+
92
+ ## Virtual Machine Protection
93
+
94
+ Transform your JavaScript into randomized virtual-machine bytecode for maximum
95
+ resistance against reverse engineering.
96
+
97
+ ToilDefender compiles protected functions into a private instruction set, packs
98
+ the bytecode into encrypted BigInt streams, and executes it through a generated
99
+ runtime VM. Instead of exposing readable JavaScript logic, your code becomes
100
+ numeric program data consumed by a randomized virtual machine.
101
+
102
+ Original logic disappears from the output bundle. Attackers no longer reverse
103
+ plain JavaScript; they must recover the VM, decode the bytecode format,
104
+ reconstruct the instruction set, and emulate the protected program.
105
+
106
+ ```js
107
+ toildefender.do({
108
+ code,
109
+ modulesCode: {},
110
+ features: {
111
+ numeric_vm: true
112
+ },
113
+ protections: {
114
+ virtualMachine: {
115
+ enabled: true,
116
+ mode: "aggressive",
117
+ bigintBytecode: true,
118
+ randomizedOpcodes: true,
119
+ encodeConstants: true,
120
+ perFunctionDialect: true,
121
+ virtualize: "marked",
122
+ minFunctionSize: 1,
123
+ maxFunctionSize: 120,
124
+ seed: "build-seed"
125
+ }
126
+ }
127
+ });
128
+ ```
129
+
130
+ Selection modes:
131
+
132
+ | `virtualize` | Meaning |
133
+ | --- | --- |
134
+ | `marked` | Virtualize functions marked by supported annotations or compiler selection. |
135
+ | `all-supported` | Virtualize every function that fits the supported syntax subset. |
136
+ | `heuristic` | Virtualize functions selected by size and compiler suitability. |
137
+
138
+ Supported VM syntax currently targets practical protection work: literals,
139
+ locals, arguments, return, assignment, arithmetic, comparisons, logical
140
+ expressions, `if` / `else`, `while`, calls, member reads, arrays, and object
141
+ literals. Unsupported syntax remains native or is skipped by selection.
142
+
143
+ ### All-Modes Output Demo
144
+
145
+ Input:
146
+
147
+ ```js
148
+ function licenseGate(input) {
149
+ const total = input.length * 7;
150
+ return input.charCodeAt(0) === 86
151
+ ? { ok: true, total: total + 13 }
152
+ : { ok: false, total: total - 5 };
153
+ }
154
+
155
+ globalThis.__result = licenseGate("Veilmark");
156
+ ```
157
+
158
+ The demo artifact is generated with every major protection enabled:
159
+
160
+ ```js
161
+ features: {
162
+ dead_code: true,
163
+ scope: true,
164
+ control_flow: true,
165
+ identifiers: true,
166
+ numeric_vm: true,
167
+ object_packing: true,
168
+ literals: true,
169
+ mangle: true,
170
+ compress: true
171
+ },
172
+ protections: {
173
+ virtualMachine: {
174
+ enabled: true,
175
+ mode: "aggressive",
176
+ bigintBytecode: true,
177
+ randomizedOpcodes: true,
178
+ encodeConstants: true,
179
+ perFunctionDialect: true,
180
+ virtualize: "all-supported",
181
+ seed: "readme-all-modes-demo"
182
+ },
183
+ hashMesh: {
184
+ enabled: true,
185
+ mode: "aggressive",
186
+ unlock: "per-function",
187
+ deriveDialectFromMesh: true,
188
+ bindToVmState: true,
189
+ encodeChaff: true,
190
+ chaffRatio: 0.55
191
+ }
192
+ }
193
+ ```
194
+
195
+ The complete beautified generated output is committed at
196
+ [docs/all-modes-output.demo.js](./docs/all-modes-output.demo.js). It is a real
197
+ 673-line artifact from the current generator and executes to:
198
+
199
+ ```json
200
+ { "ok": true, "total": 69 }
201
+ ```
202
+
203
+ That output contains the full stacked mess: flattened dispatcher runtime,
204
+ identifier rewriting, packed literals, object packing, VM bytecode execution,
205
+ BigInt program blobs, randomized opcode tables, and Hash-Mesh unlock material.
206
+
207
+ ## Hash-Mesh Unlock
208
+
209
+ Hash-Mesh Unlock derives VM bytecode keys from runtime integrity data. If
210
+ protected code, constants, VM helpers, or execution state are modified, the next
211
+ bytecode chunk decrypts incorrectly instead of exposing runnable logic.
212
+
213
+ This turns integrity checks into decryption requirements instead of patchable
214
+ boolean branches.
215
+
216
+ ```js
217
+ toildefender.do({
218
+ code,
219
+ modulesCode: {},
220
+ features: {
221
+ numeric_vm: true
222
+ },
223
+ protections: {
224
+ virtualMachine: {
225
+ enabled: true,
226
+ mode: "aggressive",
227
+ virtualize: "all-supported"
228
+ },
229
+ hashMesh: {
230
+ enabled: true,
231
+ mode: "aggressive",
232
+ unlock: "per-function",
233
+ deriveDialectFromMesh: true,
234
+ bindToVmState: true,
235
+ encodeChaff: true,
236
+ chaffRatio: 0.55,
237
+ serverBound: false
238
+ }
239
+ }
240
+ });
241
+ ```
242
+
243
+ Hash-Mesh is an obfuscation and tamper-resistance layer. It is not a
244
+ cryptographic secrecy guarantee for code running on an attacker-controlled
245
+ machine.
246
+
247
+ ## CLI
248
+
249
+ Install globally or run through `npx`:
250
+
251
+ ```bash
252
+ npm install -g @dacely/toildefender
253
+ toildefender --help
254
+ ```
255
+
256
+ ```bash
257
+ toildefender \
258
+ --input ./src \
259
+ --output ./dist-protected \
260
+ --features scope,control_flow,identifiers,literals,mangle,compress
261
+ ```
262
+
263
+ For multi-entry projects, declare entry files in `package.json`:
264
+
265
+ ```json
266
+ {
267
+ "toildefender": {
268
+ "mainFiles": ["index.js", "worker.js"]
269
+ }
270
+ }
271
+ ```
272
+
273
+ The old `defendjs.mainFiles` field is still read as a compatibility fallback,
274
+ but new projects should use `toildefender`.
275
+
276
+ ## API
277
+
278
+ ```js
279
+ const toildefender = require("@dacely/toildefender");
280
+
281
+ const result = toildefender.do({
282
+ code: "function add(a,b){ return a + b } globalThis.x = add(1,2)",
283
+ modulesCode: {},
284
+ logLevel: "warn",
285
+ features: {
286
+ dead_code: false,
287
+ scope: true,
288
+ control_flow: true,
289
+ identifiers: true,
290
+ numeric_vm: false,
291
+ object_packing: true,
292
+ literals: true,
293
+ mangle: true,
294
+ compress: true
295
+ }
296
+ });
297
+
298
+ console.log(result.code);
299
+ ```
300
+
301
+ Main options:
302
+
303
+ | Option | Meaning |
304
+ | --- | --- |
305
+ | `code` | Entry source code. |
306
+ | `modulesCode` | Map of dependency filename to source code. |
307
+ | `features` | Feature switches for the classic pipeline. |
308
+ | `protections.virtualMachine` | User-facing VM bytecode backend configuration. |
309
+ | `protections.hashMesh` | User-facing hash-mesh unlock configuration. |
310
+ | `numericVm` | Lower-level numeric VM configuration retained for internal callers. |
311
+ | `preprocessorVariables` | Compile-time preprocessor constants. |
312
+ | `logLevel` | `error`, `warn`, `info`, `debug`, or `log`. |
313
+
314
+ ## Toil Integration
315
+
316
+ ToilDefender is intended to sit behind Toil build tooling. Framework packages
317
+ can call the API directly, then run normal syntax validation and browser tests
318
+ against the protected artifact.
319
+
320
+ Recommended Toil stack pattern:
321
+
322
+ ```txt
323
+ source bundle
324
+ -> Vite / framework build
325
+ -> ToilDefender pre-obfuscation and protection
326
+ -> syntax validation
327
+ -> browser smoke tests
328
+ -> publish/deploy artifact
329
+ ```
330
+
331
+ For security-sensitive browser code, pair this with server-side validation.
332
+ Client-side protection raises cost; it does not replace server authority.
333
+
334
+ ## Security Boundary
335
+
336
+ ToilDefender is code protection, not magic.
337
+
338
+ It helps against:
339
+
340
+ - quick static reading of shipped JavaScript
341
+ - simple string/signature extraction
342
+ - source-level control-flow recovery
343
+ - direct patching of obvious boolean integrity checks
344
+ - automated diffing across builds when seeds and dialects rotate
345
+
346
+ It does not guarantee:
347
+
348
+ - secrets stay secret in client-side code
349
+ - runtime tracing is impossible
350
+ - browser-controlled attackers cannot eventually emulate behavior
351
+ - server authorization can be moved into the browser
352
+
353
+ Put real authorization and durable decisions on the server.
354
+
355
+ ## Development
356
+
357
+ ```bash
358
+ npm test
359
+ npm run test:firefox
360
+ npm run pack:dry
361
+ ```
362
+
363
+ The regression suite covers modern syntax handling, object packing, VM bytecode
364
+ execution, Hash-Mesh unlock, and tamper failure behavior.
365
+
366
+ ## Credit
367
+
368
+ ToilDefender began as a fork of
369
+ [defendjs](https://github.com/alexhorn/defendjs), originally created by
370
+ Alexander Horn and released under the GNU Affero General Public License v3.0.
371
+
372
+ Dacely maintains this fork for the Toil stack and has added the modern parser
373
+ surface, VM bytecode backend, Hash-Mesh unlock layer, object key packing,
374
+ branding cleanup, and current regression coverage.
375
+
376
+ See [NOTICE.md](./NOTICE.md) for attribution details.
377
+
378
+ ## License
379
+
380
+ AGPL-3.0. See [LICENSE](./LICENSE).
package/cli.js ADDED
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+
3
+ var fs = require("fs");
4
+ var os = require("os");
5
+ var path = require("path");
6
+
7
+ var _ = require("lodash");
8
+
9
+ var toildefender = require("./obfuscator");
10
+
11
+ exports.run = function () {
12
+
13
+ var argv = require("minimist")(process.argv.slice(2));
14
+ if (argv.help) {
15
+ console.info(
16
+ "# Usage\n" +
17
+ "\n" +
18
+ "toildefender --input [directory] --output [directory] --features [features] --preprocessor [variable]\n" +
19
+ "\n" +
20
+ "# Parameters\n" +
21
+ "\n" +
22
+ "--input\n" +
23
+ "\tPath to input directory or file. Can be repeated multiple times.\n" +
24
+ "\n" +
25
+ "--output\n" +
26
+ "\tPath to output directory.\n" +
27
+ "\n" +
28
+ "--features\n" +
29
+ "\tComma-separated list of features. (available features: " + _.join(_.keys(toildefender.features), ", ") + ")\n" +
30
+ "\te.g. --features scope,control_flow,compress\n" +
31
+ "\n" +
32
+ "--preprocessor\n" +
33
+ "\tPreprocessor variable declaration or assignment.\n" +
34
+ (() => { switch (os.platform()) {
35
+ case "win32":
36
+ return "\te.g. --preprocessor PLATFORM_WINDOWS --preprocessor PLATFORM_WINDOWS_VERSION=10\n";
37
+ case "darwin":
38
+ return "\te.g. --preprocessor PLATFORM_MACOS --preprocessor PLATFORM_MACOS_VERSION=10.12\n";
39
+ default:
40
+ return "\te.g. --preprocessor PLATFORM_LINUX --preprocessor PLATFORM_LINUX_VERSION=4.8\n";
41
+ } })() +
42
+ "\n" +
43
+ "# Example\n" +
44
+ "\n" +
45
+ (() => { switch (os.platform()) { // bit of a pointless feature, but its neat
46
+ case "win32":
47
+ return "toildefender --input \"D:\\project\\src\" --output \"D:\\project\\dist\" --features scope,control_flow,compress --preprocessor PLATFORM_WINDOWS\n";
48
+ case "darwin":
49
+ return "toildefender --input \"~/project/src\" --output \"~/project/dist\" --features scope,control_flow,compress --preprocessor PLATFORM_MACOS\n";
50
+ default:
51
+ return "toildefender --input \"~/project/src\" --output \"~/project/dist\" --features scope,control_flow,compress --preprocessor PLATFORM_LINUX\n";
52
+ } })() +
53
+ "\n"
54
+ );
55
+ process.exit(0);
56
+ }
57
+ if (!Array.isArray(argv.input)) {
58
+ argv.input = [ argv.input ];
59
+ }
60
+ if (!Array.isArray(argv.preprocessor)) {
61
+ argv.preprocessor = [ argv.preprocessor ];
62
+ }
63
+ if (!argv.input) {
64
+ console.error(
65
+ "Missing --input"
66
+ );
67
+ process.exit(0);
68
+ }
69
+ if (!argv.output) {
70
+ console.error(
71
+ "Missing --output"
72
+ );
73
+ process.exit(0);
74
+ }
75
+
76
+ let files = [];
77
+ argv.input.forEach(item => {
78
+ let stat = fs.lstatSync(item);
79
+ if (stat.isDirectory()) {
80
+ readdirRecursiveSync(item)
81
+ .filter(
82
+ file => !/(^|[\/\\])(\.git|node_modules)($|[\/\\])/.test(file)
83
+ )
84
+ .forEach(
85
+ file => files[file] = fs.readFileSync(path.join(item, file), "utf8")
86
+ );
87
+ } else if (stat.isFile()) {
88
+ files[item] = fs.readFileSync(item, "utf8");
89
+ }
90
+ });
91
+
92
+ let mainFiles = getMainFiles(files);
93
+
94
+ let features = _.mapValues(toildefender.features, (value, key) => _.includes(argv.features, key));
95
+
96
+ let preprocessorVariables = _.fromPairs(_.map(argv.preprocessor, decl => {
97
+ let [, variable, value] = /^\s*([\w\d]+)\s*(?:=\s*([\w\d]+))?\s*$/.exec(decl) || [];
98
+ return [variable, value || null];
99
+ }));
100
+
101
+ let results = _.fromPairs(_.map(mainFiles, key => {
102
+ console.info(`Obfuscating ${key} ...`);
103
+ return [key, toildefender.do({
104
+ code: files[key],
105
+ modulesCode: _.pickBy(files, (value, _key) => key != _key && isCodeFile(_key) && !mainFiles[_key]),
106
+ features: features,
107
+ preprocessorVariables: preprocessorVariables
108
+ })];
109
+ }));
110
+
111
+ _.each(results, (result, key) => {
112
+ let target = path.join(argv.output, key);
113
+ if (!pathExists(path.dirname(target))) {
114
+ fs.mkdirSync(path.dirname(target));
115
+ }
116
+ if (pathExists(target)) {
117
+ fs.unlinkSync(target);
118
+ }
119
+ fs.writeFileSync(target, result.code);
120
+ });
121
+
122
+ function readdirRecursiveSync(dir) {
123
+ let results = [];
124
+ let files = fs.readdirSync(dir);
125
+ files.forEach(function(file) {
126
+ let stat = fs.statSync(dir + "/" + file);
127
+ if (stat.isDirectory()) {
128
+ readdirRecursiveSync(dir + "/" + file).forEach(subfile => results.push(file + "/" + subfile));
129
+ } else {
130
+ results.push(file);
131
+ }
132
+ });
133
+ return results;
134
+ }
135
+
136
+ function isSourceFile(name) {
137
+ return _.includes([ ".js", ".json" ], path.extname(name));
138
+ }
139
+
140
+ function isCodeFile(name) {
141
+ return path.extname(name) == ".js";
142
+ }
143
+
144
+ function pathExists(path) {
145
+ try {
146
+ let stat = fs.lstatSync(path);
147
+ return stat.isFile() || stat.isDirectory();
148
+ } catch (e) {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ function getMainFiles(files) {
154
+ let _package = files["package.json"] && JSON.parse(files["package.json"]);
155
+ if (_package && _package.toildefender && _package.toildefender.mainFiles) {
156
+ return _package.toildefender.mainFiles;
157
+ } else if (_package && _package.defendjs && _package.defendjs.mainFiles) {
158
+ return _package.defendjs.mainFiles;
159
+ } else if (_package && _package.main) {
160
+ return [ _package.main ];
161
+ } else if (Object.keys(files).filter(isSourceFile).length == 1) {
162
+ return [ Object.keys(files).filter(isSourceFile)[0] ];
163
+ } else {
164
+ return [ "app.js", "main.js", "index.js" ].filter(x => files[x] != null).slice(0, 1);
165
+ }
166
+ }
167
+
168
+ };
package/defendjs.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ if (require.main === module) {
4
+ require("./cli").run();
5
+ } else {
6
+ module.exports = require("./obfuscator");
7
+ }