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. As nossas placas que o falam são a linha NET; as que funcionam apenas por Bluetooth são a linha BLE.
device_key para produção em série é explicada no §10. Referência: esp32-ws-example (MIT).
1. Transporte e envelope da mensagem
- Endpoint:
wss://entrixy.com/ws(hostentrixy.com, porta 443, caminho/ws). No modelo aberto o host vem da chave ou de uma definição — ver /open. - TLS: WSS simples. Não se negoceia nenhum subprotocolo.
- Tramas: JSON de texto (UTF-8). O discriminador é o campo
type. - Religação: intervalo fixo de 2 s no firmware de referência para ESP32. Watchdog de RX: três períodos de ping sem tramas recebidas (3 × ping_s, 30 s com os valores padrão) → reinício.
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, "ping_s":10 } // success; hb_interval in s (10..3600), ping_s — the ping period
← error { "type":"error", "reason":"auth" } // failure → the server closes
Um controlador que funciona pela internet mantém a própria ligação ao servidor, por isso o servidor vê o endereço IP do controlador e a hora de cada comando que lhe é enviado. Só o controlador de rede ESP32 associado ao telefone do proprietário verifica a assinatura de cada comando (§5–6). Os controladores em ESP8266, Raspberry Pi e Node.js confiam no servidor: ele pode enviar-lhes uma abertura por si próprio.
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"|"opening"|"closing"|"unknown" ] }
level fora da lista branca é tratado como info. O app mostra message tal como chega e escreve-o no registo ao lado do toque.
O controlador também informa o que acontece nele por si mesmo. Um device_status assim tem um command_id vazio e um campo event — o app escreve-o no registo do proprietário.
→ { "type":"device_status", "command_id":"", "event":"button"|"limit"|"auto",
"level":"success", "message":"Opened", "final":true, "position":"open" }
| event | Quando |
|---|---|
button | o botão de saída interno foi premido; num acionamento biestável ele alterna abrir / fechar |
limit | o fim de curso: a porta foi movida à mão, ou o motor não chegou dentro do tempo de percurso definido no controlador aonde um toque o enviou (level danger, message "Did not close" ou "Did not open"). Quando o fim de curso confirma um toque, o evento leva "confirm":true: o app atualiza esse toque no registro em vez de acrescentar uma linha |
auto | o controlador fechou sozinho após o número de segundos configurado |
Com fim de curso, o controlador informa opening ou closing do pulso até o fim de curso confirmar a posição. Sem ele, informa a posição pretendida de imediato.
"hold":true pede ao app que mantenha a mensagem no cartão até a próxima mensagem do controlador; sem ele, a mensagem some após alguns segundos. Os controladores o enviam em Abertura e Closing (nível info, o portão está a caminho) e numa falha.
A resposta a um comando que realmente acionou o motor traz "started":true. O servidor espera o fim pelo fim de curso apenas para esse toque: «Ocupado» ou «já abrindo» num segundo toque não se apropria do movimento de outro.
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 ping_s seconds
Se um dispositivo ficar em silêncio por mais tempo do que o servidor permite, o servidor o desconecta e o mostra offline. O servidor informa o período em device_ok.ping_s: um terço desse tempo (DEVICE_OFFLINE_S, 30 s por padrão). Um dispositivo que envia "ping_ctl":true no device_hello o segue; os demais fazem ping a cada 30 s e ficam offline após 90 s de silêncio. A 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.
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>" [, "action":"close"] }
4. → device_status success
| Quem | token | Verificação |
|---|---|---|
| Proprietário | vazio | proof == HMAC-SHA256(ownerSecret, nonce)[0..15] |
| Convidado | 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]
Um fecho de um acionamento biestável leva "action":"close" e é assinado sobre o nonce seguido dos cinco bytes ASCII close no fim: proof = HMAC-SHA256(key, nonce || "close")[0..15]. Aqui key é o ownerSecret do proprietário ou o guest_key do convidado. Uma abertura é assinada sobre o nonce simples. Assim o servidor não consegue transformar uma abertura num fecho nem o contrário: a assinatura não coincidiria. Quem assina decide o que fazer, pela posição que o controlador informou.
O controlador aceita um nonce durante 30 segundos após emiti-lo e apenas na conexão em que o emitiu: um comando assinado retido pelo servidor não funciona depois.
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", "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.
O controlador de rede ESP32 guarda até 96 decisões sobre convidados. Quando a lista está cheia, uma entrada «permitido» dá lugar a uma nova revogação; se as 96 forem revogações, a mais antiga é descartada.
8. Primitivas criptográficas
- HMAC-SHA256 — assinaturas; o truncamento toma os primeiros 16 bytes.
- HKDF-SHA256 (RFC 5869),
salt=null→ 32 bytes a zero. - A abertura por socket não usa AES/GCM — só HMAC e HKDF. (O AES-256-GCM pertence ao subsistema separado que cifra os pacotes de convidado, não a isto.)
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==
Fecho (acionamento biestável)
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. 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.
- Para um protótipo ou uma peça única, hoje: o proprietário cria um objeto do tipo Aparelho na aplicação, recebe a dupla, e o configurador grava-a no firmware. Isso chega para pôr todo o protocolo de pé e testá-lo.
- Para produção em série: o modelo claim — o aparelho sai da fábrica sem dono, e o comprador liga-o à sua conta com um código ou QR. Os lotes de códigos e autocolantes são emitidos na conta de fabricante: faz o pedido e, depois de aprovado, gera um lote de
device_key, recebe um CSV com os códigos e autocolantes QR já prontos. Para perguntas sobre as condições escreva para hello@entrixy.com.
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.