ssukka 0.2.0

HTML obfuscation library and CLI for Rust. Renders identically in browsers but is hard for humans to read.
Documentation

ssukka

쓰까(ssukka) is Busan dialect (부산 사투리) for "mix it up" (섞어).

HTML obfuscation library and CLI for Rust. Renders identically in browsers but is hard for humans to read.

Features

Cosmetic (on by default)

  • Class/ID renaming - consistent across HTML, CSS, and JavaScript (including dynamic construction patterns)
  • HTML entity encoding - text and attribute values encoded as decimal/hex/named entities
  • Tag case randomization - <div> becomes <DiV>
  • Attribute shuffling - randomized attribute order
  • CSS minification - via lightningcss
  • CSS selector unicode escaping - .foo becomes .\66\6f\6f
  • JS string encoding - string literals encoded with a randomized mix of \xHH / \uXXXX / \u{..} (strict-mode-safe)
  • JS minification - comment removal and whitespace compression
  • Comment removal and whitespace collapsing
  • Deterministic output - seed-based RNG for reproducible results

Advanced (opt-in)

These change the DOM, output size, runtime cost, or accessibility, so they are off by default:

  • Honeypots / decoys (--honeypots N) - inject invisible trap links, fake form fields, and bogus data blocks to waste scraper effort and poison harvested data. Hidden from layout and assistive tech.
  • Structural obfuscation (--structural) - move visible text out of the static markup into base64 data- attributes, restored client-side by an injected script. Resists static scrapers (curl / readability extractors that don't run JS) while rendering identically. Warning: breaks no-JS, SEO, and degrades accessibility.
  • AST JS engine (--js-ast, powered by oxc):
    • Identifier mangling (--mangle) - scope-aware renaming of local JS bindings (never globals, so cross-script / inline-handler references stay intact).
    • String array (--js-string-encoding array) - hoist string literals into a base64 array decoded at runtime.
    • Dead code injection (--dead-code) - opaque-predicate-guarded junk that never executes.
    • Control-flow flattening (--cff) - reshape sequential logic into a shuffled switch dispatcher.
  • Polymorphic mode (--polymorphic) - vary which transforms run (and how) on every invocation, so identical input yields structurally different output each time (signature/cache evasion).
  • WASM build - runs in the browser / Cloudflare Workers / Deno via the wasm feature.

Every AST transform re-parses its own output and is discarded if it would emit invalid JavaScript; on a parse failure the engine falls back to the token-based path. The transforms are semantics-preserving by construction (verified by executing obfuscated output under Node).

Threat model

ssukka raises the cost of reading and scraping a page; it is not a security boundary. Anything the browser can render, a determined adversary with a headless browser can recover. Use it to deter casual copying and cheap bulk scraping, not to protect secrets.

Modern LLM-based deobfuscators can reverse simple identifier renaming and string encoding, so the strongest configurations layer transforms (string array + mangling + structural + honeypots) and lean on structural/visual approaches rather than renaming alone. Benchmark the presets with cargo bench.

Offline by design

ssukka performs no network I/O at runtime - verified with strace (zero socket/connect syscalls). All dependencies (lol_html, lightningcss, oxc, rand) are pure Rust and compile from source. External resource inlining (--inline-local-resources) reads local files only and never fetches over the network.

Architecture

Streaming pipeline built on lol_html:

Input HTML
  -> Pass 0 (optional): inline local CSS/JS
  -> Pass 1: analyze -> SymbolMap
  -> Pass 2: transform HTML / CSS / JS
  -> Output HTML

Installation

cargo install ssukka

Or build from source:

git clone https://github.com/miniex/ssukka.git
cd ssukka
cargo build --release

CLI Usage

# Basic
ssukka -i input.html -o output.html

# stdin/stdout
cat input.html | ssukka > output.html

# With options
ssukka -i input.html -o output.html --seed 42 --no-rename --no-minify-css

Options

Flag Description
-i, --input <FILE> Input HTML file (default: stdin)
-o, --output <FILE> Output file (default: stdout)
--seed <N> Seed for deterministic output
--no-rename Disable class/ID renaming
--no-minify-css Disable CSS minification
--no-minify-js Disable JS minification
--no-encode-entities Disable entity encoding
--no-shuffle-attrs Disable attribute shuffling
--no-randomize-case Disable tag case randomization
--js-string-encoding <none|escapes|array> JS string strategy (default: escapes)
--honeypots <N> Inject N invisible decoy nodes (scraper traps)
--structural Move text into encoded attrs, restore client-side
--polymorphic Randomize transforms per run (ignored with --seed)
--js-ast Use the oxc AST engine for <script> JS
--mangle Scope-aware local identifier renaming (implies --js-ast)
--cff Control-flow flattening (implies --js-ast)
--dead-code Opaque-predicate dead code injection (implies --js-ast)
--dead-code-threshold <0..1> Fraction of sites that receive dead code
--inline-local-resources Inline local <link>/<script src> (offline only)
--base-dir <DIR> Base directory for resolving local resources
# Maximum: layered obfuscation for the strongest output
ssukka -i input.html -o output.html \
    --honeypots 8 --structural --mangle --cff --dead-code \
    --js-string-encoding array

Library Usage

// Simple
let result = ssukka::obfuscate(html)?;

// With configuration
let result = ssukka::Obfuscator::builder()
    .seed(42)
    .rename_classes(true)
    .rename_ids(true)
    .encode_text_entities(true)
    .minify_css(true)
    .build()
    .obfuscate(html)?;

// Advanced, layered
use ssukka::config::JsStringEncoding;
let result = ssukka::Obfuscator::builder()
    .inject_honeypots(true)
    .honeypot_count(8)
    .structural_obfuscation(true)
    .js_ast(true)
    .mangle_identifiers(true)
    .js_string_encoding(JsStringEncoding::Array)
    .dead_code_injection(true)
    .control_flow_flattening(true)
    .build()
    .obfuscate(html)?;

WASM

cargo build --release --target wasm32-unknown-unknown --features wasm
# or: wasm-pack build --features wasm

Exposes obfuscate(html), obfuscate_seeded(html, seed), and obfuscate_max(html, honeypots, seed).

Limitations

  • The default JS path (no --js-ast) is token-based: string encoding + minification only. Enable --js-ast for AST-grade mangling / string arrays / dead code / control-flow flattening.
  • Dynamic class/ID construction in JS is handled via prefix-detection heuristics on the token path; highly dynamic patterns may not be caught.
  • AST control-flow flattening is conservative: it only flattens top-level sequences of simple expression statements (anything with declarations or control flow is left as-is to guarantee correctness).
  • External stylesheets/scripts are only processed with --inline-local-resources and only from the local filesystem.
  • Obfuscation is a deterrent, not security - see Threat model.

Development

Requires Rust >= 1.94 (pinned in rust-toolchain.toml). A Dockerfile and a Nix flake (nix develop / nix build) are also provided. The repo ships POSIX sh tooling:

./tools/format.sh   # cargo fmt + shfmt + taplo fmt + prettier (md)
./tools/lint.sh     # cargo clippy -D warnings + shellcheck + shfmt -d + taplo

License

MIT