周末有个做植保无人机的朋友来找我,说他们在野外做链路测试的时候,拿个几十块的接收设备就能把地面站和飞控之间跑的 MAVLink 报文抓出来当场解析。航点、GPS 坐标、飞行模式、遥控通道映射,全部明文,读起来就跟翻聊天记录一样轻松。他问我有没有办法把这层窗户纸糊上。
我当时的建议很直接:给链路加一把锁。MAVLink 作为无人机领域事实上的标准通信协议,在很长一段时间里都是裸奔状态。想在不大动干戈的前提下提升安全性,工程上最成熟的做法就是引入 AES-128-GCM 对 MAVLink 报文进行加密封装。这篇文章把 MAVLink 的加密原理、AES-128-GCM 的工作机制,以及两者集成时那些文档里不会写清楚的坑,完整拆一遍。适合正在做地面站、数传模块,或者想给自己的飞控数据链上把锁的开发者参考。
先说一个容易混淆的点:MAVLink 2 原生自带的安全机制叫签名,用的摘要算法是 SHA-256,作用是校验完整性和防重放,它本身不加密数据;AES-128-GCM 是 AEAD 加密算法,作用是“让别人看不到内容,也改不了内容”。所以两者不是替代关系,而是互补关系。下面从 MAVLink 的明文风险讲起,再逐步深入到 GCM 的内部原理和实际集成方案。
1. MAVLink 的通信安全问题到底出在哪
1.1 从 MAVLink 1 到 MAVLink 2,安全能力补了什么
MAVLink 诞生于 2009 年前后,最初是 PIXHAWK 项目里为了解决飞控和地面站通信而设计出来的一套轻量级串行协议。MAVLink 1 的帧结构非常紧凑,总共就那么几个字段:起始符、长度、序列号、系统 ID、组件 ID、消息 ID、载荷和校验。这个设计让它在早期 8 位 MCU 上也能跑得动,但也埋下了一个大隐患:整个协议里没有任何和“安全”相关的字段,也没有任何认证手段。
到了 MAVLink 2,帧结构做了扩展,消息 ID 从 8 位扩到 24 位,新增了不兼容标志位和兼容标志位,最关键的是加入了一套可选的签名机制。有了这套机制,接收端可以确认一条消息确实来自持有密钥的合法设备,也能识别出重放攻击。但要注意,签名只能证明“消息没被改过、确实是对方发的”,它不能阻止其他人直接读取报文内容。换句话说,你的航点坐标在加密之后依然可以被抓包工具看见,只是别人改不动了。
这一点在无人机场景里往往被忽略。很多人以为开了 MAVLink 2 签名就安全了,实际上机密性缺口依然存在。我见过不少项目在调研阶段只看到“MAVLink 2 支持签名”,就直接在预研报告里写上“已实现安全通信”,后面做安全审计的时候被打回来重做。这就是没分清“完整性保护”和“机密性保护”的结果。
1.2 明文传输的三大风险:窃听、伪造、重放
把 MAVLink 报文当成普通文本在无线链路上传输,风险可以归纳成三类:
第一类是窃听。MAVLink 里大量消息都是高价值信息,比如 GPS 坐标、航点、姿态、电池电压、遥控通道值。这些数据一旦被无关第三方拿到,轻则暴露作业位置,重则让整套作业流程被逆向分析。尤其是植保、测绘这类行业无人机,飞行轨迹本身就是商业资产。
第二类是伪造。MAVLink 1 时代伪造一条消息几乎零成本,把帧算好校验位发出去就行。攻击者可以伪装成地面站向飞控发送模式切换指令,把多旋翼从“定高模式”切成“返航模式”或者“降落模式”,直接导致炸机。就算不开飞,伪装成飞控给地面站发伪造的遥测数据,也能让地面站显示完全错误的飞行状态。
第三类是重放。攻击者不需要理解协议内容,只需要在链路上录下一段合法的消息,比如“解锁”指令,然后在适当的时候重新发一遍。如果不做时间戳或序列号校验,飞控和地面站根本分不清这是不是一条新鲜指令。MAVLink 2 的签名机制加入递增时间戳,主要就是为了挡住这种攻击。
1.3 MAVLink 2 签名的能力边界
MAVLink 2 签名机制的具体做法是:在每个需要签名的消息末尾追加 13 个字节,包含 1 字节链路 ID、6 字节 48 位时间戳和 6 字节签名摘要。时间戳按 10 微秒递增,要求每条消息的签名时间戳严格大于上一个,这样就能防止旧消息被重放。签名本身是把消息体、链路 ID、时间戳和密钥拼在一起做 SHA-256 运算,取结果前 6 字节作为摘要。
实现的时候有几个细节要注意。一是密钥长度 32 字节,如果密钥全是零,系统认为签名功能未启用。二是链路 ID 是单字节,用来区分物理链路,不同链路的签名不能混用。三是时间戳的问题,很多飞控设备没有带电池的 RTC,重启后时间戳可能从零开始,这就需要地面站在连接时做同步或容忍一定范围的回退,否则一个简单的重启就可能让所有消息被当成重放丢光。
但回到核心问题:即使开了签名,消息内容仍然是明文。用 Wireshark 的 MAVLink 解析插件照样能一针一针读出来。所以如果你对“保密”有要求,就必须在 MAVLink 协议层之外再套一层加密。下面讲的 AES-128-GCM 就是目前工程上最顺手的选项。
2. AES-128-GCM 加密的基本原理
2.1 GCM 为什么能把“加密”和“防篡改”一起干
AES-128-GCM 是 AES 加密算法在 GCM 工作模式下的一种组合。GCM 全称是 Galois/Counter Mode,它的设计思路很巧妙:基本框架是一个 CTR 模式的流密码,负责加密;同时在 Galois 域上跑一个叫 GHASH 的哈希函数,负责生成认证标签,用于校验完整性。加密和认证共用一个 AES 密钥,一次操作同时搞定机密性和完整性,这也是它被叫做 AEAD 的原因。
CTR 模式的原理可以理解成一个“带编号的遮蔽层”。AES 对一组递增的计数器值做加密,生成一段密钥流,再让明文和密钥流做异或得到密文。因为每个块的计数器不同,即使两段明文内容完全一样,加密出来的密文也不一样,这点比 ECB 模式安全得多。CTR 是流式处理,可以并行计算,在硬件实现上非常高效。
GHASH 部分则是另一种东西。它把密文和额外的附加数据按 128 位分块,在 GF(2^128) 域上做乘法累加,最终得到 128 位认证标签。接收方解密时,会用同样的密钥和逻辑重新计算标签,如果和发送方附带的标签不一致,就直接判定数据被篡改或损坏。这个标签就是 GCM 的“封条”,任何一位密文被改动,最终标签都会对不上。
2.2 认证加密里的 AAD 是用来挂“上下文”的
GCM 除了加密数据和生成标签,还接受一段可选的 AAD,全称是 Additional Authenticated Data,附加认证数据。它不会被加密,但会参与 GHASH 运算,任何改动都会导致标签校验失败。这个特性特别适合用来绑定上下文信息。
放到 MAVLink 场景里,我们完全可以把消息 ID、目标系统 ID、目标组件 ID、甚至消息长度放到 AAD 里。这样一来,即使加密后的 payload 被攻击者从一条消息搬到另一条消息里,接收方重新计算标签时也会因为 AAD 不一致而直接拒绝。这种方式比单纯在明文里加几个校验字段要干净得多。我自己做封装的时候,默认就把消息的 10 字节 MAVLink 头部塞进 AAD,因为头部要为路由服务而保留明文,正好用 GCM 的 AAD 机制把它“焊死”在密文上。
需要注意一个细节:AAD 不参与加密,所以不适合放任何你觉得敏感的数据。如果某个字段不想让别人看到,它就应该进 payload 而不是进 AAD。
2.3 128 还是 256:无人机场景怎么选
很多人在选密钥长度时会陷入“越长越安全”的执念。对 AES 来说,128 位和 256 位的安全余量在可预见的未来都足够用。量子计算对对称算法的威胁是用 Grover 算法把暴力搜索复杂度降到大约 2^64 次,但 2^64 在当前工程条件下依然是个天文数字,短期内不会构成实际威胁。真正要担心的反而是公钥体系,如果后续用到 ECDH 做密钥协商,那才需要考虑更长的曲线和抗量子方案。
从工程角度看,AES-128 在资源受限设备上优势明显。STM32F103 这类没有硬件 AES 外设的芯片上,AES-128 一轮要 10 轮迭代,AES-256 要 14 轮,差出的这 4 轮在高速数据传输时就是肉眼可见的 CPU 占用差距。而在有 AES-NI 指令集的 PC 上,两者吞吐量差别已经很小。我的习惯是:MCU 端用 128,网关和地面站如果密钥管理允许,用 256 也行,但两者千万别混用,否则排查问题会让人头疼。
2.4 为什么不选 CBC + HMAC 或者 ChaCha20
有人可能会问,加密和认证为什么不用经典的 AES-CBC + HMAC-SHA256 组合?这套方案在 TLS 1.2 时代很常见,但在嵌入式场景里非常不友好。CBC 需要处理填充,明文长度不是块大小的整数倍就要 padding,还要区分填充字节,代码里几乎每一处都可能埋 bug。HMAC 又要单独维护一个密钥,密钥派生逻辑复杂,容易在轮换时出错。相比之下,GCM 的 API 就是一把梭:一个 key,一个 nonce,返回密文和 tag,校验时对称操作,接口简单太多。
ChaCha20-Poly1305 也是 AEAD,在完全没有 AES 硬件加速的设备上甚至比 AES-GCM 更快。但现实情况是,主流加密库和硬件厂商对 AES 生态的投入已经非常充分,mbedTLS、OpenSSL、Libsodium 都优先优化 AES-GCM 路径,很多 ARM Cortex-M 系列芯片也内置了 AES 硬件引擎。如果项目里没到“没有 AES 加速导致功耗压不住”的地步,选 AES-GCM 在生态和可维护性上都是更稳的选择。
3. 把 AES-128-GCM 接到 MAVLink 链路的几种姿势
3.1 方案一:链路层做隧道加密
最省事、也最不容易出错的做法,是在链路层做隧道加密。思路很简单:飞控发出的 MAVLink 原始字节流,先经过一个加密模块做 AES-128-GCM 处理,变成密文后再交给数传电台;接收端数传电台收到密文,先解密还原出完整 MAVLink 字节流,再交给地面站解析。对飞控和地面站来说,它们看到的还是正常的 MAVLink 协议,完全感知不到加密层存在。
这个方案的优点是对现有系统侵入性最小。飞控固件不用改,地面站软件不用改,只需要在链路的两端各加一个加密适配层。很多数传模块本身就运行着一个小型 RTOS,具备跑 mbedTLS 的资源,直接在电台固件里集成即可。缺点是无法在中间节点做转发决策,因为所有路由信息都被盖住了,不太适合多跳网络或者需要网关转发的场景。
我去年帮一个做无人船的项目做链路加固,就用的这个方案。他们的数传模块是基于 STM32F427 自己做的,里面本来就有空闲内存和 Flash 空间,我直接在电台固件里加了约 3KB 的加密代码,空中链路就变成密文了,飞控端完全不用碰。整条链路唯一的问题就是密文比明文长一点,后面会细说。
3.2 方案二:保留头部的 payload 加密 + MAVLink 2 签名
如果系统里已经打开了 MAVLink 2 签名,想在它之上再补一层机密性,可以考虑保留 MAVLink 头部明文、只加密 payload。MAVLink 2 头部里有系统 ID、组件 ID、消息 ID 和长度信息,这些保留明文,让上层的路由、过滤、统计逻辑都能正常工作;而真正敏感的载荷数据,比如位置坐标、速度、模式指令,全部加密。
这种方式实现起来要稍微复杂一些,因为 MAVLink 2 的 payload 长度是可变的,加密后密文长度会加上 16 字节 tag,你需要重新构造一条消息或者使用自定义消息来容纳加密后的数据。同时,原来 MAVLink 2 签名机制还保留着,它继续保护头部和加密后的 payload,防止有人篡改路由信息或者调换密文块。相当于用 MAVLink 2 签名保“真”,用 GCM 保“密”,各干各的活。
比较适合用在有中间节点转发的场景,比如一个中继地面站需要根据系统 ID 决定把消息往哪条链路转发,但又不想看到实际坐标。这样加密和路由可以共存,安全边界也更清晰。
3.3 方案三:自定义 MAVLink 消息装密文
还有一种思路是直接定义一条新的 MAVLink 自定义消息,比如叫ENCRYPTED_PAYLOAD,消息体里放目标系统 ID、nonce 索引和一段密文。原始 MAVLink 消息在应用层序列化之后,整个当作普通字节流加密,再塞进新消息的 payload 里发送。
这种方式的好处是协议语义非常清晰,链路里的所有节点都能识别“这是一条加密消息”,方便做调试和审计。坏处是消息膨胀比较厉害,一条 20 字节的航点消息,套上一层加密消息头、加上 nonce 和 tag,可能变成 60 字节甚至更多。在 433M 这种低速数传链路上负担不小。另外,自定义消息要求通信双方都升级协议定义,联调成本比前两种方案要高。
3.4 选型建议
选哪种方案,取决于你手里设备和网络的约束条件。下面的表格是我实际项目里的判断标准:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 数传电台是自研,可改固件 | 方案一:链路隧道加密 | 对飞控/GCS完全透明,改动最小 |
| 已有MAVLink 2签名,想补机密性 | 方案二:payload加密 | 保留路由信息,两种安全机制各司其职 |
| 需要应用层互操作或自研网关 | 方案三:自定义消息 | 语义明确,便于中间节点识别处理 |
| 链路带宽极低 (<5kbps) | 方案一 + 会话级nonce优化 | 减少每条消息的nonce传输开销 |
4. 实操:一个最小可跑的 MAVLink + AES-128-GCM 加密链路
4.1 工具链与库选择
实操部分我用 Python 做演示,因为 pymavlink 生态最全,适合快速验证方案。加密库用 Python 标准生态里的cryptography,它内部调用 OpenSSL,AES-GCM 有 AES-NI 加持,性能不是问题。实际的嵌入式端我建议用 mbedTLS,它提供mbedtls_gcm_crypt_and_tag和mbedtls_gcm_auth_decrypt两组 API,在 STM32 上已经过大量验证。
发送端负责构建并加密 MAVLink 消息,接收端负责解密并解析。为了还原真实链路,我直接把 pymavlink 的pack()输出当作“明文 MAVLink 字节流”,然后对它做 AES-128-GCM 加密封装。这个流程和链路隧道方案保持一致,也最能说明问题。
4.2 密钥管理和会话初始化
AES-128-GCM 需要一个 16 字节密钥。在工程里我不能建议你把密钥直接硬编码在代码里,而是要从主密钥派生,或者通过密钥协商机制动态生成。最简单的起步方式是预共享密钥:在地面站和数传模块里各存一份相同的 16 字节密钥,配置成文件或烧录到加密芯片中,上线前用串口校验一致性。
如果想做得更规范,可以用 X25519 做 ECDH 密钥协商,每次会话生成临时密钥对,两端各自算出共享密钥,再用 HKDF 派生 AES-128 密钥和初始 nonce 基础值。这样即使某次会话密钥泄漏,也不会影响历史数据和其他会话。下面的 Python 代码演示了如何用cryptography库生成一个预共享密钥:
import os # 生成 16 字节 AES-128 密钥,妥善保管 aes_key = os.urandom(16) print(aes_key.hex())密钥生成后有两种存法。一种是直接把 hex 字符串写进设备配置区,另一种是按 MAVLink 密钥文件格式存,也就是第一行写版本号,第二行写 32 字节密钥的 hex。后者主要给 MAVLink 2 签名用,AES-GCM 只取前 16 字节即可作为 128 位密钥,也可以单独用一个文件存 AES 密钥,别和签名密钥混在一起。
4.3 发送端完整流程(Python 示例)
发送端的流程分三步:构造 MAVLink 消息并序列化成字节,对字节流做 AES-128-GCM 加密,把加密结果加上必要的 nonce 信息后通过串口发送。这里有一个很容易踩坑的点:GCM 模式要求每次加密的 nonce 都不能重复,否则密钥就形同虚设。所以我用“会话随机盐 + 消息计数”拼成 12 字节 nonce,会话开始时随机生成 8 字节盐,之后每条消息的计数器递增 4 字节。
import os import struct from cryptography.hazmat.primitives.ciphers.aead import AESGCM class MAVLinkGCMEncoder: def __init__(self, aes_key: bytes): self.aesgcm = AESGCM(aes_key) self.session_salt = os.urandom(8) self.counter = 0 def _build_nonce(self) -> bytes: # 12字节 nonce = 8字节随机盐 + 4字节大端计数 nonce = self.session_salt + struct.pack(">I", self.counter) self.counter += 1 return nonce def encode(self, mavlink_bytes: bytes, aad: bytes = b"") -> bytes: nonce = self._build_nonce() ct = self.aesgcm.encrypt(nonce, mavlink_bytes, aad) # 输出格式: 8字节盐 + 4字节计数 + 密文(含16字节tag) return self.session_salt + struct.pack(">I", self.counter - 1) + ct编码器把mavlink_bytes加密后,输出包含了完整的解密所需信息。接收端拿到这个包,先取出盐和计数,重建 nonce,再调用 GCM 解密。注意输出里的计数器是当前消息的计数,发送端内部self.counter已经自增过了,所以用self.counter - 1表示当前消息的编号,确保接收方能正确重建 nonce。
4.4 接收端完整流程
接收端的逻辑是对称的。先把收到的包切成盐、计数、密文三部分,重建 12 字节 nonce,然后调用decrypt。如果密钥或 nonce 对不上,decrypt会抛出InvalidTag异常,接收方应当直接丢弃这条包,而不是尝试去解析一层坏数据。
from cryptography.exceptions import InvalidTag class MAVLinkGCMDecoder: def __init__(self, aes_key: bytes): self.aesgcm = AESGCM(aes_key) def decode(self, packet: bytes, aad: bytes = b"") -> bytes: if len(packet) < 13: raise ValueError("packet too short") session_salt = packet[:8] counter = struct.unpack(">I", packet[8:12])[0] nonce = session_salt + struct.pack(">I", counter) ct = packet[12:] try: return self.aesgcm.decrypt(nonce, ct, aad) except InvalidTag: # 生产环境这里应该记日志并静默丢弃 return None解码成功后得到的就是完整的 MAVLink 原始字节,接下来正常调用 pymavlink 的消息解析逻辑即可。
4.5 在 STM32 这类 MCU 上的集成要点
Python 演示跑通之后,真正落地到飞控或者数传模块,通常是在 STM32 上用 mbedTLS 实现。mbedTLS 提供的是 C 接口,核心调用是mbedtls_gcm_crypt_and_tag。一个基本的发送端加密流程是这样:
mbedtls_gcm_context ctx; unsigned char tag[16]; mbedtls_gcm_init(&ctx); mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 128); mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT, payload_len, nonce, 12, aad, aad_len, plaintext, ciphertext, sizeof(tag), tag); mbedtls_gcm_free(&ctx);这里有一件非常关键的事:GCM 加密完成后,密文和 tag 都要传给接收端,tag 通常跟在密文后面。如果你把 tag 丢了或者放错位置,接收端解出来的数据永远是坏的。我在排查过的一个项目里就见过,由于 DMA 传输时把 tag 缓冲区大小设错,导致空中数据偶发解不出来,最后查了两天才定位到是 tag 传输长度少了一字节。
如果 MCU 有 AES 硬件外设,尽量用硬件引擎替代 mbedTLS 的软件实现。软件 AES 在 72MHz 的 STM32F103 上做 128 位加密,大概要消耗几十微秒每块,在高频率遥测下会挤占主循环时间。用硬件 AES + DMA 可以把 CPU 占用降到接近零,代价是代码里多引入一层 HAL 抽象,维护复杂度略高。
5. 常见问题与排查技巧实录
5.1 解密报 InvalidTag / tag mismatch
这是最常遇到的问题,现象是接收端日志里 “InvalidTag” 或 “GCM auth failed” 刷屏,或者干脆地面站连不上。原因基本集中在四类:nonce 重用、AAD 不一致、密文被链路噪声破坏、密钥不匹配。
排查方法建议从最容易的来。第一步检查密钥是否一致,把两端配置里的密钥 hex 导出来对比,别只靠眼睛看,写个脚本比对一下最稳妥。第二步检查 nonce 管理逻辑,发送端是否在某个异常分支里重复使用了同一个 counter?设备重启后 salt 是否重新随机生成了?第三步检查 AAD:两端对 AAD 的定义是否完全一致,比如发送端把 10 字节 MAVLink 头部当 AAD,接收端是不是也拿同样的头部?只要有一字节差异,标签校验必挂。
5.2 加了加密后 MAVLink 心跳超时
这种问题通常不是加密算法本身导致的,而是数据流调度策略出了问题。加密操作会引入多余延迟,如果发送端采用“攒一批消息统一加密再发送”的策略,心跳包就可能被压在后面。地面站超过一定时间没收到心跳,直接判定链路失联,紧接着触发回传链路切换或返航逻辑。
解决思路很简单:给心跳包最高优先级。在加密模块的发送队列里,要求心跳消息跳过队列、立即加密并发送。实现上可以在加密封装里单独留一条快速通道,或者给消息头部加一个标志位。这个细节看起来小,但真到了实飞的场景里,它比很多安全参数都重要。
5.3 时间戳和链路 ID 配置混乱
如果启用了 MAVLink 2 签名,又同时做 AES-GCM 加密,时间戳和链路 ID 的配置就是重灾区。MAVLink 2 签名要求 48 位时间戳严格递增,飞控重启后如果时间戳从 0 开始,地面站已经存有之前较大的时间戳,新消息会被当作重放导致全部丢弃。这个问题通常表现为“设备重启后一切正常,几分钟后链路断开”。
处理办法是在设备启动时做时间戳初始化,尽量从 RTC 或 GPS 时间同步,或者在协议层面允许一定范围的“时间戳回退重协商”流程。链路 ID 方面,确认通信两端配置的是同一个 ID,别出现飞控用 0、地面站用 1 的错位。这个问题在只有一台飞控和一个地面站的单链路场景里不明显,一旦做成多链路汇聚,链路 ID 冲突的概率会显著上升。
5.4 性能与延迟:在低带宽链路上的优化
AES-128-GCM 在普通电脑上加密 100 字节数据几乎测不出耗时,但在 433M 无线链路上,最大的瓶颈是带宽而不是 CPU。密文比明文多出 16 字节 tag,再加上每包 12 字节 nonce,对于一条 20 字节的 MAVLink 消息来说,体积膨胀了一倍多。低速链路上这种膨胀不可忽视。
优化策略有两个方向。一是减少每包固定开销,比如用我前面代码里的会话级 salt 方案,nonce 的 8 字节盐只在会话开始时传输一次,之后每条消息只带 4 字节 counter。二是做小包合并,把多条 MAVLink 消息攒成一个大字节流一起加密,平摊 tag 和 nonce 开销,但这会增加延迟,要在实时性和带宽之间找平衡。
6. 踩坑记录与工程建议
6.1 密钥泄漏是最大的安全漏洞
加密算法再强,密钥丢了就等于白干。很多开发者花了大力气实现 GCM 加密,却把密钥直接写在固件代码里,或者通过明文串口打印出来,这样整个加密体系形同虚设。密钥管理至少要做到几点:固件发布时不能包含可明文读取的密钥,最好用独立安全存储区或加密芯片保存;密钥轮换要有机制,不能一个密钥用一辈子;不同项目、不同客户之间要使用独立密钥,防止一家泄漏导致所有设备被破解。
6.2 安全日志和审计
加了加密以后,别把安全问题完全交给算法,日志审计也得跟上。接收端应当记录解密失败的次数、来源、时间,如果某个来源频繁触发 InvalidTag,这就是一个非常明确的攻击信号。发送端也要记录密钥轮换、会话建立这些关键事件。日志本身不用太详细,避免暴露敏感信息,但至少要让你在出问题的时候能快速定位到“是配置错了”还是“被攻击了”。
6.3 回退机制
加密链路一旦出问题,最怕的是无人机在空中直接失联。所以落地加密功能时一定要设计回退机制:链路加密握手失败时,是自动降级为明文运行,还是保持静默不发送?我的建议是,在安全的测试阶段开启明文降级,方便排查问题;在正式任务中关闭降级,宁可任务失败,也不能把明文数据暴露出去。这一点需要在任务开展前和飞手、地面站操作员达成一致,否则现场出了问题大家只能干瞪眼。
我在实际项目里还有一个体会:加密的接入时机最好放在系统联调的早期。如果先把整个数传链路调通了,再把加密塞进去,一旦出问题,你要同时排查射频、协议、加密三个层面,难度会陡增。反过来,从一开始就把加密层纳入联调范围,所有模块都按“线上已经是密文”的状态去测试,后面加功能时会省心很多。AES-128-GCM 本身是一个非常成熟的工具,难点从来不在算法本身,而在于你能不能把它干净地嵌进自己那条独特的链路里。