❮  Controladores Entrixy

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. Nuestras propias placas que lo hablan son la línea NET; las que funcionan solo por Bluetooth son la línea BLE.

A diferencia de BLE, un controlador de socket mantiene una conexión permanente con el servidor de Entrixy por WebSocket y se puede abrir desde cualquier lugar. El modo básico queda descrito por completo en esta especificación. Para el cifrado de extremo a extremo, en el que el servidor no puede falsificar una apertura, ver §5–7. La obtención de un device_key para producción en serie se explica en §10. Referencia: esp32-ws-example (MIT).

1. Transporte y sobre del mensaje

El firmware de referencia para ESP32 y ESP8266 comprueba el certificado del servidor con las raíces de Let’s Encrypt (ISRG Root X1 y X2) y ajusta su reloj por NTP para comprobar las fechas del certificado. Un servidor con un certificado de otra autoridad pone su raíz en SERVER_CA en el firmware. Con SERVER_CA vacío, el firmware se conecta sin comprobar el servidor.

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, "ping_s":10 }   // success; hb_interval in s (10..3600), ping_s — the ping period
← error      { "type":"error", "reason":"auth" }         // failure → the server closes

Un controlador que funciona por internet mantiene su propia conexión con el servidor, así que el servidor ve la dirección IP del controlador y la hora de cada orden que se le envía. Solo el controlador de red ESP32 vinculado al teléfono del propietario comprueba la firma de cada orden (§5–6). Los controladores en ESP8266, Raspberry Pi y Node.js confían en el servidor: este puede enviarles una apertura por sí mismo.

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"|"opening"|"closing"|"unknown" ] }

level fuera de la lista blanca se trata como info. La app muestra message tal cual y lo escribe en el registro junto a la pulsación.

El controlador también informa de lo que ocurre en él por sí mismo. Un device_status así tiene un command_id vacío y un campo event — la app lo escribe en el registro del propietario.

→ { "type":"device_status", "command_id":"", "event":"button"|"limit"|"auto",
    "level":"success", "message":"Opened", "final":true, "position":"open" }
eventCuándo
buttonse pulsó el botón de salida interior; en un accionamiento biestable alterna abrir / cerrar
limitel final de carrera: la puerta se movió a mano, o el motor no llegó dentro del tiempo de recorrido fijado en el controlador a donde lo envió una pulsación (level danger, message "Did not close" o "Did not open"). Cuando el final de carrera confirma una pulsación, el evento lleva "confirm":true: la app actualiza esa pulsación en el registro en lugar de añadir una línea
autoel controlador cerró por sí mismo tras el número de segundos configurado

Con final de carrera, el controlador informa opening o closing desde el pulso hasta que el final de carrera confirma la posición. Sin él, informa de inmediato la posición objetivo.

"hold":true pide a la app mantener el mensaje en la tarjeta hasta el siguiente mensaje del controlador; sin él, el mensaje desaparece a los pocos segundos. Los controladores lo ponen en Apertura y Closing (nivel info, la puerta está en camino) y en un fallo.

La respuesta a un comando que realmente puso en marcha el motor lleva "started":true. El servidor espera el final del final de carrera solo para esa pulsación: «Ocupado» o «ya abriendo» ante una segunda pulsación no se apropia del movimiento de otro.

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 ping_s seconds

Si un dispositivo permanece en silencio más de lo que permite el servidor, el servidor lo desconecta y lo muestra sin conexión. El servidor indica el periodo en device_ok.ping_s: un tercio de ese tiempo (DEVICE_OFFLINE_S, 30 s por defecto). Un dispositivo que envía "ping_ctl":true en device_hello lo sigue; los demás hacen ping cada 30 s y quedan sin conexión tras 90 s de silencio. A 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.

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>" [, "action":"close"] }
4. → device_status success
QuiéntokenComprobación
Propietariovacíoproof == HMAC-SHA256(ownerSecret, nonce)[0..15]
Invitado3 bytesverificar 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]

Un cierre de un accionamiento biestable lleva "action":"close" y se firma sobre el nonce con los cinco bytes ASCII close añadidos: proof = HMAC-SHA256(key, nonce || "close")[0..15]. Aquí key es el ownerSecret del propietario o el guest_key del invitado. Una apertura se firma sobre el nonce solo. Así el servidor no puede convertir una apertura en un cierre ni al revés: la firma no coincidiría. Decide qué hacer quien firma, según la posición que informó el controlador.

El controlador acepta un nonce durante 30 segundos tras emitirlo y solo en la conexión en la que lo emitió: un comando firmado que el servidor retenga no funcionará después.

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", "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.

El controlador de red ESP32 guarda hasta 96 decisiones sobre invitados. Cuando la lista está llena, una entrada «permitido» deja sitio a una nueva revocación; si las 96 son revocaciones, se descarta la más antigua.

8. Primitivas criptográficas

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==

Cierre (accionamiento biestable)

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. 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.

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.