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

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 el ESP no fija (pin) el certificado del servidor por defecto. En producción, fíjalo por host o por CA; el material está disponible a petición (hello@entrixy.com).

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é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]

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

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.

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.