Spezifikation des Socket-Protokolls
Alles, was Sie brauchen, um Ihrem eigenen Controller den Online-Modus von Entrixy zu geben: die WebSocket-Nachrichten, Ende-zu-Ende-Verschlüsselung und Testvektoren.
device_key für die Serienfertigung ist in §10 beschrieben. Referenz: esp32-ws-example (MIT).
1. Transport und Nachrichtenhülle
- Endpoint:
wss://entrixy.com/ws(hostentrixy.com, Port 443, Pfad/ws). Im offenen Modell stammt der Host aus dem Schlüssel oder aus einer Einstellung — siehe /open. - TLS: reines WSS. Es wird kein Subprotokoll ausgehandelt.
- Frames: Text (UTF-8) JSON. Als Unterscheidungsmerkmal dient das Feld
type. - Wiederverbindung: festes Intervall von 5 s. RX-Watchdog: 90 s ohne eingehende Frames → Neustart.
2. Verbindung und Authentifizierung
Der Controller stellt sich im ersten Frame vor:
→ 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 — die beiden 32-stelligen Hex-Hälften des Gerätecodes (mit optionalem ENTX-DEV:Präfix). Der Server prüft sha256(device_secret) gegen den Hash nach 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. Öffnen (Grundmodus, ohne E2EE)
❮ device_command
{ "type":"device_command", "action":"open", "command_id":"<id>", "number_id":<n> }
// action:"close" — for a bistable drive
Der Controller gibt einen Impuls auf das Relais (standardmäßig 1000 ms) und antwortet:
→ device_status
{ "type":"device_status", "command_id":"<id>",
"level":"success"|"warning"|"danger"|"info",
"message":"...", "final":true
[, "position":"open"|"closed"|"unknown" ] }
level außerhalb der Whitelist wird behandelt als info.
4. Heartbeat und Lebendigkeit
Der Server pingt nicht die Geräte — das skaliert nicht. Das Gerät hält die Verbindung selbst am Leben:
→ { "type":"ping" } ← { "type":"pong" } // the device sends this every ~30 s
Schweigt ein Gerät länger als etwa 90 s, trennt der Server die Verbindung. Ein ping vom Server beantwortet das Gerät mit pong. Für einen bistabilen Antrieb gibt es einen Heartbeat des Objektzustands: ein spontanes device_status ohne command_id mit position alle hb_interval Sekunden. Der Server kann das ändern: ❮ { "type":"hb_interval", "seconds":<N> }.
5. E2EE: Bereitstellung des ownerSecret
Die Ende-zu-Ende-Verschlüsselung stellt sicher, dass der Server eine Öffnung nicht fälschen kann — den Befehl signiert die App. ownerSecret (32 Byte) steckt nicht in der Firmware: Die App des Besitzers liefert es über die serielle USB-Schnittstelle:
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)
Nach PROVISION oder WIPE verbindet sich der Controller neu mit aktualisiertem e2ee. Ein provisionierter Controller weist ein einfaches device_command zurück — es gilt nur §6.
6. E2EE: Öffnen per Challenge-Response
Mit e2ee=true statt device_command läuft ein Challenge-Response ab. Die Frische stammt von einer einmaligen Nonce, nicht von der Zeit:
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
| Wer | token | Prüfung |
|---|---|---|
| Besitzer | leer | proof == HMAC-SHA256(ownerSecret, nonce)[0..15] |
| Gast | 3 Byte | prüfe owner_sig, leite ab guest_key, prüfe 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]
Binäre Felder sind base64. Die Nonce ist 16 Byte lang, Signaturen werden auf 16 gekürzt. Das HKDF- salt=null ist 32 Nullbytes.
7. Einen Gast entziehen
❮ 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]
Der Controller wendet es nur an, wenn version größer ist als der gespeicherte — monoton, sodass der Server ihn weder fälschen noch den Zugang zurückholen kann.
8. Kryptografische Primitive
- HMAC-SHA256 — Signaturen; beim Kürzen werden die ersten 16 Byte genommen.
- HKDF-SHA256 (RFC 5869),
salt=null→ 32 Nullbytes. - Das Öffnen über Socket kommt ohne AES/GCM aus — nur HMAC und HKDF. (AES-256-GCM gehört zum getrennten Teilsystem, das Gastbündel verschlüsselt, nicht hierher.)
Die Ableitung von guest_key und die Signaturen sind identisch mit dem Gastzweig von BLE — die Kryptografie wird wiederverwendet.
9. Testvektoren (E2EE)
Vektoren mit bekannten Ergebnissen. Binäre Felder sind sowohl hexadezimal als auch in base64 angegeben; in den Frames steht base64.
ownerSecret (32B) = a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf fingerprint = b358977a = HMAC(ownerSecret,"fp")[0..3] nonce (16B) = 000102030405060708090a0b0c0d0e0f b64 = AAECAwQFBgcICQoLDA0ODw==
Owner Fire (leeres 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. Bezug eines device_key (für OEMs)
Damit der Server ein Gerät erkennt und weiß, wem es gehört, braucht es das Paar device_key/device_secret, das dem Server bekannt und an das Konto des Besitzers gebunden ist.
- Für einen Prototyp oder ein Einzelstück heute: Der Besitzer legt in der App ein Objekt vom Typ Gerät an, erhält das Paar, und der Konfigurator schreibt es in die Firmware. Das reicht, um das gesamte Protokoll aufzusetzen und zu testen.
- Für die Serienfertigung: das Claim-Modell — ein Gerät verlässt das Werk, ohne jemandem zu gehören, und der Käufer bindet es mit einem Code oder QR an sein Konto. Chargen von Codes und Aufklebern werden im Herstellerkontoausgegeben: Sie stellen einen Antrag, und nach der Freigabe erzeugen Sie eine Charge
device_key, erhalten eine CSV mit den Codes und fertige QR-Aufkleber. Bei Fragen zu den Konditionen schreiben Sie an hello@entrixy.com.
Eine funktionierende Umsetzung beider Seiten ist esp32-ws-example (MIT). Um .bin — siehe Konfigurator im Browser. Wie sich das in die offene Architektur einfügt: /open.