❮  Entrixy 控制器

BLE 协议规范

为您自己的控制器——您的板子、您的单片机——加上 Entrixy BLE 支持所需的一切。我们现成的固件做的正是这件事;下面给出精确的协议说明和测试向量,方便您逐字节自检。

BLE 控制器 不需要 Entrixy 服务器。它根本不联网:手机直接通过蓝牙与它对话。与主人的绑定在本地完成(X25519 ECDH)。在我们的服务器上注册设备、领取密钥或开通制造商账户,都 不是必需的 ——您可以直接出货空白控制器。参考: esp32-example/entrixy-ble (MIT).

1. 密码学原语

参考实现:mbedTLS(MBEDTLS_ECP_DP_CURVE25519)在 ESP 上,Android 上用 BouncyCastle——两端结果一致。

2. GATT:服务与特征值

所有 UUID 都是 128 位,基于 656e7472-7869-7900-XXXX-426c45000001 (ASCII 的 “entrixy”)。

UUID 后缀属性用途
0000服务 UUID(完整: 656e7472-7869-7900-0000-426c45000001)
0001 NONCEREAD8 字节随机数(遗留方式;现代客户端从广播中取随机数,见 §3)
0002 FIREWRITE开启指令:24 字节(所有者)或 47 字节(访客)
0003 TIMEWRITE时钟校正,正好 26 字节
0004 WIFIWRITE用于 NTP 的 SSID 和密码(可选)
0005 RESULTREAD+NOTIFY每次写入后返回一个字节的结果码(§9)
00f0 PAIR_PUBREAD仅在配对模式下:控制器 32 字节的 X25519 公钥(小端)
00f1 PAIR_DONEWRITE仅在配对模式下:36 字节 [phone_pub 32B][device_id 4B LE]
名称 "entrixy" 与服务 UUID 被放进 scan-response,而不是主包:128 位 UUID 加上厂商数据放不进主包的 31 字节。Android 的被动扫描只能看到主包,所以过滤是基于厂商数据做的(company id 0x00E0).

3. GAP 广播

每隔 sleep_interval 秒,控制器就用厂商专有数据广播一次自己的存在。company ID 为 0x00E0 (在链路上为 E0 00)。该数据块共 23 字节 =2 字节 company ID 加 21 字节数值:

offset  size  field
[0..1]   2    Company ID = E0 00
[2..5]   4    device_id            (LE)
[6..9]   4    counter              (LE, monotonic, anti-replay, survives deep sleep)
[10..17] 8    auth_hmac            = HMAC(owner_secret, mac_in)[0..7]
[18]     1    sleep_interval_s     (plaintext)
[19..22] 4    opts_cipher          (battery, status, fw, hw — encrypted)

签名覆盖 13 字节 (不含 company ID):

mac_in = device_id(4) || counter(4) || sleep(1) || opts_cipher(4)
auth_hmac = HMAC-SHA256(owner_secret, mac_in)[0..7]
这 8 个字节 auth_hmac 充当 nonce ,用于无需往返即可立即开启:没有 owner_secret 的人只会把它们看成随机数,而所有者或访客能算出签名并立刻触发。控制器会保留自己最近签发的 16 个随机数构成的环形缓冲。

选项(opts)——XOR 密钥流

ks = HMAC-SHA256(owner_secret, "ks" || device_id(4 LE) || counter(4 LE))[0..3]
opts_cipher[i] = opts_plain[i] XOR ks[i]
opts_plain[0] = battery %   [1] = status   [2] = fw (major=(b>>4)&0xF, minor=b&0xF)   [3] = hw

双稳态模式

对于双稳态驱动装置,数值后面再追加三个字节,共 26 字节 (见 §8)。

4. 配对

新控制器会自行进入 30–90 秒的配对模式(否则按键进入;长按 5 秒为恢复出厂设置)。整个交换在本地完成,不经服务器:

  1. 手机读取 PAIR_PUB (00f0)——控制器 32 字节的公钥。
  2. 手机生成自己的 X25519 密钥对, shared = X25519(phone_priv, esp_pub).
  3. 双方派生出同一个密钥:
    owner_secret = HKDF-SHA256(salt="entrixy-pair-v1", ikm=shared, info="owner-secret", L=32)
  4. 手机写入 PAIR_DONE (00f1): [phone_pub 32B][device_id 4B LE]. 由手机生成 device_id,控制器把它保存下来。
X25519 密钥采用小端序(RFC 7748)。 owner_secret 绝不会经过空中传输。没有服务器参与:device_id 是应用自己生成的,所以它当然知道。

5. 所有者开启(owner fire)

写入 FIRE (0002), 24 字节:

[nonce 8B][ HMAC-SHA256(owner_secret, nonce)[0..15] 16B ]

nonce ——8 字节的 auth_hmac ,取自最近一次广播。控制器在环形缓冲中核对该随机数,校验签名,给继电器一个脉冲,然后作废该随机数。

6. 访客开启(guest fire)

访客并不知道 owner_secret。所有者通过服务器签发一个凭据包: token, owner_sig, guest_key。写入 FIRE, 47 字节: [token 7B][owner_sig 16B][nonce 8B][guest_sig 16B].

字段长度公式
token7B[bleId 2B LE][valid_until 4B LE unix-sec][flags 1B]
owner_sig16BHMAC-SHA256(owner_secret, token)[0..15]
nonce8B取自广播的新鲜随机数
guest_sig16BHMAC-SHA256(guest_key, nonce)[0..15]
guest_key = HKDF-SHA256(salt=null, ikm=owner_secret, info="guest" || bleId(2B LE), L=32)

控制器:随机数在环内 → owner_sig 用它自己的 owner_secretvalid_until 对照自己的时钟 → 派生出 guest_keyguest_sig。它不保存任何访客名单——只校验签名。

7. 时间同步

访客令牌的有效期需要时钟。任何受信任的客户端都可以带签名地设置时间。写入 TIME (0003), 26 字节:

[bleId 2B LE][epoch_ms 8B LE][ HMAC-SHA256(key, payload[0..9])[0..15] 16B ]

访客时间受三重保护:棘轮机制(不可回拨)、前向上限(每次写入最多 +24 小时)和「先开门后授时」的门槛。外置 RTC 和 NTP 都是可选的。

8. 双稳态状态(K_state)

K_state = HKDF-SHA256(salt=null, ikm=owner_secret, info="ble-state-v1", L=16)
[..21] state_enc = state_byte XOR K_state[counter & 15]     // bit0: 1=open, 0=closed
[22..23] state_mac = HMAC-SHA256(K_state, counter(4B LE) || state_enc)[0..1]   // 2 bytes

9. 响应码(RESULT,特征值 0005)

代码取值代码取值
0x00FIRE OK0x10TIME OK (owner)
0x30FIRE OK — 已开0x11TIME OK (guest)
0x31FIRE OK — 已关0x12TIME 长度不对
0x01随机数已过期或不在环内0x13TIME 的 HMAC 不匹配
0x02有效期已过0x15TIME 回拨了(棘轮)
0x03owner_sig 无效0x16TIME 超过了 +24 小时上限
0x04guest_sig 无效0x17TIME 之前没有近期的开启动作
0x05长度不对0x20WIFI OK
0x06HKDF failed0x21WIFI 格式不对
0x07所有者的 HMAC 无效0x22WIFI 的 HMAC 不匹配

10. 测试向量

确定性的已知答案向量:跑一遍您的实现,逐字节对比。所有数值均为十六进制,字节序与链路上一致。整条链路贯通:配对 → owner_secret → 其余一切。

Pairing (X25519 → owner_secret)

esp_priv   (clamped) = 0002030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f60
phone_priv (clamped) = 201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030241
esp_pub              = 07a37cbc142093c8b755dc1b10e86cb426374ad16aa853ed0bdfc0b2b86d1c7c
phone_pub            = 0d799600f6ffaee2e121e6b8f7a05dc66874b51db3102d0d71f799a09cb4c461
shared_z             = 53126e95ac6e407e8a412fdf82c87f1be45a2251edf9422ad00df2e83aaebd19
owner_secret         = 3609fa67bd15cf2bbaecdd5305feea0d48e1f21d714d01b29082a5fec459c64b
  = HKDF(salt="entrixy-pair-v1", ikm=shared_z, info="owner-secret", L=32)

Advertise (did=0x0A0B0C0D, counter=7, sleep=1, opts=batt100/status1/fw v2.3/hw5)

ks[0..3]     = ee0a8499
opts_cipher  = 8a0ba79c
auth_hmac    = 1f2d8012178a6e06   (= nonce)
mfg    (23B) = e0000d0c0b0a070000001f2d8012178a6e06018a0ba79c

Owner fire (24B)

fire = 1f2d8012178a6e06eaafaae40500580a774b62be6e9b6aa2
  = nonce || HMAC(owner_secret, nonce)[0..15]

Guest fire (47B) — bleId=0x0042, valid_until=0x6890ABCD, flags=0x01

token     (7B)  = 4200cdab906801
owner_sig (16B) = faa3a99a76dfba0521fb4eb4bd508559   = HMAC(owner_secret, token)[0..15]
guest_key (32B) = 248b57d94ccafeb3cba598c6b81fc18617c1b54da1386ca1ed99bcd5dd012ff5
  = HKDF(salt=null, ikm=owner_secret, info="guest"||bleId_LE, L=32)
guest_sig (16B) = 7a6e02ae269827b78b7c49840c40476e   = HMAC(guest_key, nonce)[0..15]
fire      (47B) = 4200cdab906801faa3a99a76dfba0521fb4eb4bd5085591f2d8012178a6e067a6e02ae269827b78b7c49840c40476e

Time-sync (26B) — owner (bleId=0), epoch_ms=1750000000000

time = 000000dc2074970100005e1120cdb0bb4c8f5ee22f2b1920cb99
  = [bleId 2B LE][epoch_ms 8B LE][ HMAC(owner_secret, payload)[0..15] ]

K_state (16B)

K_state = 19bcfc41f61a103d79e9967c21403a93
  = HKDF(salt=null, ikm=owner_secret, info="ble-state-v1", L=16)

参考代码与联系方式

两端的可用实现见 esp32-example/entrixy-ble (MIT 许可)。要构建 .bin 以适配您自己的板子,请使用 浏览器配置器。联网传输方式见 socket 协议。这在开放架构中的位置: /open。OEM 咨询: hello@entrixy.com.