数据帧加密: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 加密过程

加密流程其实不复杂,我画个图你就明白了:

AES-128-CTR 加密/解密流程 计数器块 Ai AES-128 加密 AppSKey/NwkSKey 密钥流 Si 明文 Pi XOR 密文 Ci 说明: 1. 计数器块 Ai 由 DevAddr、FCnt、块索引等构成 2. 用 AppSKey(或 NwkSKey)对 Ai 做 AES-128 加密,得到密钥流 Si 3. 密文 Ci = 明文 Pi XOR 密钥流 Si 4. 解密时完全相同的操作:明文 Pi = 密文 Ci XOR 密钥流 Si

加密和解密用的是同一套代码,只是输入不同。加密时输入明文,解密时输入密文。这也是 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 不是各自独立的,它们有明确的配合流程:

  1. 先加密,后计算 MIC:发送方先用 AppSKey 加密 FRMPayload,得到密文。然后用 NwkSKey 对整个数据帧(包括密文)计算 MIC。
  2. 先验证 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 的单调递增必须在硬件层面保证,不能依赖软件逻辑。