11.13. 配对与绑定¶
到目前为止介绍的所有内容都是以明文方式在无线电波上传输字节。任何在同一房间内拥有支持 BLE 的笔记本电脑的人,都可以监听广播信道、跟踪开放连接的跳频序列,并读取经过该连接的每一次读取、写入和通知。对于大多数公开的传感器数据(电池电量、环境温度)而言,这没有问题。但对于两个端点希望保密的任何内容——一个控制继电器使能的控制寄存器、一个密码、一个不应被广泛广播的测量值——链路就需要加密,并且理想情况下摄像头需要知道它正在与谁通信。
BLE 通过配对和绑定来提供这两者。
11.13.1. 配对、绑定、加密¶
三个密切相关的概念:
加密 是最终目标。一旦链路被加密,数据信道上的每个数据包就只能被两个端点解读;窃听者看到的只是噪声。
配对 是两个端点为商定加密所使用的密钥而执行的过程。它是一次性的交换,产生共享的密钥材料,链路层将其接入其加密引擎。
绑定 是在配对完成后选择将密钥持久化到非易失性存储中,这样同一对设备之间的下一次连接就会跳过配对,直接进入加密。
用通俗的话说:配对是“相互介绍”;绑定是“记住这次介绍”;加密是“从现在起私下交谈”。
在开放的 BLE 连接之上的配对流程。密钥交换完成后,链路层会对随后的每个数据包进行加密。绑定是将密钥写入闪存的额外步骤。¶
11.13.2. LE Secure Connections¶
BLE 使用的现代密钥交换是 LE Secure Connections,它构建于椭圆曲线 Diffie-Hellman 之上。双方各自生成一个临时密钥对,交换公钥部分,并将结果与各自的私钥结合,从而得出相同的共享密钥——即使窃听者掌握了整个交换过程的完整记录,也无法计算出这个密钥。
较旧的 LE Legacy 方法不太安全(掌握完整交换过程的窃听者通常可以恢复密钥),它存在的唯一目的是为了向后兼容旧的外设。aioble 默认使用现代方法(le_secure=True);请保持这一设置。
11.13.3. 发起配对¶
central 通过在一个已经打开的连接上调用 aioble.DeviceConnection.pair() 来进行配对:
async with await device.connect() as connection:
await connection.pair(bond=True, le_secure=True, mitm=False)
# ... GATT work, now over an encrypted link ...
pair 返回后,连接上的 encrypted、authenticated、bonded 和 key_size 属性会反映出协商的结果。
四个最有用的关键字参数:
bond=True—— 将生成的密钥保存到闪存,这样同一对设备之间的下一次连接就会跳过配对握手。默认值为True。le_secure=True—— 使用 LE Secure Connections。默认值为True。保持开启。mitm=False—— 是否要求中间人(man-in-the-middle)防护。这需要一个带外信道(一侧显示并由另一侧确认的数字代码、键入的口令密钥等),以便用户能够验证参与配对握手的两个设备确实是他们所认为的那两个。默认值为False(无 MITM 防护——被动窃听者无法读取链路,但主动重定向连接的攻击者可能将自己配对进来)。对于任何敏感内容,应设置为True,但要注意这要求外设确实支持某种 IO 能力。io=3—— 设备所声明的 IO 能力。蓝牙规范定义了五种:0仅显示、1显示 + 是/否、2仅键盘、3无输入无输出、4键盘 + 显示。没有 UI 的摄像头通常报告3;如果摄像头本身带有显示屏,应用程序可以显示数字确认并使用1。双方 IO 能力的组合决定了是否能够实现真正的 MITM 防护。
外设自己不会调用 pair —— 它们只是响应 central 所发起的任何操作。某个特征是否要求加密,是它在 GATT 数据库中被声明方式的一个属性;要求加密的访问位属于底层 bluetooth API 的一部分,目前没有通过 aioble 的特征构造函数暴露出来。
11.13.4. 绑定——以及密钥存储在何处¶
当 bond=True 时,aioble 会将密钥写入本地文件系统上的一个 JSON 文件。默认文件名为 ble_secrets.json,相对于当前工作目录写入。在一台刚启动的摄像头上,_boot.py 已经选定了该目录:挂载了存储卡时为 /sdcard,否则为 /flash——因此文件会落在 /sdcard/ble_secrets.json 或 /flash/ble_secrets.json。该文件保存了下次绑定对端重新连接时重新加密链路所需的条目,包括对端的身份地址。
需要记住一个不对称之处:随着密钥变化,保存会自动进行,但在下次启动时加载该文件却不会。请在启动时(在任何配对或广播之前)调用一次 aioble.security.load_secrets(),以便先前绑定过的对端能够被识别:
import aioble
aioble.security.load_secrets() # default path: ble_secrets.json
在那之后,下次有绑定过的对端出现时,aioble 会重用存储的密钥,链路无需进一步握手即可进入加密状态。
在闪存上存储密钥会带来两个实际后果:
遗忘某个设备。 删除
ble_secrets.json(或移除相关条目)即可遗忘所有绑定的对端,然后从头重新配对。物理访问会泄露密钥。 任何能访问摄像头文件系统的人都可以读取该 JSON 文件。这与网络方面在处理 TLS 密钥时出现的约束属于同一类(运维:密钥、过期与故障排查):使用每设备独立的密钥、将任何存储的密钥都视为可恢复的,并依赖于撤销能力(在这里是在 central 一侧移除绑定),而不是依赖于密钥保持机密。
11.13.5. 加密保证了什么——以及不保证什么¶
一个先配对后加密的链路按强度由强到弱提供了:
机密性。 始终如此。窃听者无法读取这些字节。
完整性。 始终如此。被修改的数据包会通不过链路层的认证加密校验而被丢弃。
认证。 仅在
mitm=True且具备相应 IO 能力时才成立。如果没有它,一个拦截了原始配对交换的中间人可能已经把自己插了进来;在没有 MITM 防护的情况下,双方无从知晓这一点。
对于大多数摄像头使用场景——手机与摄像头配对一次,之后再次连接——mitm=False 通常就足够了,因为原始配对发生在受控环境中(用户在同一房间内同时持有两台设备)。对于配对设备可能首次在远距离或通过不受信任的中间方接触摄像头的应用而言,MITM 才是正确的设置。
11.13.6. 何时配对是错误的答案¶
配对有实实在在的成本:首次连接时几秒钟的交换、为每个绑定设备持续占用的闪存,以及一旦出错时“遗忘绑定”的恢复方案。对于真正公开的数据——以信标形式发布的环境传感器读数、显示自己名称的标牌、任何不会因被读取或写入而改变世界的内容——正确的答案是根本不要加密,让任何附近的扫描器都能读取这些值。
对于其他所有情况,在 central 上调用 connection.pair(bond=True) 就是把链路从公开信道变成私密信道的那一行式补充。