Skip to content

Security: 404SecNotFound/Keymaker-v2

Security

SECURITY.md

Security Policy

Reporting a Vulnerability

Please do not open public issues for security vulnerabilities.

Report vulnerabilities privately via GitHub private security advisories: go to the repository's Security tab → Advisories → Report a vulnerability.

Please include:

  • A description of the vulnerability and its impact.
  • Steps to reproduce or a proof of concept.
  • Affected versions, if known.
  • Any suggested mitigation (optional).

We aim to acknowledge reports within 72 hours and to provide an initial assessment within 7 days. We will coordinate disclosure with the reporter; credit will be given in the release notes unless you prefer to remain anonymous.

Supported Versions

Version Supported
Latest release Yes — security fixes
Older releases No — please upgrade

Security fixes are shipped in new releases; only the latest release line receives patches.

Scope Notes

Keymaker is a client-side browser encryption PWA. In scope:

  • The cryptographic core (src/lib/crypto.ts — frozen legacy format; src/lib/keymaker-crypto.ts — KEYM v1; src/lib/keym-v2.ts and src/lib/keym-v2-shamir.ts — KEYM v2 and v3; the app writes v3) and its wire formats (IBTZ v0/v1, KEYM v1, KEYM v2, KEYM v3). See docs/FORMAT.md, docs/FORMAT-V2-DESIGN.md and docs/FORMAT-V3-DESIGN.md.
  • Key derivation, nonce/salt generation, memory handling (secureErase), and authentication of ciphertext and header metadata (AAD).
  • The static export's content security policy and supply-chain (dependency) integrity.

Out of scope / known limitations:

  • Endpoint compromise. All cryptography runs in the user's browser. If the device is compromised (malware, malicious extensions, keyloggers), no web app can protect the plaintext or password. Browser extensions are worth naming separately: they run inside the page's origin and the CSP does not constrain them, so an extension with access to the tab can read a secret before it is ever encrypted.
  • The clipboard, and the screen. Copying puts a secret somewhere every other application can read and, on some platforms, somewhere it syncs off the device. Keymaker clears its own clipboard writes on a timer, best-effort. Secret fields blur by default, which is a defence against a passing glance and not against a camera.
  • JavaScript memory hygiene is best-effort. secureErase zero-fills buffers, but the JS engine/GC may retain copies of secrets. WebCrypto keys are non-extractable where the API allows.
  • Deniability / traffic analysis. v1, v2 and v3 containers are not padded, so their length reveals the plaintext's length exactly: the overhead is fixed for a given format and settings, so container length determines plaintext length byte for byte (FORMAT-V2-DESIGN §8). A v4 container pads its payload (docs/FORMAT-V4-DESIGN.md): its length reveals only which bucket the plaintext falls in, and every plaintext up to 248 bytes gives the same length. v4 is written on request, not by default, and padding is not deniability: the container is still plainly a Keymaker container, its cipher and slots are still readable, and a plaintext just over a bucket boundary is still on the far side of it. If the size of what is being protected is itself sensitive and the backup is not v4, the cipher does not help.
  • Password strength. Weak passwords undermine any KDF. Argon2id (memory-hard) is the default recommendation, but cannot fix a weak password.
  • Vulnerabilities in third-party dependencies should be reported upstream as well; do report them here if they affect Keymaker's shipped bundle.

Cryptographic Inventory

Primitive Implementation
AES-256-GCM WebCrypto (crypto.subtle)
ChaCha20-Poly1305 @noble/ciphers (audited, pure JS)
Argon2id hash-wasm (WASM inlined as base64, offline-capable)
PBKDF2-HMAC-SHA-256 WebCrypto
HKDF-SHA-256 WebCrypto
CSPRNG crypto.getRandomValues

Format details and AAD rules: see docs/FORMAT.md. Historical audit notes: see SECURITY-AUDIT.md.

Known CSP trade-off: wasm-unsafe-eval

The Content-Security-Policy script-src includes 'wasm-unsafe-eval'. It is there because Argon2id is WebAssembly and the browser will not instantiate a module without it.

In Chromium, 'wasm-unsafe-eval' also permits eval(). That is a quirk of the implementation rather than of the specification, which scopes the token to WebAssembly compilation. So on Chromium the policy is broader than it reads.

Documented rather than fixed, because there is nothing worth fixing on either side of the trade:

  • It is not currently exploitable. Reaching eval() needs script injection, and the rest of the policy is what prevents that: default-src 'none', no unsafe-inline (every inline script carries a build-time sha256 hash and the build fails closed if one is missing), and connect-src 'none'. A dynamically injected inline script is blocked, and the browser suite asserts that rather than assuming it.
  • It weakens defence in depth, which is the honest cost. If an injection ever did become possible, this token means the attacker also gets eval() rather than being confined to what the hashed scripts already do.

The alternative is worse. Removing the token means either a JavaScript Argon2id — orders of magnitude slower, so in practice users would choose weaker parameters — or PBKDF2 as the only KDF. Both trade a real reduction in key-derivation strength for a hypothetical one in injection resistance.

Recorded here so it is not rediscovered as a finding by every subsequent audit. It has been found once already, as U20 in docs/reports/UAT-2026-08-14.md.

There aren't any published security advisories