前提
默认读者对LoRaWAN有一定的了解,此文将着重讲解如何加/解密。
帧格式
PHY 帧格式
上行PHY帧格式:
Preamble | PHDR | PHDR_CRC | PHYPayload | CRC |
---|
下行PHY帧格式:
Preamble | PHDR | PHDR_CRC | PHYPayload |
---|
MAC帧格式
一般我们拿到的原始数据就是PHYPayload,其结构如下:
bytes | 1 | 1…M | 4 |
---|---|---|---|
PHYPayload | MHDR | MACPayload | MIC |
MHDR:MAC头中指定了消息类型(MType)和帧编码所遵循的LoRaWAN规范的主版本号(Major)
MACPayload:MAC载荷,也就是所谓的“数据帧”,包含:帧头(FHDR)、端口(FPort)以及帧载荷
(FRMPayload),其中端口和帧载荷是可选的
MIC:消息校验码
数据帧
FHDR(帧头) | FPort(端口,可选) | FRMPayload(载荷,可选) |
---|
FHDR(帧头)
bytes | 4 | 1 | 2 | 0…15 |
---|---|---|---|---|
FHDR | DevAddr | FCtrl | FCnt | FOpts |
其中FCtrl的低4字节表示FOpts长度
FPort(端口)
如果帧载荷字段不为空,端口字段必须体现出来
端口字段有体现时,若FPort的值为0表示FRMPayload只包含了MAC命令
加/解密
载荷数据需要进行加密,这里并不是直接对载荷数据进行加密,而是加密事先定义好的数据块,最后利用算出来的数据块与载荷数据异或来实现加/解密
MIC用来校验消息的完整性
FRMPayload加/解密过程
采用AES128_ECB加密,密钥K根据不同的FPort来使用:
FPort | K |
---|---|
0 | NwkSKey |
1…255 | AppSKey |
算法定义了一个块序列Ai,i从1到k,k = ceil( len( FRMPayload) / 16 ),ceil为向上取整函数
bytes | 1 | 4 | 1 | 4 | 4 | 1 | 1 |
---|---|---|---|---|---|---|---|
Ai | 0x01 | 4 * 0x00 | Dir | DevAddr | FCntUp or FCntDown | 0x00 | i |
Dir:上行帧时为0,在下行帧时为1
FCnt:FCnt在数据帧中占2字节,需要补两个0,大小端同原始数据PHYPayload
对Ai进行加密可以得到序列S
Si = aes128_encrypt(K, Ai) for i = 1…k
S = S1 | S2 | … | Sk
通过与S异或计算对载荷数据FRMPayload进行加解密
FRMPayload解密实例
{
"phyPayload": "80 86 96 72 01 80 1F 09 08 DD 84 E1 6A 81 E9 B5 99 5C C5 D5 CF 77 5E 39",
"phyPayloadJSON": {
"mhdr": {
"mType": "ConfirmedDataUp",
"major": "LoRaWANR1"
},
"macPayload": {
"fhdr": {
"devAddr": "01729686",
"fCtrl": {
"adr": true,
"adrAckReq": false,
"ack": false,
"fPending": false,
"classB": false
},
"fCnt": 2335,
"fOpts": null
},
"fPort": 8,
"DecryptData": [
"63 71 A5 EB 10 00 00 00 32 00 00"
]
},
"mic": "cf775e39"
},
"AppSKey": "e022c95865de731b94cab0e19e02992b",
"NwkSKey": "0bfd388aa201cc2b63f78a1d8efb58aa"
}
通过分析帧格式,可以得到如下重要信息:
AppSKey:e022c95865de731b94cab0e19e02992b
FRMPayload:DD 84 E1 6A 81 E9 B5 99 5C C5 D5
DevAddr:86 96 72 01
fCnt:1F 09
Dir:这里是上行帧,所以Dir=0
这里的FRMPayload的数据长度为11,所以只用加密一块数据,即A1,经过简单拼凑可以得到A1如下:
01 00 00 00 00 00 86 96 72 01 1F 09 00 00 00 01
对A1进行AES128_ECB加密得到S=S1如下:
BE F5 44 81 91 E9 B5 99 6E C5 D5 86 2F 1B 4C 42
S与FRMPayload异或可以得到如下结果:
BE F5 44 81 91 E9 B5 99 6E C5 D5 86 2F 1B 4C 42
XOR
DD 84 E1 6A 81 E9 B5 99 5C C5 D5
=
63 71 A5 EB 10 00 00 00 32 00 00,与DecryptData一致
附上C语言代码,基于OpneSSL
#include <stdio.h>
#include <string.h>
#include <openssl/aes.h>
int main(int arg, char *argv[])
{
unsigned char A1[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0x96, 0x72, 0x01 ,0x1f, 0x09, 0x00, 0x00, 0x00, 0x01};
unsigned char AppSKey[16] = {0xe0, 0x22, 0xc9, 0x58, 0x65, 0xde, 0x73, 0x1b, 0x94, 0xca, 0xb0, 0xe1, 0x9e, 0x02, 0x99, 0x2b};
unsigned char S[16] = {0};
AES_KEY encrypt_key;
// 生成加密密钥 必须128bit/16BYTE
AES_set_encrypt_key(AppSKey, 128, &encrypt_key);
// 加密 一次执行一个块即16BYTE
AES_ecb_encrypt(A1, S, &encrypt_key, AES_ENCRYPT);
printf("S: ");
for (size_t i = 0; i < 16; i++)
{
printf("%02X ", S[i]);
}
printf("\r\n");
return 0;
}
MIC计算过程
采用CMAC ,即分组密码的消息认证码算法。具体公式如下:
cmac = aes128_cmac(NwkSKey, B0 | msg) MIC = cmac[0…3]
msg = MHDR | FHDR | FPort | FRMPayload
块B0的定义如下:
bytes | 1 | 4 | 1 | 4 | 4 | 1 | 1 |
---|---|---|---|---|---|---|---|
B0 | 0x49 | 4 * 0x00 | Dir | DevAddr | FCntUp or FCntDown | 0x00 | len(msg) |
Dir:上行帧时为0,在下行帧时为1
FCnt:FCnt在数据帧中占2字节,需要补两个0,大小端同原始数据PHYPayload
MIC计算实例
{
"phyPayload": "80 86 96 72 01 80 1F 09 08 DD 84 E1 6A 81 E9 B5 99 5C C5 D5 CF 77 5E 39",
"NwkSKey": "0bfd388aa201cc2b63f78a1d8efb58aa"
}
通过分析帧格式,可以得到如下重要信息:
NwkSKey:0bfd388aa201cc2b63f78a1d8efb58aa
msg:80 86 96 72 01 80 1F 09 08 DD 84 E1 6A 81 E9 B5 99 5C C5 D5
mic:CF 77 5E 39
DevAddr:86 96 72 01
fCnt:1F 09
Dir:这里是上行帧,所以Dir=0
可以得出B0:
49 00 00 00 00 00 86 96 72 01 1f 09 00 00 00 14
进一步可以得出实际参与运算的MSG = B0 | msg:
49 00 00 00 00 00 86 96 72 01 1f 09 00 00 00 14 80 86 96 72 01 80 1F 09 08 DD 84 E1 6A 81 E9 B5 99 5C C5 D5
通过上一节的公式可以得到cmac = aes128_cmac(NwkSKey, B0 | msg):
CF 77 5E 39 6E 69 9B 4E 33 15 40 18 55 77 A6 51,前4字节与phyPayload中的mic一致
附上C语言代码,基于OpneSSL
#include <openssl/cmac.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int arg, char *argv[])
{
unsigned char NwkSKey[16] = {0X0b, 0Xfd, 0X38, 0X8a, 0Xa2, 0X01, 0Xcc, 0X2b, 0X63, 0Xf7, 0X8a, 0X1d, 0X8e, 0Xfb, 0X58, 0Xaa};
unsigned char b0[16] = {0X49, 0X00, 0X00, 0X00, 0X00, 0X00, 0x86, 0x96, 0x72, 0x01 ,0x1f, 0x09, 0x00, 0x00, 0x00, 0X14};
unsigned char msg[20]= {0x80, 0x86, 0x96, 0x72, 0x01, 0x80, 0x1F, 0x09, 0x08, 0xDD, 0x84, 0xE1, 0x6A, 0x81, 0xE9, 0xB5, 0x99, 0x5C, 0xC5, 0xD5};
unsigned char MSG[36];
memcpy(MSG, b0, 16);
memcpy(MSG + 16, msg, 20);
CMAC_CTX* cmac_ctx = CMAC_CTX_new();
if (!cmac_ctx)
{
printf("Create CMAC_CTX error!\n");
return -1;
}
const EVP_CIPHER* cipher = EVP_aes_128_cbc();
if (!CMAC_Init(cmac_ctx, NwkSKey, 16, cipher, 0))
{
CMAC_CTX_free(cmac_ctx);
printf("CMAC Init error!\n");
return -1;
}
if(!CMAC_Update(cmac_ctx, MSG, 36))
{
CMAC_CTX_free(cmac_ctx);
printf("CMAC Update error!\n");
return -1;
}
size_t cmac_len;
uint8_t cmac[128];
if (!CMAC_Final(cmac_ctx, cmac, &cmac_len))
{
printf("[DeriveKey(): OEMCrypto_ERROR_CMAC_FAILURE]\n");
return -1;
}
printf("derive key len[%ld]data:\n",cmac_len);
for(int i = 0; i < cmac_len; i++)
{
printf("%02X ",cmac[i]);
}
printf("\n");
CMAC_CTX_free(cmac_ctx);
return 0;
}