DEV Community

Cover image for Protecting an Electron app's JavaScript: what actually works
VEILVM
VEILVM

Posted on

Protecting an Electron app's JavaScript: what actually works

If you sell or ship an Electron app, your JavaScript is on the customer's computer. That is not a failure of Electron. It is how the runtime works. The useful question is not "how do I hide the code completely" — you cannot — but "what actually raises the cost of copying or patching it, and what only looks like protection?"

This guide is for people protecting their own commercial apps.

asar is a packing format, not a lock

The most common misconception is that putting your files in app.asar encrypts them. It does not. An asar archive is a concatenation of files with an index at the front, similar in spirit to a tarball. Anyone can unpack it in one command:

npx @electron/asar extract /path/to/YourApp.app/Contents/Resources/app.asar ./unpacked
# Windows: look under resources/app.asar inside the install directory
Enter fullscreen mode Exit fullscreen mode

After that, your main.js, renderer bundles and any leftover src/ files are ordinary files on disk. If you have never unpacked your own release, do it now. You will often find source maps, TypeScript sources, .env samples or a readable license.js.

asar still has a purpose: it stops casual browsing of the install folder and lets Electron read files efficiently. Treat it as packaging, the way you treat a zip file.

Theater that does not hold up

A few other steps get advertised as "protection" and do not do what people hope.

Renaming app.asar or shipping app.asar.unpacked only. Unpacked directories are even easier to read. A renamed archive is still an asar.

Disabling DevTools and calling it done. You should disable DevTools in production and set Electron fuses so users cannot re-enable inspection with command-line flags. That blocks casual poking. It does not stop someone from unpacking the asar, editing a file and repacking it.

Fuses alone. Fuses such as RunAsNode, EnableNodeCliInspectArguments and EnableNodeOptionsEnvironmentVariable are worth turning off in production. They close real holes. They do not encrypt your source.

Minification of the whole app. A formatter restores layout in seconds. Minify anyway — it removes comments and shrinks the download — but do not count it as protection.

Start by shipping less

The cheapest win is not putting files in the asar at all.

  • Bundle the main process and each renderer with esbuild, webpack or Vite. Ship a handful of files, not a src/ tree.
  • Set files in package.json or an explicit electron-builder allowlist so tests, fixtures and docs stay out.
  • Generate source maps for your crash reporter, upload them privately, and keep *.map out of the release.
  • Move anything that does not need to run offline — billing, license issuance, account status — to your server.
# electron-builder.yml (excerpt)
files:
  - dist/**/*
  - package.json
asar: true
asarUnpack:
  - "**/*.node"
extraMetadata:
  main: dist/main.js
Enter fullscreen mode Exit fullscreen mode

Then confirm the result:

npx electron-builder --dir
npx @electron/asar extract dist/win-unpacked/resources/app.asar /tmp/asar-audit
find /tmp/asar-audit \( -name "*.map" -o -name "*.ts" -o -name ".env*" \)
Enter fullscreen mode Exit fullscreen mode

If that audit prints anything you would not post on a public gist, fix the pack list before you think about obfuscation.

Main process and renderer are different problems

The main process is Node.js. It can read the filesystem, spawn processes and hold license state. The renderer is a browser. If you enable nodeIntegration or disable contextIsolation, a compromised or inspected renderer can reach Node APIs. For a production app:

  • Keep contextIsolation: true and nodeIntegration: false.
  • Expose a narrow API through contextBridge.
  • Put license checks and privileged work in the main process or a utility process, not in renderer code a user can edit with DevTools overrides.
// preload.js — keep this thin
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("appApi", {
  getEntitlements: () => ipcRenderer.invoke("ents:get"),
});
Enter fullscreen mode Exit fullscreen mode

The renderer then asks for entitlements instead of deciding them. The decision still lives on the user's machine if the app is offline-first, but it is no longer a one-line edit in a renderer bundle.

Harden the code that has to stay client-side

After you have shipped less and locked the Chromium settings, some logic still has to run locally: a license verifier that works offline, a proprietary transform, a rules engine, a file-format parser. That is the part worth protecting.

Plain-JavaScript obfuscation (identifier renaming, string encoding, control-flow flattening) makes casual search-and-patch harder. Public undo scripts exist for many of those transforms, so treat it as a first layer.

VM-based protection goes further. It compiles selected functions into a custom bytecode and ships a small interpreter that runs it. The original if (license.valid) shape is no longer sitting in the file. An analyst has to understand the interpreter first. That raises cost and time. It is not unbreakable, and interpreted code runs slower than native JavaScript, so you apply it to a short list of functions, not to your UI or to node_modules.

A practical split looks like this:

  • UI, window management, logging: minify only
  • General business code: standard obfuscation
  • License verification, core algorithm: VM protection, with an integrity check inside the protected code
  • Signing keys, billing secrets: never in the app

If you want to try VM protection on a real module, VEILVM is a browser-based JSVMP workspace. You can run a file through a high-performance or dynamic VM engine without installing a toolchain. The point is selective protection of the modules you just identified, not wrapping the entire renderer.

Put those steps in CI so a release build cannot forget them. A plan with a CI API is useful here; a manual "obfuscate then paste" step gets skipped under deadline pressure.

Integrity and updates

electron-builder can embed asar integrity checks so a swapped archive fails to load on some platforms. Combine that with signed installers (Windows Authenticode, macOS notarization). Integrity does not hide code, but it stops the "edit one file and zip it back up" path that most copiers try first.

Your updater should also refuse unsigned or mismatched payloads. Otherwise an attacker ships a patched asar as a "mod" or a fake update.

What this will not do

Anyone who controls the machine can attach a debugger, dump memory or hook Electron's JavaScript bindings. Client-side protection buys time and filters out casual copying. It does not replace server-side enforcement for purchases, seats or online features, and it will not make a secret key in the renderer private.

Do not put third-party API secrets, webhook signing keys or admin flags in the asar, protected or not.

A release checklist

  1. Unpack your own asar and look at every file.
  2. Remove source maps, sources and secrets from the pack list.
  3. Turn on context isolation, turn off nodeIntegration, disable production DevTools and lock the relevant fuses.
  4. Sign the installer and enable asar integrity where your builder supports it.
  5. Protect only the high-value modules, and automate that in the release pipeline.
  6. Keep a private, unprotected build for your own debugging.

Try it on a license module

VEILVM lets you try two VM engines on a real file in the browser with no sign-up: 3 free runs a day, up to 5 KB per file (resets at 00:00 UTC). A free account raises the input limit to 30 KB. Pro is $9/month or $79/year and adds a CI API so the protection step is part of electron-builder, not a manual afterthought. Standard requests are processed in memory and are not written to logs. Start at https://vm.ldvmp.com.

Top comments (0)