NovaKey-Daemon is a cross-platform Go agent that receives authenticated secrets from a trusted device and injects them into the currently focused text field.
It’s built for cases where you don’t want to type high-value secrets (master passwords, recovery keys, etc.) on your desktop keyboard:
- the secret lives on a trusted device (e.g. your phone)
- delivery is encrypted and authenticated
- the daemon injects into the focused control (with optional clipboard mode when injection is not possible)
NovaKey listens on one TCP address (listen_addr, default 0.0.0.0:60768) and routes each incoming connection by a one-line preface:
NOVAK/1 /pair\n— pairing (and pairing subroutes)NOVAK/1 /msg\n— encrypted approve/arm/disarm/inject messages
Clients must send a route preface line (NOVAK/1 /msg\n or NOVAK/1 /pair\n). Connections without a valid preface are rejected.
NovaKey uses:
- ML-KEM-768 (Kyber) for per-message KEM shared secret establishment
- HKDF-SHA-256 for key derivation
- XChaCha20-Poly1305 for authenticated encryption
- timestamp freshness checks, replay protection, and per-device rate limiting
All /msg requests decrypt to the following plaintext structure:
- 8-byte timestamp (
uint64, big-endian, unix seconds) - Inner Message Frame v1 (required)
The inner frame is:
- versioned (
frame_version = 1) - includes
device_id,msg_type, andpayload - authenticated by the outer AEAD (and validated for device-id consistency)
Supported inner msg_type values:
- Inject — payload is the secret string
- Approve — payload optional/empty (two-man mode)
- Arm — payload optional JSON:
{"ms":15000} - Disarm — payload typically empty
Messages that do not contain a valid Inner Message Frame v1 are rejected.
- arming (“push-to-type”)
- two-man approval window (approve then inject)
- injection safety rules (
allow_newlines,max_inject_len) - target policy allow/deny lists (focused app/window)
- local Arm API (loopback only, token protected)
- clipboard policy controls:
allow_clipboard_when_disarmed(allows clipboard use when blocked by gates/policy)allow_clipboard_on_inject_failure(allows clipboard use when injection fails after gates pass; default true on Linux)
When there are no paired devices (missing/empty device store), the daemon generates a QR code (novakey-pair.png) at startup.
Pairing uses the /pair route on the same TCP listener. Clients must send the route preface:
NOVAK/1 /pair\n
High-level flow:
-
Client sends a hello JSON line containing a one-time token:
{"op":"hello","v":1,"token":"<b64url>"}\n -
Server replies with the ML-KEM public key and a short fingerprint:
{"op":"server_key","v":1,"kid":"1","kyber_pub_b64":"...","fp16_hex":"...","expires_unix":...}\n -
Client verifies
fp16_hexmatches the fingerprint embedded in the QR. -
Client sends an encrypted register request (Kyber + XChaCha20-Poly1305). The server saves the device PSK and reloads device keys.
Pairing output is sensitive (treat it like a password).
NovaKey also supports /pair/* subroutes on the same listener (routed by the same preface line). These exist for alternative pairing workflows used by clients.
NovaKey stores per-device static keys in the device store referenced by devices_file (default devices.json).
Device store is sealed using DPAPI.
Device store is stored in one of two forms:
- Sealed wrapper (preferred): encrypted-at-rest using an OS keyring–stored sealing key.
- Plaintext JSON (explicit opt-in): only used when the OS keyring is unavailable and plaintext storage is explicitly allowed.
Important: On some Linux systems (especially headless services or logins backed by hardware tokens), the daemon may not be able to access the user keyring from a system service context. In those environments, you may need to explicitly allow plaintext device storage.
Control this with:
-
require_sealed_device_store:true→ fail closed if the store is not sealed / keyring unavailable (recommended default)false→ allows plaintextdevices.jsononly when the keyring is unavailable, using strict0600perms (enable only if you must)
NovaKey supports YAML (preferred) or JSON configuration.
| Option | Default | Description |
|---|---|---|
listen_addr |
"127.0.0.1:60768" |
TCP address NovaKey listens on for /pair* and /msg. |
max_payload_len |
4096 |
Maximum allowed decrypted payload size (bytes). |
max_requests_per_min |
60 |
Per-device rate limit for accepted /msg requests. |
devices_file |
"devices.json" |
Path to the device store containing paired device keys. |
server_keys_file |
"server_keys.json" |
Path to the server’s ML-KEM key material. Treated as sensitive. |
| Option | Default | Description |
|---|---|---|
require_sealed_device_store |
false |
If true, NovaKey refuses to run when the OS keyring is unavailable or when the devices store is not sealed. |
- If you want it fail-closed by default, set it to
truein your shipped config.
| Option | Default | Description |
|---|---|---|
rotate_kyber_keys |
false |
If true, rotates the server’s ML-KEM key pair on startup (requires re-pairing). |
rotate_device_psk_on_repair |
false |
If true, re-pairing an existing device replaces its stored key. |
pair_hello_max_per_min |
30 |
Per-IP rate limit for /pair hello attempts (in-memory). |
| Option | Default | Description |
|---|---|---|
log_dir |
"./logs" |
Directory for rotating log files. Ignored if log_file is set. |
log_file |
(unset) | Single log file path. Overrides log_dir when set. |
log_rotate_mb |
10 |
Maximum size (MB) of a log file before rotation. |
log_keep |
10 |
Number of rotated log files to retain. |
log_stderr |
true |
If true, logs are also written to stderr. |
log_redact |
true |
Best-effort redaction of tokens/secrets and long blobs. |
| Option | Default | Description |
|---|---|---|
arm_enabled |
true |
Blocks injection unless locally armed. |
arm_duration_ms |
20000 |
Duration (ms) the daemon remains armed after arming is triggered. |
arm_consume_on_inject |
true |
If true, a successful injection consumes the armed state. |
| Option | Default | Description |
|---|---|---|
allow_clipboard_when_disarmed |
false |
Allows clipboard use when blocked by gates/policy. Use with care. |
allow_clipboard_on_inject_failure |
true on Linux, else false |
Allows clipboard use when injection fails after gates pass (Wayland, perms, etc.). |
| Option | Default | Description |
|---|---|---|
arm_api_enabled |
true |
Enables the local HTTP arm API. |
arm_listen_addr |
"127.0.0.1:60769" |
Address the Arm API binds to. Must resolve to loopback or it will refuse to start. |
arm_token_file |
"arm_token.txt" |
Path to the Arm API authentication token file. |
arm_token_header |
"X-NovaKey-Token" |
Header name used to supply the Arm API token. |
| Option | Default | Description |
|---|---|---|
allow_newlines |
false |
Reject newline characters in secrets when false. |
max_inject_len |
256 |
Maximum number of characters allowed in a single inject. |
| Option | Default | Description |
|---|---|---|
two_man_enabled |
true |
Requires an approve message before injection is allowed. |
approve_window_ms |
15000 |
Window (ms) after approval in which injection is allowed. |
approve_consume_on_inject |
true |
If true, approval is consumed after a successful injection. |
| Option | Default | Description |
|---|---|---|
target_policy_enabled |
false |
Enables focused target enforcement before injection. |
use_built_in_allowlist |
false |
Applies a built-in allowlist when enabled and no explicit rules are set. |
allowed_process_names |
(empty) | Allowed process names (normalized). |
allowed_window_titles |
(empty) | Case-insensitive substrings required in the focused window title. |
denied_process_names |
(empty) | Always-denied processes. |
denied_window_titles |
(empty) | Always-denied window title substrings. |
- Keep
listen_addron loopback unless you need LAN. - Prefer
require_sealed_device_store: true(fail closed) unless your Linux service environment cannot access the OS keyring. - Keep
arm_enabled: trueandtwo_man_enabled: truefor safest operation. - On Linux Wayland, injection may not be possible; rely on clipboard mode (
allow_clipboard_on_inject_failure) as needed.
SECURITY.md— threat model + security propertiesPROTOCOL.md— wire formats for/pair*and/msg
- Security:
[email protected]