上一篇讲钥匙的时候埋了个钩子:重连抓包里,角色切换前出现 Stop Encryption,切换完成后 Resume Encryption 紧跟着一条携带新随机数的 Start Encryption。当时一句话带过,这次把它背后的完整机制摊开。链路加密远不止一个开关,它是一套有仪式感的流程:换钥匙前要先暂停、暂停有暂停的规矩、恢复有恢复的顺序,甚至加密本身还有保质期,到期不换钥匙直接断链。
目录
一、临时钥匙:为了说悄悄话给所有人听
1.1 为什么需要换装
1.2 临时钥匙的分发仪式
1.3 退场也要有规矩
二、加密引擎与它的两位候选人
2.1 E0 与 AES-CCM 的出场规则
2.2 密钥从哪来:E3 与 h3
三、启动加密:一场精心编排的三步舞
3.1 前置谈判:模式和长度
3.2 三步切换:半个身位的艺术
四、暂停与恢复:不停引擎的热换胎
4.1 暂停的两种姿势
4.2 恢复的接力棒
五、密钥的保质期:2 的 28 次方滴答
六、位掩码询价
七、代码消化两处细节
7.1 密钥长度协商的递减循环
7.2 三步切换状态机
八、全景回顾
九、测验
先看这次的主角。重连段的时间线:一条认证、加密三连(模式、长度、启动)之后,分析仪的记录突然出现一串 Unknown LMP Message——不是协议出错,是分析仪读不懂密文了,这正是加密真实生效的铁证。随后 Central 主动停止加密、发起角色切换、切换完成后恢复加密并换入新随机数,盲区再现。一次重连,把加密的启动、生效、停止、恢复四个状态全部演了一遍。
对照主机侧的记录,首连时 Authentication Requested 之后 Set Connection Encryption,算法一栏写着 BR/EDR E0,密钥 16 字节;重连段 Encryption Change 之后紧跟一条 Encryption Key Refresh Complete,恰好对应角色切换后那次恢复加密触发的密钥刷新。两边时间线严丝合缝。
一、临时钥匙:为了说悄悄话给所有人听
1.1 为什么需要换装
正常情况下,每对设备的组合密钥是私有的,广播加密却要求微微网内多个从设备能解开同一份数据。矛盾就此产生:用各家的私有钥匙,广播包谁也解不开;把某家的私有钥匙广播出去,等于出卖了所有点对点链路的安全。规范的解法是给整个微微网发一把公用的临时钥匙,只活过当前连接,广播结束就集体退回各自的半永久钥匙。
先看规范怎么定义三档加密状态:
不加密、只点对点加密、点对点加广播全加密,第三档的入场券就是全员统一临时钥匙加统一随机数。规范还留了一个耐人寻味的注脚:AES-CCM 的设备收不到 E0 加密的广播包;而当微微网里全员支持安全连接时,广播干脆不加密——因为两个加密体系不互通,强加密的广播反而把自家设备挡在门外。
1.2 临时钥匙的分发仪式
流程在 Sequence 41,Central 主导全程:
第一步,Central 用两团随机数过 E22 造出 K_temp,注意不是直接选个随机数当钥匙,规范在 Part H 3.2.8 解释了原因:防止设备随机数发生器质量不佳导致钥匙退化。
第二步,Central 再生成一个随机数 RAND 用 LMP_TEMP_RAND 发出去,双方各自计算掩码 OVL = E22(当前链路钥匙, RAND, 16)。
第三步,Central 把 K_temp 与 OVL 异或后放在 LMP_TEMP_KEY 里明文发送,从设备用手里的 OVL 还原出 K_temp。原文写得很紧凑:
The Central shall then send K_temp protected by XORing with OVL to the Peripheral in an LMP_TEMP_KEY PDU.
和上一篇组合密钥交换的思路一脉相承:用双方共有的秘密做一次一密的异或保护,不暴露任何钥匙本体。不同点在于这次是单向分发,Central 造钥匙发给所有人,而不是双方各出一半。
换装完成立刻互认证确认,第一条认证必须 Central 当验证方,认证失败直接走 detach 流程。整个 4.2.4.1 包括互认证必须是单一事务,而且规范钉死了 transaction ID 必须为 0——因为参与的不止一对 LM,固定 ID 可以避免多设备并发时的事务混乱。
1.3 退场也要有规矩
广播结束,Central 发 LMP_USE_SEMI_PERMANENT_KEY 让大家换回原钥匙,Sequence 42 一问一答就完事。但规范在这里下了一道重手:
If encryption is used on the link, the procedure to go back to the semi-permanent link key shall be immediately followed by the Central stopping encryption... This is to assure that encryption with encryption parameters known by other devices in the piconet is not used when the semi-permanent link key is the current link key.
翻译一下:换回半永久钥匙后,Central 必须立即停止加密,然后按需重新启动。
为什么?临时钥匙期间,微微网里人人都知道加密参数(K_temp 是公开分发的,EN_RAND 是广播的);如果换回半永久钥匙后还沿用旧加密密钥,其他设备理论上还握着解密能力,点对点流量就可能被偷听。停一次加密,等于把所有人手里的旧门禁卡作废,再用新钥匙造新卡。另外一个小规则:当前钥匙类型不许在建链完成前就动手换,顺序不能乱。
二、加密引擎与它的两位候选人
2.1 E0 与 AES-CCM 的出场规则
规范定义了两套加密机制,选择规则和认证那篇如出一辙:
If both devices support the Secure Connections (Controller Support) and Secure Connections (Host Support) features, then AES-CCM shall be used when encryption is enabled. If at least one device does not support both... E0 shall be used.
双方 SC 全开用 AES-CCM,任何一方缺位就退回 E0。木桶效应第二次出现,上一篇讲认证降级,这篇是加密引擎降级,同一个特性位决定两条链路的命运。抓包里对端缺 SC Host,所以主机侧记录的算法是 BR/EDR E0,顺理成章。
两套引擎的基因完全不同。E0 是蓝牙原创的流密码,四个线性反馈移位寄存器加组合逻辑,1999 年定稿;AES-CCM 是 NIST 标准的认证加密,计数器模式加密加 CBC-MAC 校验,一次计算同时保证机密性和完整性。E0 只加密不校验,理论上存在比特翻转攻击的空间;AES-CCM 附带 4 字节 MIC,改一个比特都会露馅。
2.2 密钥从哪来:E3 与 h3
两套引擎的密钥派生函数也各走一路。E0 用 E3,输入是当前链路钥匙、EN_RAND 随机数和一个 96 位的 COF:
COF 的取值分两种情况,规范给出的是条件表达式:当前钥匙是临时钥匙时 COF = Central 的 BD_ADDR 重复两遍,否则 COF 直接取认证时产生的 ACO。为什么临时钥匙场景要换原料?因为临时钥匙分发过程不产生新的 ACO(它不是认证的产物),只能用地址充当绑定材料。这也解释了上一篇的伏笔:认证产生 ACO,ACO 参与造加密密钥,认证与加密的耦合点就在这里。
AES-CCM 用 h3 函数,Part H 7.7.6 的定义:
h3(W, keyID, A1, A2, ACO) = HMAC-SHA-256T(KeyID || A1 || A2 || ACO) 取前 128 位,keyID 是字符串 btak 的 ASCII 编码 0x6274616B。和 h4/h5 一样是 HMAC-SHA-256 家族,输入照样掺双方地址。有个容易忽略的差别:规范明确写了 AES-CCM 场景下 EN_RAND 不参与加密密钥的生成——LMP_START_ENCRYPTION_REQ 里那个随机数只在 E0 里有用武之地,AES-CCM 的密钥换新靠 h3 重新计算。
三、启动加密:一场精心编排的三步舞
3.1 前置谈判:模式和长度
启动加密前有两轮协商。第一轮,加密模式,Central 发 LMP_ENCRYPTION_MODE_REQ,参数 1 表示加密 0 表示不加密,收到 2 当 1 处理(历史兼容)。SC 双方都支持的组合下,模式 0 被直接禁止,想明文传数据没门,回 0x25 Encryption Mode Not Allowed。模式协商还有个隐藏设定:半永久钥匙只加密点对点包,临时钥匙连广播一起加密,和第一节的三档表对上。
模式谈拢后,发起方要先暂停 ACL-U 逻辑链路上的流量再发协商请求,响应方发完 ACCEPTED 也要先停流。原因很朴素:协商期间新旧状态切换,数据包撞上加密引擎正在换钥匙的瞬间就会损坏。规范还设了一条防线:发过加密模式请求的设备,在加密真正启动前不许发 LMP_AU_RAND,违者对方回 0x24 拒绝。还是为了 ACO——认证会产生新 ACO,两边各持不同 ACO 去算加密密钥,密钥就再也对不上了。
第二轮,密钥长度,这是蓝牙历史上出口管制的活化石。每个设备有四个参数:L_max 自己支持的最长、L_min 应用要求的最短,7 到 16 字节之间。谈判算法是个优雅的递减循环:
Central 先出价 L_sug_c = L_max_c。从设备一看,落在自己区间内(L_min_p ≤ L_sug_c ≤ L_max_p),成交;太长了,还价 L_sug_p = L_max_p,即自己能力之内最接近的下限;太短了,直接掀桌子回 0x20,加密作废。Central 收到还价再做同样的判断,循环往复直到成交或谈崩。Part H 4.1 的术语和协议正文完全一致,谈崩的可能性是规范有意保留的:应用程序有权拒绝不够长的密钥,不然恶意设备谎报一个很短的最大长度,就能把全网的加密强度拉下水。
三条硬规则别忘了:四个 L 参数连接期间不许变(防止中途拉皮条);L_min 至少 7 字节;安全模式 4 的服务可以强制要求比协商结果更长的密钥,这时候上层会重新走流程。
3.2 三步切换:半个身位的艺术
模式长度都谈好,Central 生成 EN_RAND,算好加密密钥,发 LMP_START_ENCRYPTION_REQ。从设备收到算好自己的密钥,回 ACCEPTED。看似简单的两次收发,底层却是一场精确到包的三步舞,规范原文写得非常清楚:
Central is configured to transmit unencrypted packets and to receive encrypted packets.
Peripheral is configured to transmit and receive encrypted packets.
Central is configured to transmit and receive encrypted packets.
第一步,Central 进入不对称状态:发不加密、收加密。这个窗口里 Central 发出的 START_ENCRYPTION_REQ 本身还是明文,从设备读完明文指令切换到全加密态。
第二步完成到第三步触发之间,从设备到 Central 方向可通,这条路上走的 ACCEPTED 已经是密文。Central 收到后完成第三步,全链路密文态。
为什么不能一刀切?想象两队人同时换锁,你发的换锁通知对方还没收到,你已经开始发密文,对方读不懂,直接死锁。三步法保证任何时刻至少有一个方向是可读的,指令永远能送达。停止加密走一模一样的三步,只是方向反过来。抓包里 Start Encryption Request 到 Accepted 之间 15ms,三步舞在毫秒级完成。
顺便看首连时的完整三连击,空口记录:
模式、长度、启动三条消息间隔不到 25ms,谈判在两个往返内收官,16 字节满配成交。
四、暂停与恢复:不停引擎的热换胎
4.1 暂停的两种姿势
角色切换、换钥这类操作需要暂时关掉加密,但走完整的停止流程再重启太重——停止会重置加密协商状态,而且 mode=0 在 SC 组合下还被禁着。于是有了轻量级的暂停:只停 ACL-U 数据流量,加密引擎的协商状态原样保留。
暂停的 PDU 分两套:E0 用 LMP_PAUSE_ENCRYPTION_REQ,不带参数;AES-CCM 用 LMP_PAUSE_ENCRYPTION_AES_REQ,自带一个新的 EN_RAND——因为 AES-CCM 恢复时要重新计算密钥,新随机数当场备好。流程上有个精妙的分岔:如果暂停请求的响应方是 Central,它直接回 LMP_STOP_ENCRYPTION_REQ;如果响应方是从设备,它先回 PAUSE 请求,再由 Central 回 STOP_ENCRYPTION_REQ,从设备最后回 ACCEPTED。Sequence 48 到 51 把四种组合(谁发起乘以哪种引擎)全部画了出来:
注意这里的 STOP_ENCRYPTION_REQ 是暂停流程的收尾信号,不是真正意义上的停止加密——名字有点迷惑,但配合三步法看就明白了:暂停也要走三步不对称切换,STOP 请求本身就是第三步的触发器。还有一条铁律:暂停请求不许拒绝,除非撞上事务碰撞需要消解。暂停后想恢复 ACL-U 数据流量只有一条路,走恢复流程,别的门都焊死了。
4.2 恢复的接力棒
恢复加密谁都可以发起,流程按发起方分两种。Central 发起最直接:直接发 START_ENCRYPTION_REQ,从设备算密钥回 ACCEPTED,三步舞再来一遍。
从设备发起要走接力:LMP_RESUME_ENCRYPTION_REQ 递给 Central,Central 回 START_ENCRYPTION_REQ,从设备再回 ACCEPTED。规范特别注明,从设备发起的恢复,三步切换的计时起点不是 Central 收到请求那一刻,而是 Central 发出 START_ENCRYPTION_REQ 那一刻——发令枪在 Central 手里,节奏必须听它的。
E0 的恢复沿用旧参数,密钥不变;AES-CCM 的恢复如果在换钥流程之后,要用 h3 重算新密钥。回看抓包图 的最后一幕:Resume Encryption Request 之后紧跟一条携带新随机数的 Start Encryption Request,这就是从设备发起恢复的标准接力,主机侧那条 Encryption Key Refresh Complete 正是它的回声。
规范还有一条合并优化:如果某个流程要求恢复加密,下一个流程又要求暂停加密,中间的恢复和暂停可以一起省掉。换完钥匙马上又要换随机数的场景,不用来回折腾引擎。另外整段加密流程从模式协商起算单一事务,transaction ID 从 LMP_ENCRYPTION_MODE_REQ 一路沿用到底。
五、密钥的保质期:2 的 28 次方滴答
流密码的安全有个众所周知的软肋:密钥流重复使用等于把明文异或直接暴露。E0 每次加密从密钥派生密钥流,时间一长理论上有碰撞风险;AES-CCM 的计数器也有翻转的一天。于是规范给加密密钥定了保质期,原文:
When E0 encryption is used, the Link Manager shall refresh the encryption key within 2^28 ticks of the Bluetooth clock from the previous start or resume of encryption. When AES-CCM encryption is used, the Link Manager shall refresh the encryption key before either the PayloadCounter or dayCounter roll over.
E0 的期限是蓝牙时钟 2^28 个滴答,一个滴答 312.5 微秒,算下来约 26.2 小时。AES-CCM 看的是包计数器和日期计数器,规范注明翻转至少发生在启动加密后 2^38 个滴答,量级更长。刷新手段就是暂停加立即恢复:If the encryption key has not been refreshed before the rollover event, the link shall be terminated immediately——到期没换钥匙,立即断链,没有商量。安全协议里,密钥流重复暴露的风险被认定高于断连的代价,宁可错杀。
换钥匙或换随机数的通用规则也在这一带:必须暂停加密,完成后恢复;如果双方有一方不支持暂停特性,退化为停止加重启。抓包里角色切换后的那段 stop/resume,正是这条规则在真实世界的执行:角色变了,加密参数跟着换新,顺便把密钥刷新的账一起结了。
六、位掩码询价
最后一个小流程。Central 想提前知道从设备支持哪些加密密钥长度,发 LMP_ENCRYPTION_KEY_SIZE_MASK_REQ,从设备回一个 16 位的位掩码:最低位代表 1 字节,次低位 2 字节,以此类推到 16 字节,置 1 支持,置 0 不支持,7 以下不许支持。这个掩码专门服务于广播加密——临时钥匙要对全网生效,长度必须迁就能力最弱的成员,Central 提前询价就能算出全网交集,避免一个个试错。
七、代码消化两处细节
7.1 密钥长度协商的递减循环
谈判逻辑用二十行 Python 就能复刻,重点是还价时取自己能力内最接近的下限:
def negotiate_key_size(L_max_c, L_min_c, L_max_p, L_min_p, sup_p): # sup_p: 从设备支持的长度集合, 例如 {7..16} 或 {16} L_sug = L_max_c # Central 首次出价 turn = "P" # 轮到从设备表态 while True: if turn == "P": if L_min_p <= L_sug <= L_max_p and L_sug in sup_p: return ("agreed", L_sug) # 成交 if L_sug > L_max_p: cand = [x for x in sup_p if x < L_sug] if not cand or max(cand) < L_min_p: return ("failed", None) # 无可还之价 L_sug, turn = max(cand), "C" # 还价: 能力内最接近的下限 else: # L_sug < L_min_p, 对方出价低于底线 return ("failed", None) else: if L_min_c <= L_sug <= L_max_c: return ("agreed", L_sug) if L_sug < L_min_c: return ("failed", None) cand = [x for x in range(L_min_c, L_max_c + 1) if x < L_sug] L_sug, turn = max(cand), "P"谈判最多几个来回就收敛,因为每轮严格递减且值域只有 7 到 16。恶意设备谎报小 L_max 会把自己锁死在弱加密,而对方的应用层底线 L_min 就是反制的筹码。
7.2 三步切换状态机
加密启动/停止/暂停/恢复共用的三步切换骨架:
def start_encryption_steps(central, peripheral): # 步骤1: Central 收方向先进入加密态, 发方向保持明文 central.rx_encrypted, central.tx_encrypted = True, False send(central, "LMP_START_ENCRYPTION_REQ", encrypted=False) # 步骤2: 从设备收到明文指令, 全切换 peripheral.rx_encrypted = peripheral.tx_encrypted = True send(peripheral, "LMP_ACCEPTED", encrypted=True) # 这条已是密文 # 步骤3: Central 收到密文 ACCEPTED, 全切换 central.tx_encrypted = True return "link fully encrypted"精髓在第一步:Central 的接收先于发送进入加密态。任意时刻保证至少一个方向可读,指令链不断裂。把三个布尔反过来赋值就是停止加密,把第三步换成暂停语义就是暂停流程——一套骨架,四种走位。
八、全景回顾
把五篇的内容串成一条完整的链:连接控制搭好物理通道,特性交换摸清双方家底,SC 特性位决定认证与加密各自的档位;配对或 SSP 生成根信任的组合密钥,认证完成身份互认并顺手产出 ACO;临时密钥为广播场景提供公用换装;模式与长度谈判敲定加密参数,E3 或 h3 用链路钥匙加随机数加 COF 或 ACO 现场调制加密密钥;三步舞启动加密,暂停恢复支持热换钥匙,26 小时的保质期逼着密钥定期新陈代谢。每一个环节都有明确的 PDU、明确的顺序、明确的失败出口,协议设计者对并发和时序的洁癖贯穿始终。
抓包是最好的老师。Unknown LMP Message 盲区证明加密真的生效,Stop/Resume 的位置暴露角色切换与密钥刷新的联动,Encryption Key Refresh Complete 记录着新密钥的诞生。下次面对一条无声的链路,先看这些安全流程卡在哪一步,答案往往就藏在时间戳的缝隙里。
九、测验
问题一:为什么蓝牙启动加密要用三步切换,而不是双方同时切换到加密态?
答案:
核心是指令可达性。如果双方同时切换,切换指令本身可能以错误的状态被解读:A 已进入加密态开始发密文,而 B 还在明文态,读不懂密文,也不知道 A 换好了,握手直接死锁。三步切换保证任意时刻至少一个方向是可读的:
第一步 Central 收加密发明文,START_ENCRYPTION_REQ 以明文送达,从设备有充足信息完成切换;
第二步从设备全加密,回的 ACCEPTED 虽是密文但 Central 的接收端已就绪;
第三步 Central 全加密。指令链全程不断裂,任何一步失败都能明确定位。停止、暂停、恢复复用同一骨架,只是三步的方向与语义不同。
问题二:E0 和 AES-CCM 场景下,暂停加密后恢复时密钥的处理有什么区别?为什么?
答案:
E0 恢复沿用现有参数,密钥不变,除非流程显式要求换钥;AES-CCM 有两个关键差异。其一,LMP_PAUSE_ENCRYPTION_AES_REQ 自带新的 EN_RAND,而 E0 的暂停请求不带参数——因为 AES-CCM 的恢复在换钥场景下要用 h3 重新计算加密密钥,新随机数随暂停请求提前备好。其二,规范注明 AES-CCM 的加密密钥生成根本不用 EN_RAND,h3 的输入是链路钥匙、双方地址和 ACO,密钥换新靠 h3 重算而非注入随机数。设计动机:AES-CCM 是认证加密,密钥与计数器、MIC 绑定,恢复时重算密钥可以彻底切断与旧密钥流的关联;E0 是纯流密码,密钥流由密钥加内部状态派生,保留参数延续状态即可,换钥另有 pause-resume 的通用规则覆盖。
问题三:微微网要从临时链路钥匙退回半永久钥匙时,为什么规范强制要求停止加密后再重启,而不是直接换钥匙继续用?
答案:
防止加密参数泄露的延续风险。临时钥匙是为了加密广播而分发给全网成员的,K_temp 的掩码保护只防外部窃听者,微微网内部人人都能解开它,连同配套的 EN_RAND 也对全员可见,由此派生的加密密钥所有成员都握在手里。如果退回半永久钥匙后沿用同一套加密密钥,其他成员仍然持有解密能力,点对点的私密流量就可能被同网设备偷听,半永久钥匙保护点对点流量的设计意图落空。强制停止加密等于宣告旧密钥作废,重新启动时用半永久钥匙加新 EN_RAND 走 E3 生成全新密钥,旧参数彻底失效。这也呼应了规范的安全模型:点对点安全与广播安全是两个隔离域,跨越域边界必须换钥匙。