Back

Security

ShroudJS

A JavaScript obfuscator I built to make client-side code expensive to read, tamper with, and lift. It will not make your code invisible, nothing can, but it turns a five minute copy job into real work.

I built ShroudJS to answer a question I kept circling: how far can client-side obfuscation actually go? Anything you ship to a browser has to run there, which means the machine reading it is the same machine you are trying to keep it from. There is no way around that. So the honest goal is not secrecy, it is cost. ShroudJS raises the price of understanding your code from "copy paste" to "sit down and reverse a stack of encryption layers," and for almost everyone that price is too high to bother.

How it works

It runs in a few passes. Each one removes something an attacker would use to read your code.

1. Pull the strings out

The first thing you read in someone else's code is the strings: URLs, messages, keys, class names. ShroudJS walks the source and moves every string literal it can safely touch into one encoded table at the top, replacing it at the call site with a lookup like d(42). The table is shuffled, so its order tells you nothing, and each entry is encrypted, so you cannot even read the table. This is the part that makes the recovered logic hard to follow, not just the file on disk. It is careful about where it does this: object keys, directives like "use strict", and template literals are left exactly as they are, because getting those wrong would change what the code does.

2. Strip

Then it removes everything that helps a human read what is left: comments, dead whitespace, and formatting. This runs through a hand-written lexer that understands strings, template literals with nested interpolation, regular expressions, and comments, so it never mangles a / that is really a divide or a regex. That distinction is the classic trap in JavaScript tokenizing, and getting it right is what makes the pass safe on real code.

3. Pack and encrypt

Now the whole thing is treated as raw bytes and pushed through a keyed stream cipher. Each byte is XORed with a random key and chained against the previous ciphertext byte, so a run of identical characters does not produce a run of identical output. The key is generated fresh on every run, so two builds of the same file share no static signature.

4. Self-decoding bootstrap

The encrypted payload ships inside a small bootstrap that reverses everything at load time and runs the result. The decoder, the key, and the data are split into an array that is rotated out of order and only put back at runtime, and every internal name is randomized garbage. There are no external calls and no library, not even atob. The base64 decoder, the UTF-8 decoder, and the cipher are all inlined as plain arithmetic, which is what lets the same output run in a browser or in Node with zero dependencies.

5. Tumble

Then it repeats. Each round wraps the previous output in a fresh bootstrap with a new key and a new layout, so the first thing an attacker recovers is not your code, it is another encrypted packer. Two rounds is the default, more is a flag. Every layer they peel only reveals the next one.

How strong it is

Against anyone who just wants to read it, it holds up well. A beautifier, a search engine, a scraper, or someone poking around in devtools sees encrypted noise, a decoder full of nonsense names, and no strings to grep for. The comments and the original formatting are gone permanently, stripping is one-way, so no decompiler will ever hand back the readable source with your notes in it, because that information no longer exists in the file. For casual copying, license removal, and quick tampering, which is the realistic threat to shipped JavaScript, that is enough to make people give up.

The limits

The browser has to run your real code in the end, and that is the hard limit for any client-side obfuscator. Someone who runs the page and hooks the final eval can grab the decoded output. What they get has the strings tabled and the formatting gone, but the control flow is still theirs to read.

The next step up would be control-flow flattening, turning your logic into a dispatch loop so even the shape of the code is gone. I left it out on purpose. Doing it safely needs a real parser, and I wanted ShroudJS to stay zero-dependency and never risk breaking your code. If you want to push it further, the source is small and MIT licensed, so bolting a parser-based pass on top is a fair weekend project. For anything that genuinely has to stay secret, keep it on a server behind an API, not in the bundle.

Performance

The cost is paid once, at load. Decoding is a single linear pass over the payload per round, a few milliseconds for a normal file, and then the browser compiles the recovered code once and runs it at full native speed. There is no per-call or per-frame penalty, so it stays practical even for large, feature-heavy apps. I tested it on a generated file with thousands of statements and it obfuscated in a fraction of a second and ran identically. The tradeoff is file size: the string table plus a bootstrap per round makes the output a few times larger than the input, which is the price of the layering.

What it runs on

Classic browser scripts, Node CommonJS, and bundled output all work, because the recovered code is executed as a normal script. Raw ES modules do not, since import and export cannot run through eval. The right move there is to bundle your modules down to a single classic script first, with esbuild, Rollup, or webpack, and then run ShroudJS on the bundle. It works on any valid JavaScript, not just a hand-picked subset, because the core treats your code as bytes and never has to understand it.

Install

It is on npm, so one command and you are done. No build step, still zero dependencies.

shell
npm install shroudjs

Pin the version if you want a build you can reproduce exactly: npm install shroudjs@1.0.0. Or install it globally with npm install -g shroudjs for a plain shroudjs command anywhere.

Usage

Point it at a folder of files and a folder to write to. It walks the first, obfuscates every .js, and writes them to the second with the same names and paths.

shell
npx shroudjs src dist              # src/ into dist/
npx shroudjs app.js -o app.out.js  # one file
npx shroudjs src dist --rounds 3   # more layers

Run it with no folders and it uses ./input and ./output by default. Sub-folders come out the other side with the same paths, and anything that is not JavaScript is left alone. The source and a demo are on GitHub.

github.com/chrisch88dev/shroudjs