Machine fingerprints
A fingerprint is the id your application sends to tell devices apart when it validates a key. LicenseManager never computes it: it compares what you send, exactly, and each fingerprint it hasn’t seen on a license is a new machine, which takes one of the license’s places.
What makes a good fingerprint
| Property | Why |
|---|---|
| Stable | The same on every launch, after reboots, app updates and system updates. A fingerprint that changes makes a new machine each time, until the license runs out of places. |
| Unique per device | Two devices with the same fingerprint are one machine for LicenseManager, and share one place. |
| Private | Don’t send hardware serial numbers or MAC addresses as they are: hash them. The fingerprint is shown to you, and in the customer portal when the machine has no name. |
| Specific to your application | Hash the device id with your application’s name, so your fingerprint can’t be matched with another vendor’s. |
Fingerprints are compared exactly, case included, are trimmed, and can be up to 255 characters. A SHA-256 hash, as 64 hex characters, fits.
Computing one
Read an id the operating system keeps for the device, then hash it with the name of your application:
macOS
The hardware UUID, IOPlatformUUID, from ioreg -rd1 -c IOPlatformExpertDevice. It stays the same after reinstalling macOS.
import { createHash } from "node:crypto";
function fingerprint(deviceId: string) {
// Specific to your app: the same device gives another fingerprint in another app.
return createHash("sha256").update(`acme-photo:${deviceId}`).digest("hex");
}Then send a name and a platform with the validation, like the computer’s name and macOS: they help you and your customers recognize the machine, and are what the customer portal shows.
Adding a secret stored on the device
A device id can be read by anyone, so someone could send the same one from another computer. To make that harder, have your application generate a random secret on first launch, and store it where only your application can read it:
macOS
The Keychain, in an item only your application can access.
Copying the application’s files to another computer doesn’t copy the secret, so the copy has another fingerprint. It’s not 100% safe: someone with enough access to the device can still read the secret and copy it. But it’s a good blocker against copying an installation as it is.
The best combination is both: the stable device id and the secret, hashed together in the final fingerprint. The device id ties the fingerprint to the hardware, and the secret ties it to this installation:
import { createHash, randomBytes } from "node:crypto";
function fingerprint(deviceId: string) {
// Generated on first launch, then read from the Keychain, DPAPI or the Secret Service.
const secret = readSecret() ?? storeSecret(randomBytes(32).toString("hex"));
return createHash("sha256")
.update(`acme-photo:${deviceId}:${secret}`)
.digest("hex");
}If the secret is lost, like when the customer reinstalls your application or clears its data, the fingerprint changes and it’s a new machine. Store the secret somewhere that survives updates of your application.
Don’t change how you compute fingerprints once you’ve shipped. A new formula in version 2.4 makes every machine new for LicenseManager when customers update: licenses with a max machines start refusing them with machine_limit_reached, until the old machines are deactivated.
Choosing what a machine is
The fingerprint decides what one place of a license means. Pick it to match what you sell:
| You sell | Fingerprint |
|---|---|
| A license per computer | The device id, as above. |
| A license per user, on any number of computers, like a seat | A user id from your application, and an unlimited max machines, or a max machines per user. |
| A license per installation, like a server app | An id your application generates on first launch and stores next to its data. A reinstallation is a new machine. |
| A license per user account on shared computers | The device id combined with the operating system’s user name. |
Virtual machines and containers
Cloned virtual machines and container images often share the same system id, so they share one fingerprint, and only take one place. When that matters, like for a server licensed per instance, generate an id on first launch and store it in a persistent volume, or combine the system id with something unique to the instance.
The opposite also happens: containers recreated on every deploy get a new id each time, so each deploy is a new machine. Use a fingerprint that survives redeploys, like an id stored in a volume, or have your application deactivate its machine when it shuts down.
Edge cases
A customer reinstalls their system, and the fingerprint changes. It’s a new machine, and the old one keeps its place until it’s deactivated: by you from the license’s Machines tab, or by the customer from the customer portal.
A hardware change, like a new disk or motherboard, can change the system id too. Same as above.
The same device, two licenses. One machine in each license, counted by each.
Debugging. The Validations tab of a license shows the fingerprint sent with each validation: a license with many New machine validations from one customer usually means the fingerprint isn’t stable.