LicenseManager

Blog

How to get a unique machine id in Electron

5 min read

Three laptops, each with a different machine id

Many features of a desktop app need to recognize the computer it runs on: limiting a license to 3 machines, giving one free trial per computer, counting active installs, or syncing settings per device. All of them need a machine id: a value that’s the same every time the app runs on a computer, and different on every other computer.

Electron doesn’t have an API for it, and browsers deliberately don’t expose one. But each operating system has its own, readable from Node.js in the main process.

Where each system keeps its id

SystemSourceChanges when
macOSIOPlatformUUID, the hardware UUIDThe logic board is replaced. Survives macOS reinstalls.
WindowsMachineGuid in the registryWindows is reinstalled, or the image is generalized with Sysprep.
Linux/etc/machine-idThe system is reinstalled. Often identical on cloned images.

macOS

The hardware UUID is read with ioreg:

import { execFile } from "node:child_process";
import { promisify } from "node:util";
 
const run = promisify(execFile);
 
async function macMachineId() {
  const { stdout } = await run("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
  return stdout.match(/"IOPlatformUUID" = "([^"]+)"/)?.[1];
}

It’s the most stable of the three: it’s tied to the hardware, and stays the same through system reinstalls and user accounts.

Windows

The MachineGuid value is in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography, generated when Windows is installed:

async function windowsMachineId() {
  const { stdout } = await run("REG.exe", [
    "QUERY",
    "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography",
    "/v",
    "MachineGuid",
    "/reg:64",
  ]);
  return stdout.match(/MachineGuid\s+REG_SZ\s+(\S+)/)?.[1];
}

The /reg:64 flag matters: a 32-bit process on 64-bit Windows otherwise reads the 32-bit view of the registry, which may not have the value.

Linux

systemd generates /etc/machine-id at install time. Older systems only have /var/lib/dbus/machine-id:

import { readFile } from "node:fs/promises";
 
async function linuxMachineId() {
  for (const path of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
    try {
      return (await readFile(path, "utf8")).trim();
    } catch {}
  }
}

Reading the machine id with ioreg on macOS, REG.exe on Windows and /etc/machine-id on Linux

What about node-machine-id?

The node-machine-id package reads these same sources, and returns a SHA-256 hash of the id. It’s widely used, and fine for a quick start. Keep in mind that it hasn’t been updated in years, that it runs synchronous commands by default (machineIdSync blocks the main process), and that it has no fallback when the command fails.

Whichever you use, run it in the main process, asynchronously, with a timeout, and cache the result.

Don’t use these instead

  • The MAC address (os.networkInterfaces()): changes with a dock, a VPN, a new network adapter, and macOS and Windows randomize it on some networks. Fine as a last fallback, not as the main source.
  • The hostname (os.hostname()): users rename their computers.
  • Disk or CPU serial numbers: they need admin rights or WMI queries on Windows, aren’t available in virtual machines, and the CPU id is the same on every computer with the same model.
  • A random id stored in localStorage: lost when the user clears the app’s data.

Hash it before sending it

A raw hardware UUID is a stable identifier of a person’s computer, across every app that reads it. Don’t send it to your server as is: hash it, with something specific to your app, so the value you store can’t be matched with another app’s.

import { createHash } from "node:crypto";
 
function fingerprint(machineId: string) {
  return createHash("sha256")
    .update(`com.acme.photo:${machineId}`)
    .digest("base64url");
}

Mention it in your privacy policy too: it’s a device identifier, and some jurisdictions treat it as personal data.

The edge cases that will hit you

Every app that limits machines eventually gets these support emails:

  • Cloned virtual machines share the same MachineGuid or /etc/machine-id, so two VMs count as one. Usually fine, sometimes abused.
  • Windows reinstalls create a new MachineGuid. The customer’s computer takes a second seat.
  • Several user accounts on one computer share the same machine id. That’s usually what you want: one computer, one seat.
  • Command failures: ioreg or REG.exe can fail in locked-down corporate environments. Fall back to something, never crash.

A machine id alone also has one weakness for licensing: it’s easy to fake. It’s a value anyone can read, so someone can copy the id of a licensed computer, and make their own computer report it.

Build a fingerprint, not just an id

A sturdier fingerprint combines the machine id with a random secret generated on first launch and stored on the computer, ideally in the system keychain:

fingerprint = hash(machine id) + ":" + install secret + ":" + product id
  • The machine id part makes it different on each computer.
  • The install secret makes it hard to fake: knowing the machine id of another computer isn’t enough, you also need the secret stored on it, which never leaves it except hashed with the rest.
  • The product id keeps your products’ fingerprints apart on the same computer.

Store the secret in the keychain, not in your app’s folder: it survives uninstalls and reinstalls of your app, so the customer doesn’t use a new seat each time. See safeStorage vs keytar for how to reach the keychain from Electron.

This is how LicenseManager’s Electron SDK builds its fingerprint, with fallbacks on the MAC address and the hostname when the machine id can’t be read. See machine fingerprint for the details.

Let customers free a seat

Whatever your fingerprint, it will change one day: a new laptop, a reinstall. When the customer hits the machine limit, don’t make them email you. Show which computers use their license, with their names and last seen dates, and let them deactivate one. A customer portal does it without any code in your app: see self-service licensing for what else it should let customers do.

Wrapping up

Read IOPlatformUUID, MachineGuid or /etc/machine-id from the main process, hash it with your app’s id, and fall back gracefully when it fails. For licensing, add a random secret stored in the keychain, so the fingerprint can’t be copied from another computer.

More articles