Socket protocol specification
Everything you need to add Entrixy online mode to your own controller: the WebSocket messages, end-to-end encryption and test vectors.
device_key for volume production is covered in §10. Reference: esp32-ws-example (MIT).
1. Transport and message envelope
- Endpoint:
wss://entrixy.com/ws(hostentrixy.com, port 443, path/ws). In the open model the host comes from the key or a setting — see /open. - TLS: plain WSS. No subprotocol is negotiated.
- Frames: text (UTF-8) JSON. The discriminator is the field
type. - Reconnection: a fixed interval of 5 s. RX watchdog: no incoming frames for 90 s → restart.
2. Connection and authentication
The controller introduces itself in the first frame:
→ device_hello
{ "type":"device_hello",
"device_key": "<32 hex>",
"device_secret": "<32 hex>",
"e2ee": true|false } // whether a valid ownerSecret exists (§5)
device_key/device_secret — the two 32-character hex halves of the device code (with an optional ENTX-DEV:prefix). The server checks sha256(device_secret) against the hash by device_key.
❮ device_ok { "type":"device_ok", "hb_interval":300 } // success; hb_interval in s (10..3600)
← error { "type":"error", "reason":"auth" } // failure → the server closes
3. Opening (basic mode, without E2EE)
❮ device_command
{ "type":"device_command", "action":"open", "command_id":"<id>", "number_id":<n> }
// action:"close" — for a bistable drive
The controller pulses the relay (1000 ms by default) and replies:
→ device_status
{ "type":"device_status", "command_id":"<id>",
"level":"success"|"warning"|"danger"|"info",
"message":"...", "final":true
[, "position":"open"|"closed"|"unknown" ] }
level outside the whitelist is treated as info.
4. Heartbeat and liveness
The server does not ping devices — the fan-out does not scale. The device keeps the connection alive:
→ { "type":"ping" } ← { "type":"pong" } // the device sends this every ~30 s
If a device stays silent for more than about 90 s, the server disconnects it. A ping from the server is answered by the device with pong. For a bistable drive there is an object-state heartbeat: a spontaneous device_status without command_id with position every hb_interval seconds. The server can change it: ❮ { "type":"hb_interval", "seconds":<N> }.
5. E2EE: provisioning the ownerSecret
End-to-end encryption ensures that the server cannot forge an opening — the command is signed by the app. ownerSecret (32 bytes) is not baked into the firmware: the owner's app delivers it over USB serial:
PROVISION <64 hex> → store the ownerSecret in NVS, return the fingerprint WIPE → erase it (basic mode) STATUS → show the fingerprint fingerprint = HMAC-SHA256(ownerSecret, "fp")[0..3] (hex)
After PROVISION or WIPE the controller reconnects with an updated e2ee. A provisioned controller rejects a plain device_command — only §6 applies.
6. E2EE: challenge-response opening
With e2ee=true instead of device_command there is a challenge-response. Freshness comes from a single-use nonce, not from time:
1. ← sock_challenge_req { "type":"sock_challenge_req", "command_id":"<id>" }
2. → sock_challenge { "type":"sock_challenge", "command_id":"<id>", "nonce":"<b64 16B>" }
3. ← sock_fire { "type":"sock_fire", "command_id","number_id",
"token":"<b64>", "owner_sig":"<b64>", "guest_id":<n>,
"nonce":"<b64>", "proof":"<b64>" }
4. → device_status success
| Who | token | Check |
|---|---|---|
| Owner | empty | proof == HMAC-SHA256(ownerSecret, nonce)[0..15] |
| Guest | 3 bytes | verify owner_sig, derive guest_key, verify proof |
token (3B) = [guest_did 2B LE][perms 1B] // a capability, no TTL owner_sig = HMAC-SHA256(ownerSecret, token)[0..15] guest_key = HKDF-SHA256(salt=null, ikm=ownerSecret, info="guest"||guest_did(2B LE), L=32) proof = HMAC-SHA256(guest_key, nonce)[0..15]
Binary fields are base64. The nonce is 16 bytes and signatures are truncated to 16. The HKDF salt=null is 32 zero bytes.
7. Revoking a guest
❮ sock_revoke { "type":"sock_revoke", "number_id","guest_id","version","revoked", "sig":"<b64>" }
→ sock_revoke_ack
sig = HMAC-SHA256(ownerSecret, "rev" || guest_id(2B LE) || version(4B LE) || revoked(1B))[0..15]
The controller applies it only when version is greater than the stored one — monotonic, so the server can neither forge it nor bring access back.
8. Cryptographic primitives
- HMAC-SHA256 — signatures; truncation takes the first 16 bytes.
- HKDF-SHA256 (RFC 5869),
salt=null→ 32 zero bytes. - Socket opening uses no AES/GCM — only HMAC and HKDF. (AES-256-GCM belongs to the separate subsystem that encrypts guest bundles, not here.)
The derivation of guest_key and the signatures are identical to the guest branch of BLE — the cryptography is reused.
9. Test vectors (E2EE)
Known-answer vectors. Binary fields are given in both hex and base64; the frames carry base64.
ownerSecret (32B) = a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf fingerprint = b358977a = HMAC(ownerSecret,"fp")[0..3] nonce (16B) = 000102030405060708090a0b0c0d0e0f b64 = AAECAwQFBgcICQoLDA0ODw==
Owner fire (empty token)
proof (16B) = 3fc0619c684a8261d06c1501ae4e726a = HMAC(ownerSecret, nonce)[0..15] proof b64 = P8BhnGhKgmHQbBUBrk5yag==
Guest fire (guest_did=0x0042, perms=0x01)
token (3B) = 420001 b64 = QgAB owner_sig (16B) = 850a9f31fb708b176f4ed62a43acf0ad = HMAC(ownerSecret, token)[0..15] b64 = hQqfMftwixdvTtYqQ6zwrQ== guest_key (32B) = 614da70a34890a807a25f7c5f271e530434c478f144370a2b9efaf0c58cd85c1 = HKDF(salt=null, ownerSecret, info="guest"||did_LE, L=32) proof (16B) = a8a383ae56ac8a0182df2a275e220e83 = HMAC(guest_key, nonce)[0..15] b64 = qKODrlasigGC3yonXiIOgw==
Revoke (guest_id=0x0042, version=3, revoked=1)
sig input = "rev"||guest_id_LE(2)||version_LE(4)||revoked(1) = 72657642000300000001 sig (16B) = 9433b96ddbb599f6c72bf17c0c9825fe b64 = lDO5bdu1mfbHK/F8DJgl/g==
10. Obtaining a device_key (for OEMs)
For the server to recognise a device and know whose it is, you need the pair device_key/device_secret, known to the server and bound to the owner's account.
- For a prototype or a one-off, today: the owner creates a Device object in the app, receives the pair, and the configurator bakes it into the firmware. That is enough to bring the whole protocol up and test it.
- For volume production: the claim model — a device leaves the factory belonging to nobody, and the buyer binds it to their account with a code or QR. Batches of codes and stickers are issued in the manufacturer account: you apply, and once approved you generate a batch of
device_key, receive a CSV of codes and ready-made QR stickers. For questions about terms write to hello@entrixy.com.
A working implementation of both sides is esp32-ws-example (MIT). To build .bin — see browser configurator. How this fits the open architecture: /open.