keyward asks you for two things worth stealing: a ledger of unredeemed game keys, and a Steamworks session cookie that can act as your studio. This is what it does with them, what it protects against, and what it does not.
Open a private security advisory. Please do not open a public issue for anything exploitable.
There is no bounty. There is a maintainer who will read it.
The complete list. Nothing else is contacted, ever.
| Host | When | Carrying |
|---|---|---|
partner.steamgames.com |
keyward check |
Your session cookie and one key at a time |
api.isthereanydeal.com |
keyward scan |
Your API key and a game id |
| A shop's own domain | First scan only | Nothing. Fetching its favicon |
cdn.*.steamstatic.com |
Adding a game with an appid | Nothing. Fetching its store art |
| Your webhook | keyward watch, only if you set one |
New findings, including contact names |
The browser page itself contacts nothing. It runs under a Content Security
Policy with default-src 'none', and shop logos and store art are downloaded by
the local server and stored as data URIs so the page never reaches out. This is
tested, not just intended.
There is no telemetry, no analytics, no update check, and no crash reporting.
keyward holds two: the Steamworks session cookie, and the database key if you
ran keyward encrypt. Neither goes in the config file or the database.
| Platform | Store | How |
|---|---|---|
| macOS | login keychain | security |
| Windows | DPAPI, tied to your Windows account | Windows PowerShell, ciphertext under ~/.config/keyward/secrets |
| Linux | libsecret, so GNOME Keyring or KWallet | secret-tool |
No native module and no dependency: each is the command line the platform already ships. Where none of them is available, keyward refuses to persist the secret and reads it from the environment instead.
The macOS path passes the secret to security as an argument, so it is visible
in the process list for the length of one exec. security offers no other way.
The Windows and Linux paths both use stdin.
| Secret | Where | Why there |
|---|---|---|
| Steamworks session cookie | OS keychain | It can act as your studio. It never touches the database or the config file, and on platforms without a keychain keyward refuses to persist it at all |
| IsThereAnyDeal API key | ~/.config/keyward/config.json, mode 0600 |
Read-only access to public pricing |
| Web UI password | Same file, as a salted scrypt hash | Never stored in the clear |
| Webhook URL | Same file | Treat it as a secret; anyone holding it can post to your channel |
| The key ledger | ~/.config/keyward/keyward.db, mode 0600 |
SQLite sets file modes from the umask, which is normally world-readable. keyward tightens it on every open, including the WAL sidecars |
keyward ui runs an HTTP server. Every one of these is
covered by a test:
- Loopback only. It binds
127.0.0.1, never0.0.0.0, and that is not configurable. - A token per run, 24 random bytes, required on every request and compared in constant time. It is minted at startup and dies when you stop the server.
- The
Hostheader is pinned. A hostname that resolves to loopback cannot be used to reach the API from a page on someone else's domain. - Cross-origin requests are refused, even carrying a valid token.
frame-ancestors 'none'andX-Frame-Options: DENY, so nothing can frame it and click through you.Referrer-Policy: no-referrer, because the token is in the URL and must not travel to a shop link you click.- The live-reload endpoint does not exist outside
--dev. It holds a connection open and watches the filesystem.
So that starting the server prints one thing you can click. It is in your browser history for as long as that history lasts, but it stops working the moment the server does, so a stale one is worth nothing. Moving it to a cookie would take it out of the address bar and hand back the cross-site protection that requiring a custom header currently gives for free. That trade is not worth it here.
keyward password set puts a scrypt-hashed password in front of the browser,
with the session held in memory and ten wrong answers earning a five-minute
lockout.
It gates the browser and nothing else. By default the database is a plain SQLite file that any viewer will open, and the CLI reads it without asking. To cover the file itself, see below.
keyward encrypt seals the database with AES-256-GCM. The key is 32 random
bytes in the macOS keychain, so no command asks you for anything afterwards.
Nothing new got pulled in to do this. node:sqlite hands over the database as
bytes and takes it back the same way, so keyward decrypts into memory at open
time and writes the sealed file back after every change, through a temporary
file and a rename.
What it closes. The file leaving the machine: a copied database, a backup, a
Time Machine snapshot, a folder synced to Dropbox or iCloud, a .db attached to
a support ticket.
What it does not. Anything running as your user, which can ask the keychain for the key the same way keyward does. And nothing at all while keyward is running, because the database is decrypted in memory by then. Encryption at rest is exactly that.
What it costs you. Three things, which is why it is off by default:
- Lose the key and the ledger is unreadable, by you as much as by anyone. The recovery code is printed once, at encryption time.
- The file stops being inspectable. No SQLite viewer, no
sqlite3shell. - SQLite's crash journal cannot protect a file it does not own. The write is atomic, so a crash mid-save leaves the previous database rather than a broken one, but a crash between two writes loses the last change.
- Only one keyward can hold it at a time. Saving checks that the file is still the one it read and refuses rather than overwrite another process's work.
keyward decrypt puts it back. keyward restore-key <code> re-seats the key on
another machine.
If your worry is a stolen laptop rather than a stray file, FileVault answers that better, and the two stack.
- Zero runtime dependencies.
package.jsondeclares none, and CI fails the build if that ever changes. What you are trusting is the code insrc/. - The build and test toolchain is TypeScript, Playwright and
@types/node, all pinned inpackage-lock.json. CI installs withnpm ciand runsnpm audit. - keyward downloads no code at runtime and evaluates nothing it fetches. The only things it downloads are images, which it stores and re-serves as data.
Said plainly, because a security page that only lists wins is not one.
- Anyone with your user account. The ledger and the config are readable by you, so they are readable by anything running as you. Encryption does not change this: the keychain answers that process too.
- A stolen or unlocked machine. Use FileVault, or your platform's
equivalent.
keyward encryptcovers the file at rest, not a machine someone is already sitting in front of. - A malicious dependency of the toolchain, if you build from source with a
compromised npm. The lockfile and
npm cinarrow this; they do not close it. - Your Steamworks account itself. keyward drives the same web page you would click, using your own session. If that session is stolen elsewhere, keyward is not the problem and cannot be the fix.
- The webhook. If you set one, findings leave your machine to a third party under their terms, including the names of contacts. It is off by default and it stays off unless you turn it on.
Not a vulnerability, but the thing most likely to do real harm: keyward's findings are, with one exception, patterns rather than proof. It says so on every surface that shows them. A studio that publicly accuses the wrong journalist because a tool sounded certain does more damage than the leak did, and no amount of hardening addresses that.