❮  Controladores Entrixy

Especificação do protocolo de socket

Tudo o que é preciso para acrescentar o modo online do Entrixy ao seu próprio controlador: as mensagens WebSocket, a cifra de ponta a ponta e os vetores de teste.

Ao contrário de BLE, um controlador de socket mantém uma ligação permanente ao servidor Entrixy por WebSocket e pode ser aberto de qualquer sítio. O modo básico está completamente descrito nesta especificação. Para a cifra de ponta a ponta, em que o servidor não pode forjar uma abertura, ver §5–7. A obtenção de um device_key para produção em série é explicada no §10. Referência: esp32-ws-example (MIT).

1. Transporte e envelope da mensagem

O firmware de referência para o ESP não fixa (pin) o certificado do servidor por predefinição. Em produção, fixe-o por host ou por CA; o material está disponível a pedido (hello@entrixy.com).

2. Ligação e autenticação

O controlador apresenta-se na primeira 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 — as duas metades hex de 32 carateres do código do aparelho (com um ENTX-DEV:prefixo opcional). O servidor verifica sha256(device_secret) contra o 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. Abertura (modo básico, sem E2EE)

❮  device_command
{ "type":"device_command", "action":"open", "command_id":"<id>", "number_id":<n> }
  // action:"close" — for a bistable drive

O controlador dá um impulso ao relé (1000 ms por predefinição) e responde:

→ device_status
{ "type":"device_status", "command_id":"<id>",
  "level":"success"|"warning"|"danger"|"info",
  "message":"...", "final":true
  [, "position":"open"|"closed"|"unknown" ] }

level fora da lista branca é tratado como info.

4. Heartbeat e deteção de vida

O servidor não faz ping a os aparelhos — esse leque não escala. É o aparelho que mantém a ligação viva:

→ { "type":"ping" }    ← { "type":"pong" }     // the device sends this every ~30 s

Se um aparelho ficar calado mais de cerca de 90 s, o servidor desliga-o. Um ping do servidor é respondido pelo aparelho com pong. Para um motor biestável há um heartbeat do estado do objeto: um device_status sem command_id com position a cada hb_interval segundos. O servidor pode alterá-lo: ❮  { "type":"hb_interval", "seconds":<N> }.

5. E2EE: aprovisionamento do ownerSecret

A cifra de ponta a ponta garante que o servidor não pode forjar uma abertura — o comando é assinado pela aplicação. ownerSecret (32 bytes) não vai gravado no firmware: é a aplicação do proprietário que o entrega pela porta 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)

Depois de PROVISION ou WIPE o controlador liga-se de novo com um e2ee atualizado. Um controlador aprovisionado rejeita um simples device_command — aplica-se apenas o §6.

6. E2EE: abertura por desafio-resposta

Com e2ee=true em vez de device_command faz-se um desafio-resposta. A frescura vem de um nonce de uso único, não do tempo:

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
QuemtokenVerificação
Proprietáriovazioproof == HMAC-SHA256(ownerSecret, nonce)[0..15]
Convidado3 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]

Os campos binários vão em base64. O nonce tem 16 bytes e as assinaturas são truncadas a 16. O salt=null do HKDF são 32 bytes a zero.

7. Revogar um convidado

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

O controlador só o aplica quando version é maior do que o guardado — é monótono, pelo que o servidor não o pode forjar nem devolver o acesso.

8. Primitivas criptográficas

A derivação de guest_key e as assinaturas são idênticas ao ramo de convidado de BLE — a criptografia é reutilizada.

9. Vetores de teste (E2EE)

Vetores com resposta conhecida. Os campos binários são dados em hex e em base64; nas tramas vai base64.

ownerSecret (32B)  = a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf
fingerprint        = b358977a   = HMAC(ownerSecret,"fp")[0..3]
nonce (16B)        = 000102030405060708090a0b0c0d0e0f   b64 = AAECAwQFBgcICQoLDA0ODw==

Abertura do proprietário (token vazio)

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. Obtenção de um device_key (para OEM)

Para que o servidor reconheça um aparelho e saiba de quem é, é precisa a dupla device_key/device_secret, conhecida do servidor e ligada à conta do proprietário.

Uma implementação funcional dos dois lados é esp32-ws-example (MIT). Para compilar .bin — ver configurador no navegador. Como isto se enquadra na arquitetura aberta: /open.