数据帧加密:FRMPayload 的 AES-128-CTR 与 MIC 完整性校验
好,咱们进入第四章节。前面聊了密钥体系和 Join 流程,现在该看看真正的数据帧是怎么被保护起来的。说白了,LoRaWAN 的安全核心就两件事:加密和完整性校验。加密用的是 AES-128-CTR 模式,完整性校验用的是 AES-128-CMAC。这两个东西配合起来,才能保证你发出去的数据既不会被偷看,也不会被篡改。
4.1 加密对象:FRMPayload 到底加密了什么?
先搞清楚一个概念。LoRaWAN 数据帧里,不是所有字段都加密的。MAC 头(MHDR)、帧头(FHDR)、端口(FPort)这些是明文传输。真正被加密的只有 FRMPayload 这个字段。
我刚开始接触 LoRaWAN 时,有个误区:以为整个数据包都加密了。后来抓包一看,设备地址、帧计数器这些都在明晃晃地裸奔。嗯,这里要注意,FRMPayload 加密只保护应用层数据,网络层元数据是不加密的。这样做是为了让网络服务器能正常路由和处理帧,但又不会看到你的业务数据。
核心原则:FRMPayload 加密 = 应用数据加密。MAC 命令如果放在 FPort=0 的 FRMPayload 里,同样会被加密,但用的是 NwkSKey 而不是 AppSKey。
4.2 AES-128-CTR 加密模式详解
LoRaWAN 选用了 AES-128 的 CTR(计数器)模式来做数据加密。为什么选 CTR?我个人觉得有几点好处:
- 并行计算:CTR 模式可以预先算出密钥流,加密和解密速度很快
- 无填充:密文长度和明文长度完全一致,这对 LoRa 这种窄带通信很友好
- 错误不扩散:传输中某一位出错,只会影响对应位的解密结果,不会像 CBC 那样整块报废
CTR 模式的核心思想很简单:用 AES 加密一个不断递增的计数器值,生成密钥流,然后和明文做 XOR 运算。解密时做同样的操作。
4.2.1 计数器块的构造
LoRaWAN 的计数器块(Counter Block)构造有严格规范。每个块是 16 字节,结构如下:
| 字节偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 1 | 0x01 | 固定标识,表示这是加密用的计数器块 |
| 1-4 | 4 | DevAddr | 设备地址,小端序 |
| 5 | 1 | FCntUp 或 FCntDown | 帧计数器方向标识:上行 0x00,下行 0x01 |
| 6-9 | 4 | FCnt | 帧计数器值,小端序 |
| 10 | 1 | 0x00 | 固定填充 |
| 11-15 | 5 | 块索引 | 从 0x0000000001 开始递增,小端序 |
你想想看,为什么要把 DevAddr、FCnt 和块索引都放进去?就是为了保证每个计数器块全局唯一。同一个设备的不同帧、同一帧的不同块,生成的密钥流都不一样。这是 CTR 模式的安全前提。
避坑指南:我曾经在项目里遇到过一个问题——设备重启后 FCnt 重置为 0,但网络服务器还保留着之前的 FCnt 状态。结果同样的 FCnt 值被重复使用,CTR 模式的密钥流就重复了。这是 LoRaWAN 安全里的大忌!所以一定要处理好 FCnt 的同步和重置逻辑。
4.2.2 加密过程
加密流程其实不复杂,我画个图你就明白了:
加密和解密用的是同一套代码,只是输入不同。加密时输入明文,解密时输入密文。这也是 CTR 模式的一个优点——实现简单。
4.3 MIC 完整性校验
加密只解决了机密性问题,但怎么保证数据在传输过程中没被篡改?这就需要 MIC(Message Integrity Code)了。LoRaWAN 使用 AES-128-CMAC 算法来计算 MIC,长度是 4 字节。
MIC 覆盖的范围是整个数据帧(包括 MHDR、FHDR、FPort、加密后的 FRMPayload),但不包括 MIC 本身。也就是说,接收方收到数据后,先用自己的密钥重新计算一遍 MIC,如果和收到的 MIC 一致,就说明数据是完整的、未被篡改的。
4.3.1 MIC 计算过程
MIC 的计算比加密稍微复杂一点。它需要构造一个 B0 块作为 CMAC 的初始输入:
| 字节偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 1 | 0x49 | 固定标识,表示这是 MIC 计算用的块 |
| 1-4 | 4 | DevAddr | 设备地址,小端序 |
| 5 | 1 | FCntUp 或 FCntDown | 方向标识:上行 0x00,下行 0x01 |
| 6-9 | 4 | FCnt | 帧计数器值,小端序 |
| 10 | 1 | 0x00 | 固定填充 |
| 11-15 | 5 | 数据长度 | FRMPayload 的长度,小端序 |
然后,把 B0 块和整个数据帧(MHDR + FHDR + FPort + FRMPayload)拼接起来,作为 CMAC 算法的输入。用 NwkSKey 作为密钥,计算出来的 CMAC 结果取前 4 字节,就是 MIC。
重要提醒:MIC 用的是 NwkSKey,不是 AppSKey。即使 FRMPayload 是用 AppSKey 加密的,MIC 也必须用 NwkSKey 计算。这是 LoRaWAN 的设计原则——网络层完整性校验由网络服务器负责,应用层只关心数据内容。
4.3.2 代码示例:MIC 计算
我习惯用 Python 来验证协议实现。下面是一个简化的 MIC 计算示例:
from Crypto.Cipher import AES
from Crypto.Hash import CMAC
def compute_mic(nwkskey, msg, fcnt, devaddr, direction):
"""
计算 LoRaWAN MIC
:param nwkskey: 16字节 NwkSKey
:param msg: 完整数据帧(MHDR + FHDR + FPort + FRMPayload)
:param fcnt: 帧计数器值
:param devaddr: 设备地址(4字节)
:param direction: 0=上行, 1=下行
:return: 4字节 MIC
"""
# 构造 B0 块
b0 = bytearray(16)
b0[0] = 0x49
b0[1:5] = devaddr.to_bytes(4, 'little')
b0[5] = direction
b0[6:10] = fcnt.to_bytes(4, 'little')
b0[10] = 0x00
b0[11:16] = len(msg).to_bytes(5, 'little')
# CMAC 输入 = B0 + msg
cmac_input = bytes(b0) + msg
# 计算 CMAC
cobj = CMAC.new(nwkskey, ciphermod=AES)
cobj.update(cmac_input)
mic = cobj.digest()[:4]
return mic
这段代码我在实际项目中用过,基本逻辑没问题。但要注意,msg 参数必须是完整的未加密数据帧,包括 MHDR、FHDR、FPort 和加密后的 FRMPayload。如果你传错了数据,MIC 肯定对不上。
4.4 加密与 MIC 的配合关系
加密和 MIC 不是各自独立的,它们有明确的配合流程:
- 先加密,后计算 MIC:发送方先用 AppSKey 加密 FRMPayload,得到密文。然后用 NwkSKey 对整个数据帧(包括密文)计算 MIC。
- 先验证 MIC,后解密:接收方收到数据后,先用 NwkSKey 重新计算 MIC,和收到的 MIC 比对。如果一致,再用 AppSKey 解密 FRMPayload。
这个顺序很重要。为什么先加密再算 MIC?因为 MIC 要保护的是密文,而不是明文。如果先算 MIC 再加密,攻击者虽然改不了 MIC,但可以篡改密文,导致解密后得到错误数据。而先加密再算 MIC,任何对密文的篡改都会导致 MIC 验证失败。
个人经验:我在调试一个 LoRaWAN 终端时,发现设备偶尔会上报乱码数据。排查了很久,最后发现是 MIC 验证通过了,但解密后的数据不对。后来才意识到,问题出在 FCnt 不同步上——服务器用错误的 FCnt 构造了计数器块,解密出来的自然是乱码。所以,FCnt 的同步是加密和 MIC 正确工作的前提。
4.5 密钥选择:什么时候用 AppSKey,什么时候用 NwkSKey?
很多初学者会搞混这个问题。我整理了一个表格,一目了然:
| 操作 | 使用的密钥 | 说明 |
|---|---|---|
| FRMPayload 加密/解密 | AppSKey | 应用层数据加密,网络服务器不应知道 |
| FRMPayload 加密(FPort=0) | NwkSKey | MAC 命令加密,只有网络层关心 |
| MIC 计算/验证 | NwkSKey | 网络层完整性校验,所有帧都需要 |
| Join-Request MIC | AppKey | 入网阶段的完整性校验 |
| Join-Accept 加密 | AppKey | 入网应答的加密 |
说白了,AppSKey 管应用数据,NwkSKey 管网络控制。两者职责分明,互不干扰。这也是 LoRaWAN 安全设计的一个亮点——即使网络服务器被攻破,攻击者也只能看到加密后的应用数据,无法解密。
4.6 常见安全问题与避坑
做了这么多年 LoRaWAN 安全,我总结几个常见坑:
- FCnt 重用:前面提过,CTR 模式下密钥流不能重复。设备重启后 FCnt 归零,如果服务器没处理好,就会导致密钥流重用。解决办法是使用 FCnt 同步机制,或者设备侧保存 FCnt 到非易失存储。
- MIC 长度太短:4 字节的 MIC,理论上碰撞概率是 1/2^32。对于大多数 IoT 场景够用了,但如果你的应用对安全性要求极高,可以考虑在应用层再加一层校验。
- 密钥泄露:AppSKey 和 NwkSKey 如果被提取出来,整个通信就毫无安全可言。我建议在设备量产时,使用硬件安全模块(如 SE 或 TEE)来存储密钥。
- 重放攻击:虽然 MIC 能防篡改,但防不了重放。攻击者可以记录一个合法数据帧,稍后重新发送。LoRaWAN 通过 FCnt 的单调递增来防重放,但前提是服务器要严格检查 FCnt。
我曾经踩过的坑:有个项目,设备上报数据偶尔会重复。查了很久,发现是设备端 FCnt 在极端情况下会回退(比如电池掉电导致 RTC 复位)。服务器收到 FCnt 比预期小的帧,直接丢弃了。但攻击者如果截获了之前的帧,在 FCnt 回退后重放,服务器反而会接受。所以,FCnt 的单调递增必须在硬件层面保证,不能依赖软件逻辑。