Skip to content

Threat model

This page covers what Keylight defends against, what it explicitly does not, and the boundaries of each mitigation. Read it before shipping a Keylight-backed app so you understand what the system does and doesn’t protect.

Keylight is a commercial licensing system, not a DRM / anti-tamper system. What it does:

  • Enforce server-side entitlement (activation caps, license state, lifecycle)
  • Isolate accounts so one account cannot observe or mutate another
  • Issue signed offline leases so an app can verify entitlement without a network round-trip on every launch
  • Keep private signing keys off the client - only public keys ship in apps

What it explicitly does not do:

  • Hide your app’s gating logic - a patched binary can ignore LicenseManager.isEntitled
  • Prevent reverse engineering of the SDK or of your app - the SDK source is public and the compiled binary is inspectable
  • Provide cryptographic trial protection - the XOR file is obfuscation, not encryption (see Local storage model)
  • Provide strong clock-rollback protection - see Clock manipulation for what the 1-hour heuristic does and does not cover

If your product needs DRM-grade protection, layer an obfuscation/anti-tamper toolchain on top. Keylight stops casual abuse and enforces commercial limits; it is not the last line of defense against a skilled attacker.

Keylight is designed to raise the economic floor for license abuse in commercial desktop/mobile apps, not to defeat a determined reverse engineer. The expected adversaries are:

  1. Casual users who clone a VM, reinstall, or edit plist files to reset a trial. (Mitigated.)
  2. Key sharers who buy one license and try to activate it on many devices. (Mitigated via per-key activation caps enforced server-side.)
  3. Reverse engineers with a disassembler and time. (Not mitigated. If this is your threat model, you need a different product.)

The SDK persists two pieces of local state - the trial start timestamp and the activated license (key + instance id + lease) - in two layers:

  1. Keychain (authoritative). OS-managed secure storage, scoped by configuration.keychainServicePrefix. All reads and writes prefer Keychain.
  2. File fallback (crash recovery). XOR-obfuscated files in ~/Library/Application Support/.keylight/<accountId>/ that exist so that legitimate users do not lose their entitlement when Keychain is wiped (app re-signing, user-driven resets, certain OS migrations).

The file layer is not tamper-resistant. The XOR key is fixed in the SDK source and is recoverable with strings in minutes. The files exist to raise the floor against casual inspection and to recover a user’s own license from Keychain loss - not to prevent tampering.

Tampering is caught by a different mechanism: every lease inside the file is Ed25519-signed by Keylight and re-verified on every read (LeaseVerifier.verify). A user editing expiration dates, instance IDs, or status flags inside the file will fail signature verification and flip to .invalid. Copying the file to another machine is possible but the server’s activation cap will reject the second activation when validation runs.

What this means in practice:

  • Casual reset of the trial (deleting the file / clearing Keychain) is possible. This is intentional - the trial is a marketing floor, not a DRM layer.
  • Forging an entitlement by editing the license file is not possible without the Ed25519 private key.
  • Copying a license file between machines is possible but the lease inside is bound to a specific instanceId; on next validation, the server will reject the second device once the activation cap is exceeded.
  • High-value functionality should require an activated license, not a trial file. Activated state is backed by a server-signed lease with cryptographic integrity.

License keys are generated once at mint time, returned in the Stripe webhook response, and never persisted by Keylight in raw form. Storage holds only:

  • A SHA-256 hash of the full key, used for lookup and activation checks
  • A masked display form like TEST-A1B2-****-**** for dashboard display
  • Your app’s 4-char prefix

A storage compromise therefore cannot leak activatable keys. An attacker could learn which keys exist (by hash) and how many activations each has, but cannot activate anything without the original raw key.

The tradeoff: in manual Stripe mode you handle email delivery yourself, and if you miss the webhook response the key is unrecoverable and must be reissued.

Keylight hard-enforces two per-account quotas:

  • maxActiveInstances - the total number of licensed instances active at any one time, across all of your apps. An account on the Free plan has 5 active instances total, not 5 per app. Enforced atomically on every /activate call; a breach returns HTTP 402.
  • maxMonthlyApiCalls - a monthly rolling window of hot-path API calls (/activate + /validate). A breach returns HTTP 429 with a Retry-After header set to the seconds remaining until the next calendar-month (UTC) reset.

Fields that are not gated:

  • maxLicenses - this field is a safety ceiling (100,000 on every plan today) and is not checked at mint time. License creation itself is not rate-limited; the meaningful commercial quota is maxActiveInstances, since an unused key does not consume any resources.

A canceled account retains quota access during the 7-day grace window. A suspended account retains /validate access so already-active instances can keep rotating leases, but /activate is blocked.

CORS is scoped only to the public API routes (/:tenantId/:productId/activate, /validate, /deactivate). The admin dashboard is same-origin and does not set CORS headers - it is not intended to be called cross-origin.