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加密的结合方式

说白了,流程就三步:

  1. 序列化:把结构体变成字节数组
  2. 加密:用AES加密这个字节数组
  3. 打包:把加密后的数据,加上必要的元信息(比如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-256-GCM 加密流程图 发送端 原始结构体数据 序列化 → 字节数组 AES-256-GCM 加密 打包 IV + Tag + 密文 网络传输 接收端 拆包 → IV + Tag + 密文 AES-256-GCM 解密 反序列化 → 结构体 使用解密后的数据 认证失败 → 丢弃数据,记录日志 密钥:发送端和接收端预先共享相同的256位密钥

避坑指南

我曾经在一个项目里,直接把序列化后的数据用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,剩下的交给库函数。

嗯,最后提醒一句:加密不是万能的。它解决的是传输过程中的安全问题。如果设备本身被攻破了,密钥被提取出来,那加密也就形同虚设了。所以,设备本身的安全防护,同样重要。

公众号:蓝海资料掘金营,微信deep3321