25、序列化与加密:加密序列化数据、AES加密与序列化结合
说实话,做嵌入式通信这么多年,我踩过最大的坑就是——裸奔的序列化数据。
你想想看,你辛辛苦苦把结构体打包成二进制流,通过无线或者有线发出去。结果中间被人抓包,数据一览无余。更可怕的是,有人篡改你的数据,再发出去。设备收到后,直接执行了错误指令。嗯,我在一个物联网项目里就遇到过这种事,当时设备被人远程改了配置参数,差点造成生产事故。
所以,序列化之后,一定要加密。今天我们就聊聊怎么把AES加密和序列化结合起来。
为什么序列化数据需要加密?
序列化本身不提供任何安全性。它只是把内存里的结构体,变成字节流。谁拿到这个字节流,谁就能反序列化出原始数据。
我个人的习惯是:只要数据经过网络、无线、或者存储到外部Flash,一律加密。哪怕你觉得数据不重要,也要防一手。因为攻击者往往从“不重要”的数据入手,逐步渗透。
常见的威胁有:
- 窃听:抓包获取敏感数据,比如设备密钥、用户信息
- 篡改:修改序列化数据中的字段,比如把温度阈值从60改成90
- 重放:记录一段合法数据,稍后重新发送,欺骗设备
加密能解决窃听问题。但要防篡改和重放,还得加上消息认证码(MAC)和序列号。今天我们先聚焦加密本身。
AES加密基础回顾
AES(Advanced Encryption Standard)是目前最主流的对称加密算法。嵌入式里常用的是AES-128和AES-256。区别在于密钥长度,128位用16字节密钥,256位用32字节密钥。
我个人建议:能上256就别用128。虽然128在理论上是安全的,但多花几个字节的密钥存储空间,换来更高的安全余量,值。
AES有几种工作模式:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| ECB | 简单,但相同明文块产生相同密文块 | 不推荐,有安全风险 |
| CBC | 需要IV,密文块依赖前一块 | 通用场景,但需要处理填充 |
| CTR | 流式加密,不需要填充 | 适合流式数据,性能好 |
| GCM | 同时提供加密和认证 | 强烈推荐,一步到位 |
我在项目里最常用的是AES-256-GCM。为什么?因为它自带认证标签,能同时防窃听和防篡改。省得你额外再算一次HMAC。
序列化与AES加密的结合方式
说白了,流程就三步:
- 序列化:把结构体变成字节数组
- 加密:用AES加密这个字节数组
- 打包:把加密后的数据,加上必要的元信息(比如IV、认证标签),组成最终的数据包
反序列化就是逆过程:拆包、解密、反序列化。
这里有个关键点:加密后的数据长度,和原始数据长度不一定相等。因为AES是块加密,需要填充。如果用GCM模式,还会额外多出认证标签。
所以,数据包的结构要设计好。我一般这样设计:
// 加密数据包结构
typedef struct {
uint8_t iv[12]; // GCM模式需要12字节IV
uint8_t tag[16]; // GCM认证标签,16字节
uint32_t data_len; // 加密后的数据长度
uint8_t encrypted_data[]; // 变长数组,存放密文
} encrypted_packet_t;
你看,接收方先读IV和tag,再读长度,最后读密文。这样就能完整还原。
实战:AES-256-GCM加密序列化数据
下面我写一个完整的例子。假设我们要序列化一个传感器数据结构体,然后加密发送。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <mbedtls/aes.h>
#include <mbedtls/gcm.h>
// 原始数据结构体
typedef struct {
uint32_t device_id;
float temperature;
float humidity;
uint32_t timestamp;
uint8_t status;
} sensor_data_t;
// 加密数据包结构
typedef struct {
uint8_t iv[12];
uint8_t tag[16];
uint32_t encrypted_len;
uint8_t *encrypted_data;
} encrypted_packet_t;
// 序列化 + 加密
int serialize_and_encrypt(const sensor_data_t *data,
const uint8_t *key,
encrypted_packet_t *packet) {
uint8_t buffer[256];
size_t offset = 0;
// 1. 序列化
memcpy(buffer + offset, &data->device_id, sizeof(data->device_id));
offset += sizeof(data->device_id);
memcpy(buffer + offset, &data->temperature, sizeof(data->temperature));
offset += sizeof(data->temperature);
memcpy(buffer + offset, &data->humidity, sizeof(data->humidity));
offset += sizeof(data->humidity);
memcpy(buffer + offset, &data->timestamp, sizeof(data->timestamp));
offset += sizeof(data->timestamp);
buffer[offset++] = data->status;
// 2. 生成随机IV
// 实际项目中用真随机数生成器
for (int i = 0; i < 12; i++) {
packet->iv[i] = rand() & 0xFF;
}
// 3. 加密
mbedtls_gcm_context ctx;
mbedtls_gcm_init(&ctx);
mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 256);
size_t encrypted_len = offset;
packet->encrypted_data = malloc(encrypted_len);
if (!packet->encrypted_data) {
mbedtls_gcm_free(&ctx);
return -1;
}
int ret = mbedtls_gcm_crypt_and_tag(&ctx,
MBEDTLS_GCM_ENCRYPT,
encrypted_len,
packet->iv, 12,
NULL, 0, // 不需要附加数据
buffer, // 明文
packet->encrypted_data, // 密文
16, // tag长度
packet->tag);
if (ret != 0) {
free(packet->encrypted_data);
mbedtls_gcm_free(&ctx);
return -1;
}
packet->encrypted_len = encrypted_len;
mbedtls_gcm_free(&ctx);
return 0;
}
// 解密 + 反序列化
int decrypt_and_deserialize(const encrypted_packet_t *packet,
const uint8_t *key,
sensor_data_t *data) {
uint8_t buffer[256];
mbedtls_gcm_context ctx;
mbedtls_gcm_init(&ctx);
mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 256);
int ret = mbedtls_gcm_auth_decrypt(&ctx,
packet->encrypted_len,
packet->iv, 12,
NULL, 0,
packet->tag, 16,
packet->encrypted_data,
buffer);
if (ret != 0) {
// 认证失败!数据被篡改或密钥不对
mbedtls_gcm_free(&ctx);
return -1;
}
// 反序列化
size_t offset = 0;
memcpy(&data->device_id, buffer + offset, sizeof(data->device_id));
offset += sizeof(data->device_id);
memcpy(&data->temperature, buffer + offset, sizeof(data->temperature));
offset += sizeof(data->temperature);
memcpy(&data->humidity, buffer + offset, sizeof(data->humidity));
offset += sizeof(data->humidity);
memcpy(&data->timestamp, buffer + offset, sizeof(data->timestamp));
offset += sizeof(data->timestamp);
data->status = buffer[offset];
mbedtls_gcm_free(&ctx);
return 0;
}
这段代码里,我用的是mbedTLS库。它在嵌入式里很常见,资源占用小。如果你用其他平台,比如OpenSSL或者硬件加密引擎,逻辑是一样的。
核心要点:GCM模式在解密时会自动验证认证标签。如果数据被篡改,mbedtls_gcm_auth_decrypt会返回非0值。这时候绝对不能信任解密后的数据。
序列化加密的完整流程图
下面这张图,把整个流程串起来了。你看一眼就明白。
避坑指南
我曾经在一个项目里,直接把序列化后的数据用AES-ECB加密就发出去了。结果安全审计没过。为什么?ECB模式下,相同的明文块会产生相同的密文块。攻击者不需要解密,光看密文模式就能猜出数据规律。
所以,千万别用ECB模式。哪怕你只是传一个温度值,也不行。
还有几个坑,我帮你列出来:
- IV不能固定:每次加密都要生成新的随机IV。固定IV会导致相同的明文产生相同的密文,和ECB一样危险。
- 密钥存储要安全:不要把密钥硬编码在代码里。我建议用芯片的OTP区域或者安全元件存储。
- 注意内存清零:加密完成后,明文缓冲区要及时清零。防止被调试器或者内存dump出来。
- 认证标签不能丢:GCM的tag是完整性保证。如果传输过程中tag被截断或丢失,接收端无法验证数据是否被篡改。
小技巧:如果你用的MCU支持硬件AES引擎,一定要用硬件加速。软件算AES-256-GCM,在低主频芯片上可能会吃掉大量CPU时间。我测过,STM32F4用硬件AES,加密1KB数据只需要几十微秒。
警告:不要自己实现AES算法。即使你懂数学,也容易引入侧信道攻击漏洞。永远使用经过验证的库,比如mbedTLS、OpenSSL、或者芯片厂商提供的加密库。
性能考量
加密是有代价的。序列化数据越大,加密耗时越长。我一般这样评估:
| 数据大小 | 软件AES-256-GCM耗时(STM32F4 @168MHz) | 硬件AES-256-GCM耗时 |
|---|---|---|
| 64字节 | ~0.5ms | ~0.02ms |
| 256字节 | ~2ms | ~0.08ms |
| 1KB | ~8ms | ~0.3ms |
| 4KB | ~32ms | ~1.2ms |
你看,硬件加速的优势非常明显。如果你的系统对实时性要求高,比如控制指令必须在1ms内响应,那软件加密可能扛不住。这时候要么用硬件加密,要么只加密关键字段,不加密整个数据包。
我个人习惯是:能硬件加密就硬件加密,不能的话,尽量把数据包控制在256字节以内。这样软件加密的延迟还能接受。
总结
序列化加加密,说白了就是给数据穿上一件防弹衣。序列化负责把数据变成字节流,加密负责让字节流变得不可读、不可篡改。
AES-256-GCM是我最推荐的组合。一次调用,同时搞定加密和认证。你只需要管理好密钥和IV,剩下的交给库函数。
嗯,最后提醒一句:加密不是万能的。它解决的是传输过程中的安全问题。如果设备本身被攻破了,密钥被提取出来,那加密也就形同虚设了。所以,设备本身的安全防护,同样重要。