ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

蓝牙5.4安全广播实战:EAD加密与GATT安全级别特征解析

2026/9/9 22:05:31 拓冰建站 浏览量
蓝牙5.4安全广播实战:EAD加密与GATT安全级别特征解析 蓝牙5.4核心规格里真正让广播链路发生质变的不是PHY速率而是三个容易被拆开讲的东西加密广播数据、LE GATT安全级别特征、广播编码选择。我在做一个低功耗蓝牙信标项目时正好把这三块完整过了一遍从协议栈底层到业务逻辑踩了不少坑也把Core Spec 5.4相关章节翻了好多遍。这篇就按我在项目里实际推进的顺序把这三个特性怎么配合、怎么用、怎么避坑一次说清楚。这个内容更适合谁看我的判断是正在做低功耗蓝牙传感器、电子货架标签、室内定位信标或者任何“不想让广播数据裸奔”的嵌入式开发者以及那些被蓝牙配对安全等级问题折腾过的协议栈工程师。如果你只是做App层开发也可以看第二和第三章的连接部分能帮你少走很多弯路。1. 蓝牙5.4的这组改动把广播从通知变成了可用业务1.1 广播数据为什么一直处于裸奔状态传统BLE广播数据的格式非常简单一个广播包Payload里塞一串AD Structure每个AD Structure由Length、AD Type、Data三部分组成。问题是这些东西从射频出去之后完全是明文。手机上的nRF Connect、LightBlue这类工具打开扫描任何自定义字段都一览无余。过去很多开发者怎么处理要么把广播内容做得让人看不懂比如用我们自己定义的字节序和位运算把温湿度、电池电压塞进几个字节里。这种做法顶多叫“混淆”不叫“加密”。只要有心人抓包分析一段时间格式就能被逆向出来然后伪造一个一模一样的信标广播你的网关根本分不清哪个是真设备。所以在蓝牙5.4之前广播链路的定位非常尴尬适合做发现、做通知但没办法承载真正有业务价值的数据。所有敏感数据都只能走GATT连接。可GATT连接不是万能的尤其是电池供电、需要秒级上报的设备连接一次的功耗可能抵得上几十次广播。这也正是5.4加密广播数据EAD出现的主要原因让广播载荷在物理层和链路层的格式不变的前提下内容做到机密性和完整性保护。1.2 EAD、GATT安全级别特征、编码选择是一条链路不是三个孤立功能我前期看资料时最大的误判是把这三个特性拆开理解。加密广播数据关注“广播内容怎么加密”GATT安全级别特征关注“连接通道的安全等级怎么声明”广播编码选择关注“用哪种PHY发送广播”。看起来毫无关联实际用起来是一条链路。以电子货架标签举例。标签平时用可连接广播做存在性发报价格数据通过加密广播下发保证顾客用手机扫描也解不出来。但加密广播需要一个SessionKey这个Key不能直接写在固件里否则逆向固件就等于破解所有设备。更稳妥的办法是首次部署时让标签进入GATT连接模式通过加密连接把SessionKey写进标签。那么问题来了这条连接本身够不够安全GATT安全级别特征就是干这个的App在连接后先读这个特征看看标签是不是只接受认证加密再决定要不要继续当前的安全协商流程。至于广播编码选择是因为加密广播解出来还需要接收端扫得到。如果标签部署在仓库深处广播需要传50米普通1M PHY可能不够Coded PHY更合适。5.4把“广播用哪种编码”变成可控配置后我们终于可以在同一个设备上根据广播事件类型灵活切换而不是为了兼容性把所有广播都绑死在1M PHY上。1.3 哪些项目最值得吃这波更新不是所有项目都要立刻上。我梳理了一下下面几类项目收益最大电子货架标签/电子价签价格数据通过加密广播下发顾客扫到也不怕关键还能远程批量更新工业传感器节点秒级上报温度、振动、防拆状态不想给别人抓包分析产线规律医疗/健康一次性设备无连接上报数据但要求隐私合规广播内容不能明文防伪溯源标签动态Token通过EAD广播扫描端拿到Token后到云端验真防复制。如果只是室内定位信标广播内容只有一个固定ID那EAD带来的收益有限重点反而是广播编码选择和功耗的平衡。2. 加密广播数据EAD的包格式和加解密实现2.1 EAD在AD Structure里的位置EAD不是一个新广播类型它本质上就是把一个“加密后的AD Structure列表”再包成一个AD Structure放进原来的广播Payload里。它在广播包里的位置和普通AD Structure完全一样头部还是Length和AD Type只是AD Type变成了加密数据类型Assigned Numbers里分配了0x31。typedef struct { uint8_t length; // 整个EAD结构长度包含类型、IV、随机数、密文和MIC uint8_t type; // 0x31Encrypted Data uint8_t iv[4]; // 4字节初始向量 uint8_t randomizer[4]; // 4字节随机数 uint8_t encrypted_data[]; // 密文长度可变 uint8_t mic[4]; // 4字节消息完整性校验 } ead_ad_structure_t;解密之后的encrypted_data应该是一段完整的、由普通AD Structure组成的明文列表。举个例子你可以把原本打算公开广播的0x16 Service Data字段改成放在EAD里面扫描端先解密EAD再按普通AD Structure去解析Service Data。这样做的好处是现有上层解析逻辑不用大改只要在解析广播包时先做一层EAD解包。这里有个很容易被忽略的细节EAD的AD Length字段长度包含的是从AD Type开始一直到MIC结束的全部字节。有些协议栈在解析时会把MIC当成下一个AD Structure的Length导致广播包后面出现一截无法解析的数据。所以如果你要自己写解析器一定要先按EAD结构把IV、Randomizer、密文、MIC拆出来再把剩余数据当普通AD处理顺序不能反。2.2 AES-CCM的SessionKey和Nonce处理EAD加密算法用的是AES-128-CCM。这是一个带完整性校验的认证加密模式可以同时保证机密性和完整性。用生活化的方式理解普通加密像把信塞进信封别人看不到内容但可以偷换CCM相当于信封上还贴了一道防伪封条试图篡改会被发现。这里最关键的是两样东西SessionKey和Nonce。SessionKey是128位对称密钥广播端和扫描端必须共享。如果不知道SessionKey即使收到完整EAD包也解不出任何内容。Nonce是一个加密时使用的唯一值用来防止同样的明文在不同时刻加密出同样的密文。EAD构造Nonce时会用到IV和Randomizer。我看规范时特别注意了它的要求每次广播的IV必须变化Randomizer也必须重新随机生成不能让两条广播消息共用同一组Nonce。否则攻击者可以通过密文对比做重放或猜测CCM的安全性就打了折扣。在BLE里同一组IV和Randomizer如果被子广播Subevent复用就会引发解密失败。我之前在调试时踩过一次协议栈为了省电在同一个广播事件里连续发送几个相同内容的EAD包结果IV没有递增扫描端在同一个广播事件中收到重复包Nonce判定失败后一个包被当成重放包丢弃。后来把IV按广播事件递增问题才解决。2.3 广播端构造EAD的完整流程我习惯把加密广播当成一个“加壳”流程顺序如下准备要广播的业务AD Structure明文列表例如一个0x16 Service Data字段里面放温度、批次号、随机Token生成4字节IV和4字节Randomizer确保与上次广播不同用SessionKey和由IV、Randomizer等构造的Nonce调用AES-128-CCM加密得到密文和MIC组装EAD AD Structure先填AD Type0x31再依次填IV、Randomizer、密文、MIC将EAD AD Structure和其他明文AD Structure如Flags、Manufacturer Specific一起塞进广播Payload。伪代码大概是这样的uint8_t plaintext[] { 0x03, 0x16, 0xAA, 0x01, 0x1F, // 业务数据 }; ead_packet_t ead; fill_iv_randomizer(ead, iv, randomizer); aes_ccm_encrypt(session_key, nonce, plaintext, sizeof(plaintext), ead.encrypted_data, ead.mic); // 组装完整广播Payload build_ad_payload(adv_data, adv_len, ead);实际项目中业务数据往往还要加一个递增计数器。这个计数器会让每次广播的明文都不同即使IV和Randomizer被协议栈复用也能多一层防重放保障。加密本身不解决重放问题防重放需要应用层参与。2.4 扫描端解密EAD的流程和失败定位扫描端解析EAD时需要主动识别AD Type0x31然后按结构拆字段再用SessionKey进行CCM解密和MIC校验。如果MIC校验失败就说明密文在传输过程中被篡改或者SessionKey不对又或者是Nonce构造方式与广播端不一致。我整理过一份排查清单实际调试时特别有用广播端和扫描端的Nonce构造规则是否一致字节序是否一致SessionKey是否真的匹配注意很多SDK会把128位Key打印成32个十六进制字符大小写不同都会导致解密失败MIC放在EAD结构的最后4字节如果协议栈自动补齐了广播包的对齐字节也会导致MIC提取位置偏移扫描端是否把同一个设备的历史广播包和当前广播包混在一起造成重放窗口判断错误。排查思路不要一上来就怀疑算法先在协议栈日志里对照广播端和扫描端的IV、Randomizer。如果两者相同却解不出来问题大概率在Key或Nonce。如果两者不同那就要看扫描端有没有丢包或者两个设备的时间基准是否偏差过大。3. LE GATT安全级别特征把安全策略变成可查询的元数据3.1 没有这个特征之前连接后的安全协商是“黑盒”GATT连接本身不包含安全协商配对和加密是独立机制。传统做法是客户端连上来之后服务端根据它自己的安全策略决定是否拒绝访问。问题在于这个安全策略对客户端是个黑盒。客户端只有在发起读写请求被拒绝之后才知道“哦原来这个服务需要加密才能读”。更麻烦的是降级攻击。攻击者可以伪造一个中间设备让客户端以为服务端只支持无加密连接从而诱导客户端降级到No Security。如果有一个特征能让服务端把自己支持的安全级别明文写出来客户端在发起关键请求之前就能先读到沟通成本会低很多。蓝牙5.4新增的LE GATT安全级别特征就是干这个的。3.2 特征值编码与安全模式1 Level 1到Level 4的对应关系这个特征的值并不复杂本质上就是告诉客户端“我这台设备最高/最低支持到什么安全级别”。参考BLE传统安全模式可以这样理解和编码特征值安全层级含义0x01Security Mode 1 Level 1无加密、无认证0x02Security Mode 1 Level 2未认证配对加密0x03Security Mode 1 Level 3已认证配对加密0x04Security Mode 1 Level 4LE Secure Connections 已认证加密需要说明的是具体特征值定义以Core Spec 5.4原文和SIG分配的UUID为准不同协议栈可能还有自己的扩展字段。但逻辑上是同一个思路客户端读到0x04就知道这台设备要求SC配对读到0x01就知道设备完全不设防后续如果涉及敏感数据App应该主动拒绝连接或者提示用户。实际上规范中这个特征更倾向于“服务器声明自己所支持的级别”。所以服务端实现时应该返回它能够支持的最高安全级别而不是当前连接已经达到的级别。客户端拿到之后要判断的是“当前这条连接是否达到了这个级别”如果没达到就需要发起配对或加密请求。3.3 客户端怎么用这个特征规避降级攻击连接建立后客户端第一件事就是查找并读取这个特征。读的时候要注意特征本身可能受服务端安全策略保护所以未加密连接上不一定读得到。那怎么办我的经验是先用默认方式读一次。如果直接被拒绝说明服务端要求加密那就发起配对配对完成后再读一次如果读到Level 3或Level 4确认当前连接已经满足才去操作其他敏感服务。这样能防降级攻击吗能。因为攻击者一旦在中间做代理就无法同时通过服务端的加密认证。客户端读到特征值之后还会验证当前连接确实用了SC配对和加密链路而不是只凭特征值做判断。特征值只是声明真正决定安全性的还是Pairing流程和加密链路本身。我之前遇到过一个场景App连接了一个旧的BLE设备设备固件版本不支持安全级别特征读特征时报Attribute Not Found。这时App不能直接判定“这个设备不安全”因为有些老设备虽然不支持这个特征但实际配对策略是安全的。所以App要做兼容性处理特征不存在时退回原来的安全协商逻辑。3.4 实现中的几个细节如果你是设备端固件开发者添加这个特征有几点要注意。第一特征属性一般设置为Read允许客户端读取但不要开放Write。安全级别声明如果可写攻击者可以直接改成No Security那客户端反而会被误导。第二特征对应的服务UUID和特征UUID一定要按照规范分配不要自己造一个私有UUID放到随便哪个服务里。否则客户端找不到标准服务这个特征就失去了互操作性意义。第三要注意GATT缓存。很多设备端协议栈启动时会给服务端属性赋值如果这个值在运行过程中会变化比如设备刚从无加密模式切换成安全模式要主动发起Service Changed Indication否则客户端复用缓存可能读不到最新值。第四这个特征与EAD的SessionKey分发是天然合作伙伴。设备初次部署时通过加密GATT连接写SessionKeyApp在读安全级别特征确认连接等级后再写Key可以避免Key在弱安全连接上下发。4. 广播编码选择速率、距离、功耗的平衡木4.1 Coded PHY的纠错和速率折算蓝牙5.0引入Coded PHY时大家最关心的是距离。Coded PHY本质上是通过前向纠错FEC把有效载荷中的每个bit扩展成多个symbol接收端可以容忍更多误码。S8时物理层速率等效为125kbpsS2时等效为500kbps。作为对比传统1M PHY的物理层速率是1Mbps2M PHY是2Mbps。速率降下来距离确实远了。我在开阔场地实测同样发射功率下1M PHY广播稳定接收距离大约30米Coded PHY S8能到60米以上环境好的时候接近80米。这很容易让人产生一种误解无脑用Coded PHY就好了。问题在于代价。编码后的广播PDU占用空口时间变长同样长度的广播包发送时间可能是原来的4倍甚至8倍。广播间隔如果不变占空比会大幅上升平均电流跟着往上走。对电池供电设备来说这可能直接导致续航缩水。所以Coded PHY不是免费的午餐它是一个典型的速率、距离、功耗三选二问题。4.2 5.4的Advertising Coding Selection改了什么在蓝牙5.4之前广播端也能配置Coded PHY但很多协议栈把“广播类型”和“PHY类型”绑得过死。比如某个连接事件里广播者想用1M PHY发一个可连接广播同时又想用Coded PHY发另一个不可连接广播配置起来很麻烦甚至只能在启动广播时选一次。5.4对广播编码选择做了机制层面的细化让广播者可以更灵活地指定不同广播事件或广播集对应的编码方式而不是让链路层默默选择。对于应用层开发者的意义在于“这个广播应该用125kbps还是1Mbps”终于变成一个可以显式管理的业务参数而不是只能从SDK示例代码里抄一个固定写法。我在项目里最直接的使用方式是把设备配置成双广播事件一个事件用1M PHY发可连接广播供部署人员近场调试另一个事件用Coded PHY发加密的远距离业务广播供仓库网关扫描。这样既保证了正常业务的距离又不影响现场调试时的连接速度和兼容性。4.3 不同项目的编码选择方案没有万能答案只能按业务权衡。我按常见项目类型整理了一个参考场景广播内容推荐编码主要原因室内信标防篡改Beacon ID1M PHY距离近、时延低、兼容性好电子货架标签加密价格和批次Coded PHY S8货架遮挡多需要穿障碍仓储传感器节点加密温湿度Coded PHY S2或S8距离和功耗平衡可穿戴连接广播连接握手信息1M PHY手机兼容性优先大批量PAwR响应节点状态响应Coded或1M由拓扑决定避免子广播事件之间的干扰里面的逻辑很简单静态场景、长时间部署、距离要求高的优先Coded PHY交互性强、需要频繁连接、周围手机设备多的优先1M PHY如果是点对点高速传输或音频这类高吞吐场景才考虑2M PHY不过广播里2M PHY用得比较少。4.4 实测对比125kbps与1M PHY的信号表现我在一个约2000平米的仓库做过一次对比测试。设备放在货架中段网关放在通道尽头。1M PHY广播时走到距离约35米的位置丢包率开始明显上升有时连续几个广播周期都收不到切换成Coded PHY S8后同一位置基本能稳定收到走到50米以外才开始出现零星丢包。代价也很直观。用电流分析仪测试同样10字节业务载荷、100ms广播间隔1M PHY平均电流约60uACoded PHY S8直接到160uA左右。这个数值受设备射频前端和协议栈实现影响很大但趋势是确定的。后来我把广播间隔从100ms调到200ms平均电流降回到100uA以内距离优势仍然保留。项目上如果续航压力大记得把广播间隔和Coded PHY放在一起调不要单独看PHY参数。另外要提醒一点扫描端也要同步配置Coded PHY。很多手机App底层扫描用的还是1M PHY广播端即使发了Coded广播也扫不到。如果是Android平台ScanSettings里要把PHY类型配成LE_CODED如果是自研网关扫描参数的PHY字段别漏。5. 落地蓝牙5.4安全广播时的避坑清单5.1 协议栈和芯片支持情况要先查蓝牙5.4是规范版本但芯片和协议栈不一定把所有新特性都暴露给上层。EAD的API、GATT安全级别特征的支持、Advertising Coding Selection的配置入口在不同厂商SDK里差别很大。我调研时会先看三件事芯片是否声明了对应的HCI Command/Option支持协议栈Release Note是否提到EADSDK里是否有EAD示例或预编译库。如果SDK没有现成API不要硬改HCI层。EAD涉及密钥管理和Nonce构造自己做容易出错而且不同协议栈对广播包内存管理的处理方式也不一样强行封装很容易踩内存溢出。建议优先找Nordic、TI、Silicon Labs等主流厂商的最新SDK确认版本再动手。5.2 SessionKey分发的“鸡生蛋”问题EAD是加密广播但它不能凭空变出密钥。广播信道本身没有安全通道SessionKey必须通过其他途径预先共享。常见的做法有三种首次连接时通过GATT加密连接写Key通过带外方式注入比如扫码、NFC配对、出厂预置使用云端证书体系设备连接云端后拉取Key。如果你走GATT分发Key这条路就绕不开前面提到的LE GATT安全级别特征。正确流程应该是连接建立读安全级别特征确认当前连接达到预期级别再在这个加密链路上写SessionKey。顺序不能乱。我有一次图省事直接在无加密连接上把Key写进设备结果测试时抓包发现Key以明文出现在GATT Write Request里整个EAD方案形同虚设。5.3 安全级别特征与广播间隔的时序问题设备端如果同时开着可连接广播和EAD广播App连接设备后可能会先收到一堆EAD广播再读到GATT服务列表。这时候安全级别特征还没读到App如果先对敏感服务发起读写请求很可能被安全策略拒绝。优化方法是把安全级别特征放到GATT服务列表前面让客户端在Discover All Primary Services时较早看到它。另一个做法是App侧在连接后不急于操作业务服务先读这个特征。设备端和App端配合好用户就不会遇到“明明连接成功了但下面所有按钮都是灰色”的尴尬。5.4 编码选择对扫描功耗的反向影响这是我和网关团队反复磨合才意识到的问题。大家容易盯着广播端觉得Coded PHY只有广播端在增加功耗扫描端不就开个接收窗口嘛。实际不是。Coded PHY的接收需要扫描端在125kbps模式下持续监听接收窗口要比1M PHY长很多因为每个bit都被扩展了。网关如果同时跟踪几十个Coded广播标签接收功耗和CPU占用都会明显上升。在低功耗大规模部署项目里必须把广播端和扫描端当成一个整体来设计。可以把部分节点配置成Coded PHY关键网关周围节点用1M PHY避免所有节点都在一个慢速信道上挤牙膏。5.4的编码选择如果利用得好是可以动态切换的业务高峰期用Coded PHY保证覆盖业务低峰期切回1M PHY省电。6. 把这三个特性用起来之后的实际感受6.1 一次完整的调试过程我在电子货架标签项目里完整跑通过一次“加密广播安全级别特征Coded PHY”的链路。设备上电后先用1M PHY发一个普通可连接广播App连接设备读取LE GATT安全级别特征确认Level 4后通过加密通道写入SessionKey。写成功后标签切换到Coded PHY开始发送EAD广播。网关在Coded PHY上扫描解密EAD得到价格数据和批次号整个流程跑通。最头疼的问题发生在解密的最后一个环节网关偶尔会把同一个标签的旧广播包和新广播包混在一起。EAD广播包到达网关的时间有抖动网关的接收队列里同时存在两个相同IV的包后一个包被判成重放解密失败。解决方式不是改加密参数而是在应用层维护一个“最近广播时间戳”窗口只有时间戳比上次更新的包才进入解密流程。6.2 后续可以扩展的方向这套方案真正落地后我反而觉得它最大的价值不是“加密”而是让广播数据变成了一个可编程的安全业务通道。后续我准备继续做三件事一是把EAD和PAwR响应结合起来让大批量节点不仅能广播还能安全地回答网关的查询二是在固件升级场景里用加密广播下发Bootloader版本信息防止设备被恶意降级三是把GATT安全级别特征和设备的证书状态绑定让客户端在连接后迅速判断设备是否还在有效期内。最后说一个我自己调试时的习惯不要一上来就全量加密广播。先用明文广播把所有逻辑调通包括SessionKey的正确性、扫描端能否解析、Coded PHY的信号覆盖情况最后一步再切到EAD。加密只是加壳如果里面的业务逻辑还没验证排查问题时会多一层干扰。等整条链路稳定了再放心地把广播壳子换成EAD你会发现蓝牙5.4这套组合拳确实比想象中好用。