KA1 protocol
KA1 is the wire format. One endpoint, one method, one envelope shape, and no key material ever transmitted.
Transport
Every call is a POST of a JSON envelope to
https://keyauth.devnguvcl.dev/api/v1/gateway. The action name lives inside the
ciphertext, so an observer cannot distinguish a login from a heartbeat.
{
"v": 1,
"app": "<32 hex>",
"sid": "<32 hex or empty>",
"ts": <unix seconds>,
"n": "<24 hex nonce>",
"iv": "<24 hex>",
"ct": "<base64>",
"tag": "<32 hex>",
"sig": "<64 hex>"
}
request { "a": "login", "p": { … } }
success { "ok": true, "t": …, "d": { … } }
failure { "ok": false, "t": …,
"e": "INVALID_KEY",
"m": "…" }
Key derivation
salt = SHA-256( "KA1" 0x00 appId 0x00 sessionId 0x00 nonce 0x00 binding )
encKey = HKDF-SHA256( appSecret, salt, "KA1|<dir>|enc", 32 )
macKey = HKDF-SHA256( appSecret, salt, "KA1|<dir>|mac", 32 )
dir = "c2s" for requests, "s2c" for responses
binding = "" for requests, the raw request signature for responses
aad = "KA1" 0x1f v 0x1f app 0x1f sid 0x1f ts 0x1f nonceHex 0x1f dir
ct, tag = AES-256-GCM( encKey, iv, payload, aad )
sig = HMAC-SHA256( macKey, aad 0x1f ivHex 0x1f ctBase64 )
Four properties follow from that construction, and each one blocks a specific attack:
- No key on the wire. Both sides derive from the application secret, so capturing traffic yields nothing reusable.
-
Direction separation. Requests and responses use different
dirvalues, so a recorded request can never be replayed back as a response. - Response binding. The response salt mixes in the request signature, so a captured response is useless against any other request.
- Header authentication. The envelope header is the AEAD associated data, so app id, session, timestamp and nonce are all covered by the tag and cannot be edited in flight.
Treat plaintext failures as fatal
Errors raised before the envelope can be opened, such as UNKNOWN_APP or
BAD_SIGNATURE, are necessarily returned in plaintext. A correct client must treat any
unsigned response as a hard failure. Accepting one as success is how a man in the middle would
inject a fake “login ok”.
Session lifecycle
initwith an emptysid. The server checks version and blacklists, opens a session and returns the new session id inside the encrypted payload.- Every later call carries that
sidin the envelope header. loginvalidates the key, binds the device and returns the active release.validateextends the session and re-checks the key, so a ban takes effect mid session.logoutcloses it, or it expires after the configured lifetime.
The response to init is keyed with the request session id, which is empty. A
client cannot derive keys for an id it has not received yet, which is why the new id travels in the
payload rather than the header.
Actions
| Action | Purpose |
|---|---|
init | Version check, blacklist check, opens a session. |
login | Validates a key, binds devices, returns the active release. |
validate | Heartbeat, extends the session, re-checks the key. |
logout | Closes the session immediately. |
log | Writes a client message into the activity log. |
var | Fetches one encrypted application variable. |
file.list | Re-reads the active release, optionally with inline payloads. |
file.chunk | Fetches one encrypted chunk of the file behind a JSON key. |
ban | Client initiated ban. Disabled unless enabled per application. |
Replay and abuse protection
- Nonce cache. A nonce seen once is refused with
REPLAY_DETECTED. - Timestamp window. Outside the per application tolerance you get
CLOCK_SKEW. - Rate limits. Per IP per application, plus a stricter counter on failed logins.
- Blacklists. IP and hardware id, checked on every authenticated request.
Implementing this yourself? Writing your own client has a language agnostic checklist and test vectors.