All guides

Hardening an Electron App's License Check: Main Process, Fuses, ASAR Integrity and Signed Tokens

Stop "unpack app.asar, edit one line" cracks: keep your Electron license check in the main process, flip the fuses, enable ASAR integrity, sign tokens.

Electron licensing illustration for Hardening an Electron App's License Check: Main Process, Fuses, ASAR Integrity and Signed Tokens

An Electron app license check is harder to bypass when the main process owns the decision, the renderer receives a status, and signed release artifacts tie the packaged code to your publisher identity. Start with how to add licensing to an Electron app if you need to issue and validate your first key. This guide covers what to harden before shipping that check.

Why Electron apps are easy to crack: app.asar is your source code

app.asar packages your application's files. It provides convenient distribution and can hide source from casual browsing, but its JavaScript is still readable after extraction. A renderer-only flag is especially easy to override. Someone who can modify the renderer can skip the conditional that hides a paid menu and call whatever is behind it.

Minifying JavaScript can make inspection less convenient. It cannot give bundled code the same protection as a server-held signing key. Keep private signing keys and vendor API credentials outside the package, including source maps and release configuration. Electron's ASAR archive documentation explains how packaged files are accessed.

Rule #1: the license check lives in the main process, not the renderer

Read the user-entered key from protected local storage in the main process. Validate with the app version and expected product ID there, and store the decision there. If a paid export runs through IPC, its main-process handler must check the current entitlement immediately before doing the work. Disabling an Export button is useful feedback. The handler is where access is enforced.

Start the window with a checking state and let the main process validate asynchronously. Gate paid operations while that decision is pending. Keep any free or read-only flows available according to your product policy. The startup licensing guide shows how to keep this path responsive during timeouts.

Exposing only license status to the UI with contextBridge and IPC

Keep contextIsolation and renderer sandboxing enabled, with nodeIntegration off. Load a bundled renderer, restrict navigation and new windows, and validate the IPC sender. The following main-process module takes an existing BrowserWindow, the exact URL you load into it and your main-process key reader. Register the handler once for that window, then call refreshLicense from the main process. The getter cannot start a request or return the key, token or raw provider response.

// main-license.cjs
const { app, ipcMain } = require('electron');
const { PermitCoreClient } = require('@permitcore/permitcore');
const path = require('node:path');

function installLicenseStatus(window, rendererUrl, readKey, settings) {
  const client = new PermitCoreClient({
    baseUrl: 'https://api.permitcore.dev',
    trustedPublicKeys: settings.publicKeys,
    deviceId: settings.deviceId,
    cacheDirectory: path.join(app.getPath('userData'), 'license-cache')
  });
  let state = 'checking';
  ipcMain.handle('license:status', (event) => {
    const frame = event.senderFrame;
    if (event.sender !== window.webContents ||
        !frame || frame !== window.webContents.mainFrame ||
        frame.url !== rendererUrl) throw new Error('Invalid license sender');
    return state;
  });
  return async function refreshLicense() {
    state = 'checking';
    try {
      const key = await readKey();
      const result = key && await client.validate(
        key, app.getVersion(), settings.productId
      );
      state = result?.isValid
        ? (result.isOffline ? 'offline' : 'active')
        : 'restricted';
    } catch {
      state = 'restricted';
    }
  };
}

module.exports = { installLicenseStatus };
// preload.cjs
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('license', {
  getStatus: () => ipcRenderer.invoke('license:status')
});

The renderer calls window.license.getStatus() and displays checking, active, offline or restricted. Give each privileged IPC operation its own narrow interface and authorization check. Electron's context isolation guide and IPC sender guidance explain these boundaries.

Electron fuses that close the obvious bypasses

Fuses change features in the packaged Electron binary. For the macOS or Windows release in this example, disable RunAsNode, EnableNodeOptionsEnvironmentVariable and EnableNodeCliInspectArguments to remove unused ways to run Node or attach its inspector. Turning off RunAsNode can break child_process.fork in Electron. Check that your app and its dependencies do not require these features.

Enable OnlyLoadAppFromAsar to prevent falling back to an unpacked app directory. Pair it with EnableEmbeddedAsarIntegrityValidation on supported targets. Run this example with the packaged app or executable path before signing, and integrate the equivalent settings into your package hooks in production. Electron documents each fuse and its defaults.

// set-fuses.mjs — run on the packaged macOS or Windows artifact
import { flipFuses, FuseVersion, FuseV1Options } from '@electron/fuses';

const packagedApp = process.argv[2];
if (!packagedApp) throw new Error('Provide the packaged app path');

await flipFuses(packagedApp, {
  version: FuseVersion.V1,
  [FuseV1Options.RunAsNode]: false,
  [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
  [FuseV1Options.EnableNodeCliInspectArguments]: false,
  [FuseV1Options.OnlyLoadAppFromAsar]: true,
  [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true
});

ASAR integrity and code signing: making "unpack, edit, repack" fail

Electron documents ASAR integrity for macOS from Electron 16 and Windows from Electron 30. The binary needs the expected hash of the ASAR header, and the archive needs its integrity metadata. With ASAR enabled, Electron Packager 18.3.1 or later and Electron Forge 7.4.0 or later supply the package metadata. Flipping a fuse without that hash can make a legitimate package fail at launch.

Package and set fuses before signing. Sign the resulting artifacts and notarize macOS releases for direct distribution. The integrity check can detect an archive change on its own. Signing and the platform's signature enforcement make it meaningful against replacement of both the archive and its expected hash or binary. Test on a clean machine with the same enforcement your customers use. Verify an unchanged release starts, then test archive tampering on a disposable copy. ASAR integrity and Electron code signing describe the packaging steps.

No secrets in the bundle: server-signed tokens verified with a public key

Keep the private signing key server-side. Ship trusted public keys with the app or through a trusted configuration channel. The public key verifies a signature without granting the ability to mint proof. Obtain trusted pins independently of an incoming token or cache file. Plan how to distribute replacement pins before retiring a key. See the signing key rotation guide.

PermitCore Node SDK 2.1.1 verifies signed pc_grace_v1 cache proof for network interruptions. It also supports a separate pc_offline_v1 token provisioned for offline activation. On installations designed to stay offline, call activateOffline(token, deviceId, productId) for the provisioned token and validateOffline(deviceId, productId) for subsequent checks against that saved proof. See offline licensing for the trust and device requirements.

Offline launches: cached signed validation and a grace period

For normally connected apps, set a bounded offline grace period and configure trustedPublicKeys before the first online validation. Use a private cache directory and a stable device ID. When a valid response includes signed grace proof, the SDK verifies it before caching. On a network failure, validate can return isValid: true and isOffline: true from verified cache. The proof is bound to the key and product and, when applicable, the activated device. Its signed validUntil limits offline use.

Give customers a readable offline state and a way to reconnect. An explicit server rejection does not fall back to old proof on that request. A device that stays offline cannot learn about server-side revocation until it reconnects, and expired or invalid cached proof cannot authorize a new offline session. See offline grace mode for policy choices.

What hardening can't stop (and why revocation and updates matter more)

Someone who controls a machine can instrument it or replace the client. Fuses, IPC boundaries, signatures and short-lived proof raise the work required for common bypasses. Use verified server-side authorization for paid operations that already run on your infrastructure. Keep update artifacts signed and Electron current so you can ship fixes. Revalidate when connected according to your policy, and revoke compromised keys without punishing every customer.

For abuse that involves a shared key instead of a modified app, review device and sharing limits. If you find a redistributed build, follow the response checklist in what to do when your software is cracked. Make recovery and legitimate offline use part of the same release test.

Putting it together with the PermitCore Node SDK (checklist)

  1. Install PermitCore Node SDK 2.1.1 and construct the client only in the main process.
  2. Keep the key reader private. Call validate(key, app.getVersion(), productId) and use the result at the paid operation boundary.
  3. Configure trusted public keys and cache storage. Test a valid key, an explicit rejection, a wrong product, a network outage, and expired or tampered offline proof.
  4. Verify the preload exposes only the status getter and a different frame cannot invoke it. Check that paid IPC handlers enforce the entitlement independently.
  5. Set fuses, generate ASAR integrity metadata, sign the final package and test update and tamper handling on each supported platform.
Put the guide into practice

Build your first licensed product without building a license server.

Create a free PermitCore account, issue a test license, and validate it from your application.