端到端加密
bleRPC 支持可选的端到端加密,使用 4 步密钥交换握手和 AES-128-GCM 会话加密。加密在连接建立期间协商,对应用层透明。
加密组件
| 组件 | 算法 |
|---|---|
| 密钥协商 | X25519 ECDH |
| 认证 | Ed25519 Signatures |
| 密钥派生 | HKDF-SHA256 |
| 会话加密 | AES-128-GCM |
| 重放保护 | 单调递增计数器 |
能力协商
加密由 peripheral 在 CAPABILITIES 响应中广播。6 字节载荷包含 flags 字段:
如果 flags & 0x01 被设置,peripheral 支持加密,central 应发起密钥交换握手。
密钥交换握手
握手使用 4 个步骤,每个步骤都在 KEY_EXCHANGE 控制容器(control_cmd=0x6)中传输。每个载荷以 1 字节步骤标识符为前缀。
步骤 1:Central → Peripheral(33 字节)
Central 生成临时 X25519 密钥对并发送其公钥
步骤 2:Peripheral → Central(129 字节)
Peripheral 生成自己的临时 X25519 密钥对,计算共享密钥,派生会话密钥,并使用其长期 Ed25519 密钥签名两个公钥
签名覆盖 central_x25519_pubkey || peripheral_x25519_pubkey(64 字节),将双方的临时密钥绑定在一起以防止中间人攻击
步骤 3:Central → Peripheral(45 字节)
Central 验证签名,计算共享密钥和会话密钥,然后发送加密确认以证明它持有正确的会话密钥
明文是固定的 16 字节标记(BLERPC_CONFIRM_C),使用派生的会话密钥和随机 12 字节 nonce 通过 AES-128-GCM 加密。Peripheral 解密后检查其是否与预期标记完全一致,从而证明双方持有相同的会话密钥
步骤 4:Peripheral → Central(45 字节)
Peripheral 解密步骤 3,验证其与预期的 BLERPC_CONFIRM_C 标记一致,然后发送自己的加密确认,其中包含 BLERPC_CONFIRM_P
Central 成功解密步骤 4 后,握手完成,双方共享相同的会话密钥
密钥派生
会话密钥从 X25519 共享密钥通过 HKDF-SHA256 派生:
shared_secret = X25519(my_privkey, peer_pubkey) # 32 bytes
session_key = HKDF-SHA256(
salt = central_x25519_pubkey || peripheral_x25519_pubkey, # 64 bytes
ikm = shared_secret,
info = b"blerpc-session-key",
len = 16 # AES-128 key
)
Central 和 peripheral 都使用两个 X25519 公钥的拼接作为 salt 执行相同的派生,从而得到相同的 16 字节会话密钥
会话加密
握手完成后,所有命令载荷在容器分割之前进行加密:
加密载荷格式
加密过程
- 序列化 CommandPacket(类型 + 名称 + protobuf 数据)
- 使用当前 TX 计数器构造 12 字节 nonce:
[counter_LE:4][direction:1][0x00:7]—direction为 Central→Peripheral 时是0,Peripheral→Central 时是1 - 使用 AES-128-GCM 加密:
ciphertext, tag = AES-GCM(session_key, nonce, plaintext) - 前缀添加当前计数器:
[counter:4][ciphertext:N][tag:16] - 为下一条消息递增 TX 计数器(因此会话中的第一条消息使用计数器
0) - 按常规方式分割到容器中
解密过程
- 将容器重新组装为完整载荷
- 提取计数器(前 4 字节)、密文和标签(最后 16 字节)
- 验证计数器 > 上次接收的计数器(重放检测)
- 构造 nonce 并使用 AES-128-GCM 解密
- 将解密的数据解析为 CommandPacket
重放检测
每个方向维护一个独立的单调递增计数器:
- TX 计数器:从 0 开始。每条消息使用当前值加密,随后该值递增 — 因此第一条消息使用计数器 0
- RX 计数器:跟踪上次接收的计数器。任何计数器 ≤ 上次接收计数器的消息将被拒绝
这可以防止重放攻击,即攻击者重新发送之前捕获的加密数据包
信任模型
首次使用信任 (TOFU)
首次连接时,客户端会固定(pin) peripheral 的 Ed25519 公钥。在之后的每次连接中,它会验证 peripheral 是否提供相同的密钥,若密钥发生变化则拒绝连接。自 v0.7.0 客户端库起,该固定默认启用且采用 fail-closed,在密钥交换期间自动执行;应用只需提供一个用于持久化已固定密钥的小型存储(Android SharedPreferences、iOS UserDefaults、AsyncStorage 或文件)
这类似于 SSH 的 known_hosts 机制。它在首次连接后提供中间人攻击保护,无需 PKI 或证书颁发机构
平台实现
| 平台 | 加密库 | 密钥交换 | 会话加密 |
|---|---|---|---|
| Python | cryptography | CentralKeyExchange / PeripheralKeyExchange | BlerpcCryptoSession |
| Kotlin (Android) | Java Crypto | CentralKeyExchange | BlerpcCryptoSession |
| Swift (iOS) | CryptoKit | CentralKeyExchange | BlerpcCryptoSession |
| Dart (Flutter) | cryptography | CentralKeyExchange | BlerpcCryptoSession |
| C (Zephyr) | PSA Crypto (mbedTLS) | blerpc_central_kx_* / blerpc_peripheral_kx_* | blerpc_crypto_session |
配置
启用加密
加密在 peripheral 端启用。启用后,peripheral 在其能力中广播加密支持,并期望 central 发起密钥交换
Zephyr 固件
# prj.conf
CONFIG_BLERPC_ENCRYPTION=y
Python Peripheral
# server.py - encryption is enabled when an Ed25519 signing key is provided.
# X25519 keypairs are generated ephemerally for each connection.
peripheral = BlerpcPeripheral(
ed25519_private_key_hex="...",
)