NovaKey-Daemon is security-critical software: it receives encrypted secrets over TCP and injects them into the active window. We take security seriously and welcome review.
NovaKey currently implements:
- /msg (Protocol v3): ML-KEM-768 + HKDF-SHA-256 + XChaCha20-Poly1305, with timestamp freshness checks, replay protection, and per-device rate limiting.
- /pair (Pairing v1): one-time pairing token + ML-KEM-768 + XChaCha20-Poly1305 registration on the same TCP listener.
Optional safety controls:
- The shipped sample configuration enables arming and two-man by default (text inject requires local arm and a recent approve window)
- Arming gate (“push-to-type”)
- Two-man mode (typed approve then inject)
- Injection safety rules (
allow_newlines,max_inject_len) - Target policy allow/deny lists
- Arm API (token protected)
Reviewer note: Please test only on systems you own/operate and do not expose your test daemon to the public Internet. This project is designed for LAN/local testing and normal desktop sessions.
Security updates are provided for the latest stable release only.
| Version | Supported | Notes |
|---|---|---|
| Latest release | Supported | Receives security fixes promptly |
| All others | Not supported | Upgrade recommended |
Please do not open public GitHub issues for security problems.
Email:
[email protected]- or
[email protected]if needed - PGP key: https://downloads.osbornepro.com/publickey.asc
If you need encrypted comms, include “PGP” in your email and we’ll coordinate.
- Steps to reproduce
- Affected version(s) and OS(es)
- Impact
- Proof-of-concept if available
- Relevant logs/config (with secrets redacted)
NovaKey listens on one TCP address: listen_addr (default 127.0.0.1:60768).
Each connection is routed by an initial ASCII preface line (required):
NOVAK/1 /pair\n→ pairing handlerNOVAK/1 /msg\n→ message handler
Connections that do not begin with one of these exact lines are rejected before any cryptographic processing.
Routing is performed using a fixed ASCII preface line:
NOVAK/1 /pair\nNOVAK/1 /msg\n
Routing occurs before any cryptographic processing.
Security properties are unchanged by routing:
/pairis protected by a one-time token and ML-KEM/msgrequires a valid per-device PSK
Pairing is initiated when there are no paired devices (missing/empty device store).
The daemon creates a one-time pairing token (128-bit) with a TTL (default 10 minutes):
- token encoding: base64 raw URL (
base64.RawURLEncoding) - token is consumed by the first successful
/pairhello
This prevents random LAN pairing attempts.
The daemon exposes its ML-KEM public key during pairing, and also provides a short fingerprint:
fp16_hex = hex(sha256(pubkey)[0:16])
The QR should embed this fingerprint so the phone can verify the received public key matches what was scanned (mitigates “wrong host / wrong key” and some spoofing scenarios on a LAN).
Pair registration uses:
- ML-KEM-768 decapsulation on the daemon
- XChaCha20-Poly1305 AEAD for the register payload
- AEAD key derived with HKDF-SHA-256:
IKM = sharedKemsalt = tokenBytesinfo = "NovaKey v4 Pair AEAD"- outLen = 32 bytes
AAD binds the request to the encapsulation and nonce:
AAD = "PAIR" || ct || nonce
Successful pairing results in a per-device 32-byte PSK stored in the device store. Treat pairing results and the device store as secrets.
Device store at rest:
- On Windows, the device store is DPAPI-protected (
*.dpapi.json). - On non-Windows, the device store is sealed with XChaCha20-Poly1305 using an OS keyring-derived key when available.
- In environments where a daemon process cannot access the user keyring (commonly headless services or logins backed by hardware tokens), NovaKey can be configured to allow plaintext device storage with strict permissions (
0600). This is an explicit opt-in and should be enabled only when required.
If an attacker obtains a device PSK, they can produce valid /msg frames (arming/two-man can reduce silent injection risk, but does not protect a compromised host).
Each device has:
device_id(string)device_key_hex(32 bytes, hex)
device_key_hex is never sent in plaintext.
Each /msg request includes a KEM ciphertext:
- server decapsulates → per-message shared secret
Per-message AEAD key:
IKM = sharedKemsalt = deviceKeyinfo = "NovaKey v3 AEAD key"- outLen = 32 bytes
- Nonce: 24 bytes (random per message)
- AAD: binds the entire header through the KEM ciphertext
- Prevents tampering with device routing / KEM material
After decrypting, the plaintext includes a timestamp and then an inner typed frame:
inner msgType = 1→ Inject (payload is secret string)inner msgType = 2→ Approve (payload empty/ignored)inner msgType = 3→ Arm (payload JSON with duration)inner msgType = 4→ Disarm (payload empty)
Only typed frames are accepted; there is no legacy or magic-string control path.
- plaintext includes Unix timestamp (seconds)
- server rejects stale messages and large clock skew
- server caches
(deviceID, nonce)for a TTL window to detect replays
- server enforces accepted message limits per device (
max_requests_per_min)
When arm_enabled: true, frames can decrypt/validate but injection is blocked unless locally armed.
When two_man_enabled: true, injection requires a recent approve (inner msgType=2) from the same device (per-device approval window).
If arm_api_enabled: true:
- binds only to loopback (
arm_listen_addrmust resolve to loopback) - token-protected via a random token stored in
arm_token_file, supplied in headerarm_token_header
Note: processes running as the same user may be able to read the token file; host compromise is considered game-over.
Even after crypto succeeds:
- newline blocking by default (
allow_newlines: false) - max injected length (
max_inject_len) - optional target allow/deny policy for focused apps (process/window)
Target policy normalization note:
Process comparisons are normalized to reduce configuration foot-guns:
- lowercased
- path stripped (
/usr/bin/firefox,C:\...\chrome.exe) .exestripped.appstripped (macOS)
NovaKey can write logs to stderr and/or a rotating file.
When log_redact: true:
- secrets registered via
addSecret()are replaced with[REDACTED] - long base64/hex-ish blobs are replaced with
[REDACTED_BLOB] - common key/value patterns are redacted, including URL query params (e.g.
token=...&fp=...)
Even with redaction enabled, logs should still be treated as potentially sensitive and protected accordingly.
- passive sniffing / active tampering on LAN
- replay attempts
- unauthorized clients without device PSK
- rate abuse from a valid device
- fully compromised host OS / same-user malware
- physical attacks / hardware keyloggers
- compromised build pipeline
- QR exposure == pairing exposure during TTL
Thank you for helping keep NovaKey secure.
— Robert H. Osborne (OsbornePro)
Maintainer, NovaKey-Daemon
---