Spécification du protocole socket
Tout ce qu'il faut pour ajouter le mode en ligne d'Entrixy à votre propre contrôleur : les messages WebSocket, le chiffrement de bout en bout et les vecteurs de test.
device_key pour la production en série est traitée au §10. Référence : esp32-ws-example (MIT).
1. Transport et enveloppe des messages
- Endpoint:
wss://entrixy.com/ws(hostentrixy.com, port 443, chemin/ws). Dans le modèle ouvert, l'hôte provient de la clé ou d'un réglage — voir /open. - TLS: du WSS simple. Aucun sous-protocole n'est négocié.
- Trames : JSON texte (UTF-8). Le discriminant est le champ
type. - Reconnexion : intervalle fixe de 5 s. Watchdog RX : 90 s sans trame entrante → redémarrage.
2. Connexion et authentification
Le contrôleur se présente dans la première trame :
→ 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 — les deux moitiés hexadécimales de 32 caractères du code de l'appareil (avec un ENTX-DEV:préfixe facultatif). Le serveur vérifie sha256(device_secret) par rapport au hachage par 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. Ouverture (mode de base, sans E2EE)
❮ device_command
{ "type":"device_command", "action":"open", "command_id":"<id>", "number_id":<n> }
// action:"close" — for a bistable drive
Le contrôleur envoie une impulsion au relais (1000 ms par défaut) et répond :
→ device_status
{ "type":"device_status", "command_id":"<id>",
"level":"success"|"warning"|"danger"|"info",
"message":"...", "final":true
[, "position":"open"|"closed"|"unknown" ] }
level hors de la liste blanche est traité comme info.
4. Heartbeat et détection de vie
Le serveur ne ping pas les appareils — ce fan-out ne passe pas à l'échelle. C'est l'appareil qui maintient la connexion en vie :
→ { "type":"ping" } ← { "type":"pong" } // the device sends this every ~30 s
Si un appareil reste muet plus de 90 s environ, le serveur le déconnecte. Un ping venu du serveur, l'appareil y répond par pong. Pour une motorisation bistable, il existe un heartbeat de l'état de l'objet : un device_status sans command_id avec position toutes les hb_interval secondes. Le serveur peut la modifier : ❮ { "type":"hb_interval", "seconds":<N> }.
5. E2EE : provisionnement de l'ownerSecret
Le chiffrement de bout en bout garantit que le serveur ne peut pas forger une ouverture — la commande est signée par l'application. ownerSecret (32 octets) n'est pas inscrit dans le micrologiciel : c'est l'application du propriétaire qui le livre par le port série USB:
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)
Après PROVISION ou WIPE, le contrôleur se reconnecte avec un e2ee mis à jour. Un contrôleur provisionné rejette un simple device_command — seul le §6 s'applique.
6. E2EE : ouverture par défi-réponse
Avec e2ee=true au lieu de device_command on procède par défi-réponse. La fraîcheur vient d'un nonce à usage unique, pas du temps :
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
| Qui | token | Vérification |
|---|---|---|
| Propriétaire | vide | proof == HMAC-SHA256(ownerSecret, nonce)[0..15] |
| Invité | 3 octets | vérifier owner_sig, dériver guest_key, vérifier 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]
Les champs binaires sont en base64. Le nonce fait 16 octets et les signatures sont tronquées à 16. Le salt=null de HKDF vaut 32 octets nuls.
7. Révoquer un invité
❮ 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]
Le contrôleur ne l'applique que lorsque version est supérieur à celui qui est stocké — c'est monotone, si bien que le serveur ne peut ni le forger ni rétablir l'accès.
8. Primitives cryptographiques
- HMAC-SHA256 — signatures ; la troncature prend les 16 premiers octets.
- HKDF-SHA256 (RFC 5869),
salt=null→ 32 octets nuls. - L'ouverture par socket n'utilise pas AES/GCM — seulement HMAC et HKDF. (AES-256-GCM relève du sous-système distinct qui chiffre les lots d'invités, pas d'ici.)
La dérivation de guest_key et les signatures sont identiques à la branche invité de BLE — la cryptographie est réutilisée.
9. Vecteurs de test (E2EE)
Vecteurs à réponse connue. Les champs binaires sont donnés en hexadécimal et en base64 ; les trames transportent du base64.
ownerSecret (32B) = a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf fingerprint = b358977a = HMAC(ownerSecret,"fp")[0..3] nonce (16B) = 000102030405060708090a0b0c0d0e0f b64 = AAECAwQFBgcICQoLDA0ODw==
Ouverture propriétaire (jeton vide)
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. Obtention d'un device_key (pour les OEM)
Pour que le serveur reconnaisse un appareil et sache à qui il appartient, il faut la paire device_key/device_secret, connue du serveur et liée au compte du propriétaire.
- Pour un prototype ou une pièce unique, aujourd'hui : le propriétaire crée un objet de type Appareil dans l'application, reçoit la paire, et le configurateur l'inscrit dans le micrologiciel. Cela suffit pour monter tout le protocole et le tester.
- Pour la production en série : le modèle « claim » — l'appareil sort d'usine sans appartenir à personne, et l'acheteur le lie à son compte avec un code ou un QR. Les lots de codes et d'étiquettes sont émis dans le compte fabricant : vous déposez une demande et, une fois approuvée, vous générez un lot de
device_key, recevez un CSV des codes et des étiquettes QR toutes prêtes. Pour toute question sur les conditions, écrivez à hello@entrixy.com.
Une implémentation fonctionnelle des deux côtés est esp32-ws-example (MIT). Pour compiler .bin — voir configurateur dans le navigateur. Comment cela s'inscrit dans l'architecture ouverte : /open.