← License Tracker

Security overview

This document describes how License Tracker protects your data, what it does not protect against, and how to report a vulnerability. It is written for customers and reviewers who want specifics. Parameter values below are taken from the source code of the current release.

Summary

  • Your data lives in one encrypted vault file on your computer. Nothing is sent to the publisher, and there is no account, cloud sync or telemetry.
  • The vault is encrypted twice: the whole database file, and then the most sensitive fields individually.
  • The encryption key is derived from your master password with Argon2id, kept only in memory, and wiped when the vault locks.
  • The program's local server accepts connections only from this computer and rejects requests from other websites.
  • There is no password recovery and no backdoor. A forgotten master password means the vault cannot be opened by anyone.

Threat model

What License Tracker is designed to protect against

  • A lost or stolen computer or disk, or a copied vault file. Someone who has licenses.db, its .keymeta.json file and every snapshot, but not your master password, cannot read your licenses. They can only guess passwords, and each guess costs a full Argon2id derivation (see below).
  • Backups and copies stored elsewhere. Snapshots and copied data folders are encrypted the same way as the vault, so you can keep them on a USB drive or in cloud storage.
  • Other websites open in your browser. A web page cannot read from or write to the local server (see Local server).
  • Someone walking up to an unattended computer. The vault auto-locks after a period of inactivity (15 minutes by default) and wipes the key from memory.

What it does not protect against

  • Malware running under your Windows account while the vault is unlocked. Such a program can read the process memory, capture keystrokes, read the screen or the clipboard, or use the app the same way you do. No local application can defend against this. Keep Windows and your antivirus up to date.
  • A weak master password. The protection of a copied vault rests on your password. Argon2id makes guessing slow, but it cannot save a password such as password1. Use a long one; several unrelated words work well.
  • Data you export. CSV and calendar (.ics) exports are plain text. JSON exports are plain text except for sensitive fields, which are left out or encrypted with a passphrase you choose.
  • Things you copy. A license key you copy goes to the Windows clipboard, and Windows clipboard history (Win+V) may keep it.
  • Memory written to disk by Windows. While the vault is unlocked, the key is in memory and could end up in the page file or hibernation file. Windows BitLocker or Device Encryption protects those; we recommend turning it on.

Encryption

Key derivation

The master password is never stored. When you create or unlock the vault, a 256-bit key is derived from it:

ParameterValue
AlgorithmArgon2id
Memory512 MiB
Iterations (time cost)3
Parallelism4 lanes
Output32 bytes
Salt16 random bytes, generated when the vault is created

If a computer cannot allocate 512 MiB, derivation falls back to 256, 128 and then 64 MiB, keeping the other parameters. The salt and the parameters actually used are stored in licenses.db.keymeta.json next to the vault. That file is not secret, but it is needed to open the vault. It is written atomically (temporary file and rename) so a crash cannot leave it half-written.

A wrong password is detected by the database failing to decrypt, not by comparing against a stored hash. There is no password hash on disk.

Layer 1: whole-file encryption

The database is SQLite with SQLite3 Multiple Ciphers (bundled in better-sqlite3-multiple-ciphers). Every page of the file is encrypted with the library's chacha20 scheme:

SettingValue
CipherChaCha20-Poly1305 (sqleet-compatible), with a Poly1305 tag on every page
Key inputthe 32-byte Argon2id key, hex-encoded
Page key derivationPBKDF2-HMAC-SHA256, 64,007 iterations, 16-byte random salt stored at the start of the file
Legacy modeoff

These settings are fixed in the code and applied before the key on every open, re-key and snapshot, so a change in library defaults cannot make existing vaults unreadable. The Poly1305 tags mean that a modified page is detected rather than silently read.

Everything in the vault is covered by this layer, including names, prices, dates, icons and scanner results.

Layer 2: field-level encryption

The most sensitive values are encrypted a second time, individually, with libsodium crypto_secretbox (XSalsa20-Poly1305) under the same session key, with a fresh random 24-byte nonce for every value:

  • license keys,
  • order numbers,
  • registered email addresses,
  • account user names,
  • notes,
  • vendor account passwords,
  • any custom field marked Encrypted.

A weakness in the page cipher alone would not expose these values.

Keys in memory

The derived key exists only in the program's memory while the vault is unlocked. Locking (by hand, by auto-lock, from the tray, or by quitting) overwrites the key buffer with zeros, closes the database and revokes every session. The web page also drops all decrypted data it was showing when the vault locks. In the interface, revealed keys and passwords are masked again after 30 seconds.

Changing the password

A password change verifies the current password, keeps a copy of the vault from before the change (it opens with the old password), re-encrypts every field-level value under the new key, and re-keys the database file with PRAGMA rekey, all in one transaction. The new key settings are staged in a .keymeta.json.pending file and only promoted once the change has committed, so an interruption cannot leave a vault that neither password opens. All existing sessions are signed out.

Local server

License Tracker runs as a local web server that its own window (Microsoft Edge in app mode) and the lt command-line tool talk to.

  • Loopback only. It listens on 127.0.0.1. Other computers on your network cannot connect to it.
  • Host allowlist. Requests whose Host header is not localhost, 127.0.0.1, ::1, a *.localhost name or a host you configured explicitly are refused with HTTP 421. This blocks DNS-rebinding attacks, where a malicious site points its own domain at your loopback address.
  • Cross-site write protection. Any request that changes data is refused with HTTP 403 if it carries a foreign Origin header or a Sec-Fetch-Site header of cross-site or same-site.
  • Sessions. Unlocking issues a random 32-byte session token. The window gets it as an HttpOnly, SameSite=Strict cookie that page scripts cannot read and other sites cannot send; the lt tool uses it as a bearer token. Expired or idle sessions get a 401 response.
  • Auto-lock. Sessions expire after the configured idle time (default 15 minutes; 5 minutes to 4 hours, or never). When the last session expires, the key is wiped and the database closed.
  • Unlock throttling. After 5 consecutive wrong passwords, each further attempt must wait 1, 2, 4, 8 ... seconds after the previous failure, up to 5 minutes. Throttled attempts are rejected before any key derivation. The same counter covers the current-password check when changing the password.
  • Content Security Policy. Pages are served with default-src 'self', script-src 'self' (no inline scripts, no eval), connect-src 'self' (an injected script could not send data to the internet) and frame-ancestors 'none', plus X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer and cross-origin isolation headers. API responses are marked Cache-Control: no-store.
  • Error responses never include stack traces or internal details.
  • Tray control. The tray icon's Lock and Quit commands use a random token generated for each run, so other local programs cannot trigger them.

Network access

License Tracker makes outbound requests only for:

  1. The update check, once at start-up: an HTTPS GET of https://licensetracker.danewagenhoffer.com/updates/latest.json, with a User-Agent naming the program version. Set LICENSETRACKER_UPDATE_CHECK=off to disable it.
  2. Icon fetching, when you fetch an icon, add a developer or vendor with a website, or run lt seed: requests to that website, and, if it has no usable icon, to Google's favicon service with the website's domain name.

Icon fetching only accepts http/https URLs. Before every request, including each redirect hop (followed manually, at most 5), the host name is resolved and refused if any of its addresses is private, loopback, link-local or otherwise not publicly routable. Responses are capped (512 KB for HTML, 2 MB for images) and images are accepted only if their content is PNG, JPEG or WebP. A residual risk remains: a DNS server could answer differently between the check and the connection.

Receipt parsing (pasted text, .eml, .msg, .pdf, .txt, .html) and the software scanner run entirely on your computer.

See backend-node/legal/PRIVACY.txt for the full list of what is stored and sent.

Signed updates

The update manifest is a small JSON document signed with Ed25519. The program contains the publisher's update public key and ignores any manifest whose signature does not verify, so a compromised web server or a network attacker cannot use the update notice to point you at a different download. The manifest also lists the SHA-256 hash and size of the installer.

License Tracker only shows a notice. It never downloads or runs an installer by itself.

License keys

License keys have the form LT1-<payload>.<signature>. The payload names the licensee, their email address, the edition and the issue date; the signature is Ed25519 over the exact payload bytes. The program contains only the public key and checks keys offline, with no activation server. The private signing key never leaves the publisher's machine. A small list of revoked key IDs is built into each release.

The trial and license state are kept in product-license.json in the data folder. It holds no secrets. When the trial ends, the vault becomes read-only; reading, searching, exporting, snapshots and deleting keep working.

Snapshots and recovery

  • Once a day on first unlock, and before every schema upgrade or snapshot restore, the vault is copied with SQLite VACUUM INTO into the backups folder, together with its own .keymeta.json. The newest 10 are kept.
  • Snapshots are encrypted with the key in use when they were taken, so they open with the master password of that time.
  • Restoring a snapshot first snapshots the current vault, so a restore can be undone.
  • Older program versions refuse to open a vault written by a newer version instead of risking damage.

What is not encrypted

  • licenses.db.keymeta.json: the Argon2id salt and parameters.
  • product-license.json: the trial start date and your license key (your name and email address).
  • Log files in %LOCALAPPDATA%\License Tracker\logs. They are designed never to contain passwords, keys or license secrets, and values that look like passwords, tokens or long hex keys are masked before writing.
  • Files you export as CSV, .ics or JSON (except passphrase-encrypted fields).

Reporting a vulnerability

Please email support@licensetracker.danewagenhoffer.com with "Security" in the subject line. Include the version (Settings > About), the steps to reproduce, and what an attacker could achieve. Please do not include real license keys or vault files from your own data. You will get an acknowledgement, and fixes for confirmed issues are released as updates and noted in the changelog.