socket 协议规范
为您自己的控制器加上 Entrixy 联网模式所需的一切:WebSocket 消息、端到端加密和测试向量。 我们自己讲这套协议的板子属于 NET 系列;只走蓝牙的属于 BLE 系列。
device_key ,见 §10。参考: esp32-ws-example (MIT).
1. 传输与消息封装
- Endpoint:
wss://entrixy.com/ws(hostentrixy.com,端口 443,路径/ws)。在开放模型下,主机地址来自密钥或设置——参见 /open。 - TLS: 就是普通的 WSS。不协商任何子协议。
- 帧: 文本(UTF-8)JSON。区分字段为
type。 - 重连: ESP32 参考固件中为固定间隔 2 秒。接收看门狗:连续三个 ping 周期没有收到帧(3 × ping_s,默认为 30 秒)→ 重启。
2. 连接与认证
控制器在第一帧中自我介绍:
→ 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 ——设备码的两半,各 32 个十六进制字符(可带可选的 ENTX-DEV:前缀)。服务器会核对 sha256(device_secret) 与哈希是否一致,依据 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
通过互联网工作的控制器自己与服务器保持连接,因此服务器能看到控制器的 IP 地址和发给它的每条命令的时间。只有已绑定到所有者手机的 ESP32 网络控制器会校验每条命令的签名(§5–6)。ESP8266、Raspberry Pi 和 Node.js 控制器信任服务器:服务器可以自行向它们发送开启命令。
3. 开启(基础模式,无端到端加密)
❮ device_command
{ "type":"device_command", "action":"open", "command_id":"<id>", "number_id":<n> }
// action:"close" — for a bistable drive
控制器给继电器一个脉冲(默认 1000 毫秒)并回复:
→ device_status
{ "type":"device_status", "command_id":"<id>",
"level":"success"|"warning"|"danger"|"info",
"message":"...", "final":true
[, "position":"open"|"closed"|"opening"|"closing"|"unknown" ] }
level 不在白名单内的,会被当作 info。应用会原样显示 message 并把它写进日志,紧挨着那次点击。
控制器还会报告它自身发生的事情。这样的 device_status 带有空的 command_id 以及一个 event 字段;应用会把它写进所有者的日志。
→ { "type":"device_status", "command_id":"", "event":"button"|"limit"|"auto",
"level":"success", "message":"Opened", "final":true, "position":"open" }
| event | 何时 |
|---|---|
button | 室内出门按钮被按下;对于双稳态驱动器,它在开启 / 关闭之间切换 |
limit | 限位开关:门被手动移动,或电机在控制器设定的行程时间内未到达按下所指定的位置(level danger, message "Did not close" 或 "Did not open")。限位开关确认某次按下时,事件带有 "confirm":true:应用更新日志中该次按下,而不是新增一行 |
auto | 控制器在设定的秒数后自行关闭 |
有限位开关时,控制器从脉冲开始报告 opening 或 closing ,直到限位开关确认位置。没有限位开关时,它立即报告目标位置。
"hold":true 请求应用在卡片上保留该消息,直到控制器的下一条消息;没有它,消息几秒后消失。控制器在 开启 与 Closing (级别 info,门在运行中)以及故障时设置它。
真正启动了驱动的命令,其回复带有 "started":true。服务器只为这样的按下等待限位开关给出结束:第二次按下得到的“忙”或“正在打开”不会占用他人的动作。
4. 心跳与存活检测
服务器 不会 ping 设备——这种扇出扛不住规模。由设备自己维持连接:
→ { "type":"ping" } ← { "type":"pong" } // the device sends this every ping_s seconds
如果设备沉默超过服务器允许的时间,服务器会断开它并显示离线。服务器在 device_ok.ping_s中告知周期:该时间的三分之一(DEVICE_OFFLINE_S,默认 30 秒)。设备若声明 "ping_ctl":true 在 device_hello 中,则遵循该周期;未声明的设备每 30 秒 ping 一次,沉默 90 秒后显示离线。服务器发出的 ping ,设备会以之回应: pong。对于双稳态驱动装置,还有一路对象状态心跳:设备会自发发送 device_status (不带 command_id ,带 position ),每隔 hb_interval 秒一次。
5. 端到端加密:ownerSecret 的预置
端到端加密确保 服务器无法伪造开门 ——指令由应用签名。 ownerSecret (32 字节)并不烧进固件:由所有者的应用来投递 ,经 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)
执行 PROVISION 或 WIPE 之后,控制器会带着更新后的 e2ee重新连接。已完成预置的控制器会 拒绝 普通的 device_command ——只适用 §6。
6. 端到端加密:挑战—应答式开启
使用 e2ee=true 而不是 device_command 时,走的是挑战—应答。新鲜性来自 一次性随机数,而不是时间:
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
| 谁 | token | 校验 |
|---|---|---|
| 所有者 | 空 | proof == HMAC-SHA256(ownerSecret, nonce)[0..15] |
| 访客 | 3 字节 | 校验 owner_sig,派生 guest_key,校验 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]
双稳态驱动器的关闭命令带有 "action":"close" ,签名对象是 nonce 后接五个 ASCII 字节 close 附加在末尾: proof = HMAC-SHA256(key, nonce || "close")[0..15]。其中 key 是所有者的 ownerSecret 或访客的 guest_key。开启命令只对 nonce 本身签名。因此服务器既不能把开启变成关闭,也不能反过来:签名会对不上。由签名的一方根据控制器报告的状态决定做什么。
控制器只在签发后 30 秒内、且仅在签发它的连接上接受 nonce:被服务器扣留的签名命令之后不会生效。
二进制字段用 base64 表示。随机数为 16 字节,签名截断到 16 字节。HKDF 的 salt=null 为 32 个零字节。
7. 吊销访客
❮ 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]
只有当 version 大于已保存的值时,控制器才会应用它——这是单调递增的,所以服务器既伪造不了,也没法把权限还回去。
ESP32 网络控制器最多保存 96 条关于访客的决定。列表已满时,由一条“允许”记录为新的撤销腾出位置;如果 96 条全是撤销,则删除最早的一条。
8. 密码学原语
- HMAC-SHA256 ——签名;截断时取前 16 个字节。
- HKDF-SHA256 (RFC 5869),
salt=null→ 32 个零字节。 - socket 开门不使用 AES/GCM——只用 HMAC 和 HKDF。(AES-256-GCM 属于另一个子系统,用于加密访客凭据包,与此无关。)
对 guest_key 的派生方式和签名,与 BLE 的访客分支完全一致——密码学部分是复用的。
9. 测试向量(端到端加密)
已知答案的向量。二进制字段同时给出十六进制和 base64;帧里传的是 base64。
ownerSecret (32B) = a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf fingerprint = b358977a = HMAC(ownerSecret,"fp")[0..3] nonce (16B) = 000102030405060708090a0b0c0d0e0f b64 = AAECAwQFBgcICQoLDA0ODw==
所有者开启(空令牌)
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==
关闭(双稳态驱动器)
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. 获取 device_key(面向 OEM)
要让服务器认出设备并知道它归谁,需要这一对: device_key/device_secret,它为服务器所知,并绑定到所有者的账户。
- 做原型或单件,目前的做法: 所有者在应用里新建一个「设备」对象,拿到这对值,配置器把它写进固件。这样就足以把整套协议跑起来并做测试。
- 量产的做法: 认领(claim)模式——设备出厂时不属于任何人,买家用一个码或二维码把它绑到自己账户上。成批的码和标签在 制造商账户中签发:您提交申请,通过后即可生成一批
device_key,拿到装着这些码的 CSV 文件和做好的二维码标签。有关条款的问题请写信至 hello@entrixy.com。
两端的可用实现见 esp32-ws-example (MIT 许可)。要构建 .bin ——参见 浏览器配置器。这在开放架构中的位置: /open。