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. Unsere eigenen Platinen, die es sprechen, sind die NET-Reihe; die nur über Bluetooth arbeiten, sind die BLE-Reihe.
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 2 s in der Referenz-Firmware für den ESP32. RX-Watchdog: drei Ping-Perioden ohne eingehende Frames (3 × ping_s, 30 s mit den Standardwerten) → 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, "ping_s":10 } // success; hb_interval in s (10..3600), ping_s — the ping period
← error { "type":"error", "reason":"auth" } // failure → the server closes
Ein Controller, der über das Internet arbeitet, hält selbst die Verbindung zum Server, daher sieht der Server die IP-Adresse des Controllers und den Zeitpunkt jedes Befehls an ihn. Nur der an das Telefon des Eigentümers gebundene ESP32-Netzwerkcontroller prüft die Signatur jedes Befehls (§5–6). Die Controller auf ESP8266, Raspberry Pi und Node.js vertrauen dem Server: Er kann ihnen selbst ein Öffnen schicken.
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"|"opening"|"closing"|"unknown" ] }
level außerhalb der Whitelist wird behandelt als info. Die App zeigt message wörtlich an und schreibt sie neben dem Tippen ins Protokoll.
Der Controller meldet auch, was bei ihm selbst passiert. Ein solcher device_status hat ein leeres command_id und ein Feld event – die App schreibt ihn ins Protokoll des Besitzers.
→ { "type":"device_status", "command_id":"", "event":"button"|"limit"|"auto",
"level":"success", "message":"Opened", "final":true, "position":"open" }
| event | Wann |
|---|---|
button | die Ausgangstaste innen wurde gedrückt; bei einem bistabilen Antrieb wechselt sie zwischen Öffnen und Schließen |
limit | der Endschalter: die Tür wurde von Hand bewegt, oder der Antrieb ist nicht innerhalb der im Controller eingestellten Fahrzeit dort angekommen, wohin ihn ein Druck geschickt hat (level danger, message "Did not close" oder "Did not open"). Bestätigt der Endschalter einen Druck, trägt das Ereignis "confirm":true: die App aktualisiert diesen Druck im Protokoll, statt eine Zeile hinzuzufügen |
auto | der Controller hat nach der eingestellten Zahl von Sekunden selbst geschlossen |
Mit Endschalter meldet der Controller opening oder closing vom Impuls, bis der Endschalter die Position bestätigt. Ohne Endschalter meldet er sofort die Zielposition.
"hold":true bittet die App, die Meldung bis zur nächsten Meldung des Controllers auf der Karte zu halten; ohne das verschwindet sie nach einigen Sekunden. Die Controller setzen es für Öffnen und Closing (Stufe info, das Tor ist unterwegs) und für einen Fehler.
Die Antwort auf einen Befehl, der den Antrieb tatsächlich gestartet hat, enthält "started":true. Nur für einen solchen Druck wartet der Server auf das Ende durch den Endschalter: „Belegt“ oder „öffnet bereits“ bei einem zweiten Druck übernimmt nicht die Bewegung eines anderen.
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 ping_s seconds
Schweigt ein Gerät länger, als der Server erlaubt, trennt der Server es und zeigt es offline. Den Takt nennt der Server in device_ok.ping_s: ein Drittel dieser Zeit (DEVICE_OFFLINE_S, standardmäßig 30 s). Ein Gerät, das "ping_ctl":true in device_hello sendet, folgt ihm; andere pingen alle 30 s und gelten nach 90 s Stille als offline. Auf 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.
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>" [, "action":"close"] }
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]
Ein Schließen für einen bistabilen Antrieb trägt "action":"close" und wird über den Nonce mit den fünf angehängten ASCII-Bytes close am Ende: proof = HMAC-SHA256(key, nonce || "close")[0..15]. Dabei ist key der ownerSecret des Besitzers oder der guest_key des Gastes. Ein Öffnen wird über den reinen Nonce signiert. Der Server kann also weder ein Öffnen in ein Schließen verwandeln noch umgekehrt: Die Signatur würde nicht passen. Was zu tun ist, entscheidet der Signierende – nach der Stellung, die der Controller gemeldet hat.
Der Controller akzeptiert eine Nonce 30 Sekunden nach der Ausgabe und nur in der Verbindung, in der er sie ausgegeben hat: Ein vom Server zurückgehaltener signierter Befehl wirkt später nicht mehr.
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", "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.
Der ESP32-Netzwerkcontroller speichert bis zu 96 Entscheidungen zu Gästen. Ist die Liste voll, macht ein Eintrag „erlaubt“ Platz für einen neuen Widerruf; sind alle 96 Widerrufe, entfällt der älteste.
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==
Schließen (bistabiler Antrieb)
signed input = nonce || "close" = 000102030405060708090a0b0c0d0e0f636c6f7365 owner proof = ef15ca41823f126de0815f84c7dc8275 = HMAC(ownerSecret, input)[0..15] b64 = 7xXKQYI/Em3ggV+Ex9yCdQ== guest proof = 54f23d61766a274d8996c26e67925c32 = HMAC(guest_key, input)[0..15] b64 = VPI9YXZqJ02JlsJuZ5JcMg==
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.