4、数据帧加密与完整性保护:AES-128 CTR模式加密、CMAC消息完整性校验
好,咱们接着聊。上一节我们把密钥的生命周期和派生机制捋了一遍,这节终于要动真格的了——数据帧到底是怎么被加密的?完整性又是怎么保证的?
说实话,我早年刚接触LoRaWAN时,觉得加密不就是套个AES嘛,有啥难的?结果第一次做产品联调,发现服务器死活解不开终端发上来的数据。查了一整天,最后发现是CTR模式的计数器初始值没对齐。嗯,这种坑,我踩过,今天一并讲清楚。
4.1 为什么是AES-128?
LoRaWAN选AES-128,不是拍脑袋决定的。你想想看,终端设备大多是电池供电,算力有限。AES-128在硬件实现上非常高效,很多MCU甚至内置了AES硬件加速模块。我建议你在选型时,优先考虑带硬件AES的芯片,能省不少电。
至于为什么不用256位?说白了,128位在可预见的未来足够安全。LoRaWAN的威胁模型主要针对无线窃听和重放攻击,128位的密钥空间,以目前的算力,暴力破解基本不现实。
4.2 CTR模式:流加密的妙用
LoRaWAN的数据帧加密用的是AES-128 CTR模式。CTR模式是个流加密模式,它不像CBC那样需要填充数据。这对LoRaWAN来说太重要了——LoRaWAN的帧长度经常是变长的,而且可能很短,比如就几个字节的上行数据。CTR模式加密后的密文长度和明文完全一样,没有额外开销。
CTR模式的核心思想是:用AES加密一个计数器值,生成密钥流,然后和明文做异或。
公式很简单:
密文 = 明文 XOR AES(密钥, 计数器块)
解密时,用同样的计数器块生成同样的密钥流,再和密文异或一次,就还原出明文了。
关键点:CTR模式的安全性完全取决于计数器值是否唯一。如果两个不同的数据帧用了相同的计数器块和相同的密钥,那密钥流就一样,攻击者可以通过异或两个密文直接得到明文的异或值,进而破解。
我在项目中遇到过一个问题:终端设备重启后,帧计数器从0重新开始,但服务器端还保留着之前的帧计数器状态。结果两边计数器不同步,解密全失败。后来我加了个机制,终端每次重启后先发一个特殊的Join-Request重新入网,重置所有计数器。这个坑,大家一定要注意。
4.3 计数器块的构造
LoRaWAN的计数器块不是简单的递增数字,它有固定的结构。我直接给你看格式:
计数器块 (16字节) =
[0x01] (1字节, 标志位)
+ [方向位: 0x00上行/0x01下行] (1字节)
+ [DevAddr] (4字节)
+ [帧计数器 FCnt] (4字节)
+ [0x00] (4字节, 填充)
+ [块索引 Block Index] (2字节)
为什么要有方向位?因为上行和下行的帧计数器是独立的,防止重放攻击。DevAddr确保不同终端即使帧计数器相同,生成的密钥流也不同。
块索引是干嘛的?一个数据帧可能很长,超过16字节,就需要多个AES加密块。块索引从0开始递增,每个块加密16字节数据。
| 字段 | 长度 | 说明 |
|---|---|---|
| 标志位 | 1字节 | 固定为0x01,标识这是加密计数器块 |
| 方向位 | 1字节 | 0x00上行,0x01下行 |
| DevAddr | 4字节 | 终端设备地址 |
| FCnt | 4字节 | 帧计数器值 |
| 填充 | 4字节 | 固定为0x00 |
| 块索引 | 2字节 | 从0开始,每16字节递增1 |
4.4 CMAC:消息完整性校验
加密只能保证数据不被窃听,但不能保证数据在传输过程中没有被篡改。这就是CMAC登场的原因。
CMAC,全称是Cipher-based Message Authentication Code。它基于AES-128,能生成一个4字节的消息完整性校验码(MIC)。LoRaWAN的每个数据帧末尾都带了这个MIC。
我习惯把CMAC理解成「数字签名」的简化版。发送方用密钥对整帧数据(包括帧头、负载、帧计数器等)计算一个MIC,接收方用同样的密钥和同样的数据重新计算,如果两个MIC一致,说明数据没有被篡改。
CMAC的计算过程稍微复杂一点,但核心逻辑是:
MIC = AES-CMAC(密钥, 数据, 长度)
LoRaWAN规定MIC只取前4字节。为什么是4字节?因为LoRaWAN的帧长度非常宝贵,4字节的MIC在安全性和开销之间取得了平衡。4字节的碰撞概率是1/2^32,对于物联网场景来说足够了。
注意:MIC的计算范围包括整个MAC层数据帧,但不包括前导码和CRC。具体来说,从MHR(MAC帧头)开始,到FRMPayload(加密后的负载)结束,全部参与MIC计算。
4.5 加密与完整性校验的完整流程
咱们把整个流程串起来看看。假设终端要发送一个上行数据帧:
- 构造明文负载:应用层数据,比如传感器读数。
- 生成计数器块:根据DevAddr、方向位(上行)、帧计数器FCnt、块索引构造。
- CTR模式加密:用AppSKey对明文负载进行加密,得到密文负载。
- 构造完整帧:把MHR、FCnt、密文负载、FPort等拼起来。
- 计算MIC:用NwkSKey对整帧(从MHR到密文负载)计算CMAC,取前4字节。
- 发送:把帧头和MIC一起发送出去。
接收端收到后,先验证MIC,再解密。顺序不能乱——先验证完整性,再解密。如果先解密再验证,万一数据被篡改,解密出来的就是垃圾数据,白白浪费算力。
我的经验:在调试阶段,如果发现服务器报MIC校验失败,不要急着怀疑密钥。先检查帧计数器FCnt是否同步。我遇到过好几次,终端重启后FCnt归零,但服务器还在期待更大的FCnt值,导致MIC计算用的FCnt不一致。解决办法是在服务器端允许一定范围内的FCnt回退,或者干脆让终端重新入网。
4.6 下行帧的加密差异
下行帧的加密逻辑和上行基本一致,但有两点不同:
- 方向位:下行帧的方向位是0x01。
- 密钥:下行帧的加密用的是AppSKey,但MIC计算用的是NwkSKey。为什么?因为NwkSKey是网络服务器和终端共享的,而AppSKey是应用服务器和终端共享的。MIC由网络服务器计算,所以必须用NwkSKey。
这里有个容易混淆的地方:上行帧的加密和MIC计算分别用了AppSKey和NwkSKey,下行帧也一样。但下行帧的加密是由应用服务器完成的,而MIC是由网络服务器添加的。两个服务器之间需要协调好,否则MIC会算错。
4.7 知识体系图
下面这张图把数据帧加密和完整性保护的核心逻辑串起来了,我建议你保存下来,调试时对照着看:
4.8 代码示例:CTR加密与CMAC计算
下面是一个简化的Python示例,演示了LoRaWAN数据帧的加密和MIC计算过程。实际产品中建议用C语言实现,但逻辑是一样的:
from Crypto.Cipher import AES
from Crypto.Util import Counter
import struct
# 假设参数
dev_addr = 0x12345678
fcnt = 42
direction = 0x00 # 上行
app_skey = bytes.fromhex('00112233445566778899AABBCCDDEEFF')
nwk_skey = bytes.fromhex('FFEEDDCCBBAA99887766554433221100')
# 明文负载
plaintext = b'Hello LoRaWAN!'
# 1. CTR加密
# 构造计数器块初始值
counter_init = struct.pack('B', 0x01) # 标志位
counter_init += struct.pack('B', direction) # 方向
counter_init += struct.pack('>I', dev_addr) # DevAddr
counter_init += struct.pack('>I', fcnt) # FCnt
counter_init += struct.pack('>I', 0) # 填充
counter_init += struct.pack('>H', 0) # 块索引从0开始
# 创建CTR模式加密器
ctr = Counter.new(128, initial_value=int.from_bytes(counter_init, 'big'))
cipher = AES.new(app_skey, AES.MODE_CTR, counter=ctr)
ciphertext = cipher.encrypt(plaintext)
print(f'密文: {ciphertext.hex()}')
# 2. CMAC计算
from Crypto.Hash import CMAC
# 构造需要计算MIC的数据(MHR + FCnt + 密文负载 + FPort等)
# 这里简化,实际需要包含完整帧头
mic_data = struct.pack('>I', dev_addr) # 简化示例
mic_data += struct.pack('>I', fcnt)
mic_data += ciphertext
cmac = CMAC.new(nwk_skey, ciphermod=AES)
cmac.update(mic_data)
mic = cmac.digest()[:4] # 取前4字节
print(f'MIC: {mic.hex()}')
调试技巧:如果你在开发中遇到解密失败,我建议你先把计数器块的每个字节打印出来,和服务器端对比。我曾经遇到过大小端搞反的情况——DevAddr在MCU里是小端存储,但协议要求大端,结果计数器块构造错了,解密出来的全是乱码。
4.9 安全建议
最后,给你几条我在实际项目中总结的安全建议:
- 永远不要重用帧计数器:哪怕终端重启,也要通过重新入网来重置计数器,而不是手动清零。
- 密钥存储要安全:AppSKey和NwkSKey不能硬编码在代码里,建议使用安全元件或密钥存储区。
- MIC校验不能跳过:有些开发者为了调试方便,暂时关闭了MIC校验。我建议你永远不要这样做,因为一旦关闭,你就无法检测到数据是否被篡改。
- 注意时间同步:虽然LoRaWAN不强制要求时间同步,但CTR模式的计数器块依赖FCnt,如果终端和服务器的时间偏差导致FCnt跳跃过大,也会出问题。
好了,这一节的内容就到这里。加密和完整性校验是LoRaWAN安全的基石,理解了CTR模式和CMAC,你就掌握了数据帧保护的核心。下一节我们会聊更深入的内容,但先把这些基础打牢。
公众号:蓝海资料掘金营,微信deep3321