端到端加密
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 字节步骤标识符为前缀。
sequenceDiagram
participant C as Central
participant P as Peripheral
Note over C: Generate ephemeral X25519 keypair
C->>P: Step 1: [step:1] + central_x25519_pubkey (33 bytes)
Note over P: Generate ephemeral X25519 keypair
Note over P: Compute shared_secret = X25519(priv, central_pub)
Note over P: Derive session_key = HKDF(shared_secret)
Note over P: Sign(ed25519_privkey, central_pub || peripheral_pub)
P->>C: Step 2: [step:1] + x25519_pub + signature + ed25519_pub (129 bytes)
Note over C: Compute shared_secret = X25519(priv, peripheral_pub)
Note over C: Derive session_key = HKDF(shared_secret)
Note over C: Verify signature (TOFU for first connection)
Note over C: Generate confirmation = encrypt(random, session_key)
C->>P: Step 3: [step:1] + encrypted_confirmation (45 bytes)
Note over P: Decrypt confirmation to verify session_key match
Note over P: Generate confirmation = encrypt(random, session_key)
P->>C: Step 4: [step:1] + encrypted_confirmation (45 bytes)
Note over C: Decrypt confirmation to verify session_key match
Note over C,P: Session key established - all subsequent data is encrypted
步骤 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 字节随机数据,使用派生的会话密钥和随机 12 字节 nonce 通过 AES-128-GCM 加密
步骤 4:Peripheral → Central(45 字节)
Peripheral 解密步骤 3 以验证会话密钥,然后发送自己的加密确认
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] - 按常规方式分割到容器中
解密过程
- 将容器重新组装为完整载荷
- 提取计数器(前 4 字节)、密文和标签(最后 16 字节)
- 验证计数器 > 上次接收的计数器(重放检测)
- 构造 nonce 并使用 AES-128-GCM 解密
- 将解密的数据解析为 CommandPacket
重放检测
每个方向维护一个独立的单调递增计数器:
- TX 计数器:每次加密前递增。从 0 开始
- RX 计数器:跟踪上次接收的计数器。任何计数器 ≤ 上次接收计数器的消息将被拒绝
这可以防止重放攻击,即攻击者重新发送之前捕获的加密数据包
信任模型
首次使用信任 (TOFU)
首次连接时,central 接受 peripheral 的 Ed25519 公钥并存储。在后续连接中,central 验证 peripheral 提供的是相同的密钥。如果密钥发生变化,连接将被拒绝(或提示用户)
这类似于 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="...",
)