Especificación del protocolo de socket
Todo lo necesario para añadir el modo en línea de Entrixy a tu propio controlador: los mensajes WebSocket, el cifrado de extremo a extremo y los vectores de prueba.
device_key para producción en serie se explica en §10. Referencia: esp32-ws-example (MIT).
1. Transporte y sobre del mensaje
- Endpoint:
wss://entrixy.com/ws(hostentrixy.com, puerto 443, ruta/ws). En el modelo abierto el host viene de la clave o de un ajuste; ver /open. - TLS: WSS a secas. No se negocia ningún subprotocolo.
- Tramas: JSON de texto (UTF-8). El discriminador es el campo
type. - Reconexión: intervalo fijo de 5 s. Watchdog de RX: 90 s sin tramas entrantes → reinicio.
2. Conexión y autenticación
El controlador se presenta en la primera trama:
→ 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 — las dos mitades hex de 32 caracteres del código del dispositivo (con un ENTX-DEV:prefijo opcional). El servidor comprueba sha256(device_secret) contra el hash por 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. Apertura (modo básico, sin E2EE)
❮ device_command
{ "type":"device_command", "action":"open", "command_id":"<id>", "number_id":<n> }
// action:"close" — for a bistable drive
El controlador da un pulso al relé (1000 ms por defecto) y responde:
→ device_status
{ "type":"device_status", "command_id":"<id>",
"level":"success"|"warning"|"danger"|"info",
"message":"...", "final":true
[, "position":"open"|"closed"|"unknown" ] }
level fuera de la lista blanca se trata como info.
4. Heartbeat y detección de vida
El servidor no hace ping a los dispositivos: ese abanico no escala. Es el dispositivo el que mantiene viva la conexión:
→ { "type":"ping" } ← { "type":"pong" } // the device sends this every ~30 s
Si un dispositivo calla más de unos 90 s, el servidor lo desconecta. Un ping del servidor lo responde el dispositivo con pong. Para un motor biestable hay un heartbeat del estado del objeto: un device_status sin command_id con position cada hb_interval segundos. El servidor puede cambiarlo: ❮ { "type":"hb_interval", "seconds":<N> }.
5. E2EE: aprovisionamiento del ownerSecret
El cifrado de extremo a extremo garantiza que el servidor no puede falsificar una apertura — la orden la firma la aplicación. ownerSecret (32 bytes) no va incrustado en el firmware: lo entrega la aplicación del propietario por el puerto serie 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)
Tras PROVISION o WIPE el controlador se reconecta con un e2ee actualizado. Un controlador aprovisionado rechaza un device_command sin más: solo se aplica §6.
6. E2EE: apertura por desafío-respuesta
Con e2ee=true en lugar de device_command se hace un desafío-respuesta. La frescura viene de un nonce de un solo uso, no del tiempo:
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én | token | Comprobación |
|---|---|---|
| Propietario | vacío | proof == HMAC-SHA256(ownerSecret, nonce)[0..15] |
| Invitado | 3 bytes | verificar owner_sig, derivar guest_key, verificar 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]
Los campos binarios van en base64. El nonce es de 16 bytes y las firmas se truncan a 16. El salt=null de HKDF son 32 bytes cero.
7. Revocar a un invitado
❮ 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]
El controlador solo lo aplica cuando version es mayor que el guardado: es monótono, así que el servidor no puede ni falsificarlo ni devolver el acceso.
8. Primitivas criptográficas
- HMAC-SHA256 — firmas; el truncado toma los primeros 16 bytes.
- HKDF-SHA256 (RFC 5869),
salt=null→ 32 bytes cero. - La apertura por socket no usa AES/GCM: solo HMAC y HKDF. (AES-256-GCM pertenece al subsistema aparte que cifra los paquetes de invitado, no a esto.)
La derivación de guest_key y las firmas son idénticas a la rama de invitado de BLE — la criptografía se reutiliza.
9. Vectores de prueba (E2EE)
Vectores con respuesta conocida. Los campos binarios se dan en hex y en base64; en las tramas va base64.
ownerSecret (32B) = a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf fingerprint = b358977a = HMAC(ownerSecret,"fp")[0..3] nonce (16B) = 000102030405060708090a0b0c0d0e0f b64 = AAECAwQFBgcICQoLDA0ODw==
Apertura del propietario (token vacío)
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. Obtención de un device_key (para OEM)
Para que el servidor reconozca un dispositivo y sepa de quién es, hace falta el par device_key/device_secret, conocido por el servidor y ligado a la cuenta del propietario.
- Para un prototipo o una unidad suelta, hoy: el propietario crea un objeto de tipo Dispositivo en la aplicación, recibe el par, y el configurador lo incrusta en el firmware. Con eso basta para levantar todo el protocolo y probarlo.
- Para producción en serie: el modelo de reclamación (claim): el dispositivo sale de fábrica sin dueño y el comprador lo vincula a su cuenta con un código o QR. Los lotes de códigos y pegatinas se emiten en la cuenta de fabricante: presentas la solicitud y, una vez aprobada, generas un lote de
device_key, recibes un CSV con los códigos y pegatinas QR ya hechas. Para preguntas sobre las condiciones escribe a hello@entrixy.com.
Una implementación funcional de ambos lados es esp32-ws-example (MIT). Para compilar .bin — ver configurador del navegador. Cómo encaja esto en la arquitectura abierta: /open.