AES128 ECB与CBC模式C实现

AI助手已提取文章相关产品:

AES128 ECB、CBC模式加密解密函数(C语言实现 - 单片机/嵌入式)

在物联网设备日益普及的今天,从智能门锁到工业传感器,大量终端每天都在处理敏感数据。这些设备往往运行在资源极其有限的单片机上——Flash 只有几十KB,RAM 不过几KB,连标准库都难以完整支持。然而,安全需求却一点没打折扣:用户密码不能明文存储,通信报文不能被嗅探,固件更新必须防篡改。

于是问题来了:我们能否在一个没有操作系统的裸机环境中,用纯 C 实现一套真正可用的 AES 加解密模块?不仅要小,还得安全、可移植、易于审计。

答案是肯定的。本文将带你一步步构建一个适用于 STM32、ESP32、nRF 等主流 MCU 的轻量级 AES-128 实现,重点聚焦 ECB CBC 两种工作模式,并深入剖析它们在实际应用中的取舍。


AES(Advanced Encryption Standard)自 2001 年成为 NIST 标准以来,早已取代 DES 成为对称加密的事实标杆。它采用 128 位分组长度,支持 128、192、256 三种密钥长度。其中 AES-128 因其在安全性与性能之间的优秀平衡,成为嵌入式领域的首选。

它的核心是一个基于“代换-置换网络”(SPN)的迭代结构,每轮包含四个关键步骤:

  • SubBytes :通过 S-Box 对每个字节进行非线性替换,引入混淆;
  • ShiftRows :行内循环移位,增强扩散;
  • MixColumns :在伽罗瓦域 GF(2⁸) 上对列做线性变换,进一步打乱数据关系;
  • AddRoundKey :将当前状态与轮密钥异或。

整个加密过程共 10 轮,初始先执行一次 AddRoundKey ,随后进行 9 轮完整操作,最后一轮省略 MixColumns 。这种设计确保了即使明文仅有一位差异,密文也会呈现完全随机的分布。

为了在单片机上高效运行,我们可以选择不使用查表法(T-table),虽然会牺牲一些速度,但换来的是极低的内存占用和清晰的逻辑结构,更适合调试和安全审查。

下面是一个简化版的单块 AES-128 加密函数实现:

const uint8_t sbox[256] = {
    0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
    /* ... 完整 S-Box 表略,共 256 字节 */
};

uint8_t galois_mul2(uint8_t x) {
    return (x << 1) ^ ((x & 0x80) ? 0x1b : 0x00);
}

void aes128_encrypt(const uint8_t* input, uint8_t* output, const uint8_t* w) {
    uint8_t state[16];
    memcpy(state, input, 16);

    // 初始轮密钥加
    for (int i = 0; i < 16; i++) {
        state[i] ^= w[i];
    }

    // 主循环:9轮完整操作
    for (int round = 1; round < 10; round++) {
        // SubBytes
        for (int i = 0; i < 16; i++) {
            state[i] = sbox[state[i]];
        }

        // ShiftRows
        uint8_t t;
        t = state[1]; state[1]=state[5]; state[5]=state[9]; state[9]=state[13]; state[13]=t;
        t = state[2]; state[2]=state[10]; state[10]=t; t=state[6]; state[6]=state[14]; state[14]=t;
        t = state[3]; state[3]=state[15]; state[15]=state[11]; state[11]=state[7]; state[7]=t;

        // MixColumns
        uint8_t col[4];
        for (int c = 0; c < 4; c++) {
            col[0] = state[c +  0];
            col[1] = state[c +  4];
            col[2] = state[c +  8];
            col[3] = state[c + 12];

            state[c +  0] = galois_mul2(col[0]) ^ galois_mul2(col[1]) ^ col[1] ^ col[2] ^ col[3];
            state[c +  4] = galois_mul2(col[1]) ^ galois_mul2(col[2]) ^ col[2] ^ col[3] ^ col[0];
            state[c +  8] = galois_mul2(col[2]) ^ galois_mul2(col[3]) ^ col[3] ^ col[0] ^ col[1];
            state[c + 12] = galois_mul2(col[3]) ^ galois_mul2(col[0]) ^ col[0] ^ col[1] ^ col[2];
        }

        // AddRoundKey
        for (int i = 0; i < 16; i++) {
            state[i] ^= w[16 * round + i];
        }
    }

    // 第10轮:无 MixColumns
    for (int i = 0; i < 16; i++) {
        state[i] = sbox[state[i]];
    }
    // ShiftRows 同上
    uint8_t t = state[1]; state[1]=state[5]; state[5]=state[9]; state[9]=state[13]; state[13]=t;
    t = state[2]; state[2]=state[10]; state[10]=t; t=state[6]; state[6]=state[14]; state[14]=t;
    t = state[3]; state[3]=state[15]; state[15]=state[11]; state[11]=state[7]; state[7]=t;

    for (int i = 0; i < 16; i++) {
        state[i] ^= w[160 + i]; // 最后一轮密钥
    }

    memcpy(output, state, 16);
}

这个版本虽然未使用 T-table 优化,但在 Cortex-M0 等低端芯片上仍可接受。更重要的是,它的每一步都是透明的,便于验证正确性和排查侧信道风险。

接下来,真正的挑战来了:如何把这块“砖”砌进墙里?也就是选择合适的工作模式。

ECB 模式:简单但危险

ECB(Electronic Codebook)可能是最直观的模式:每个 16 字节的明文块独立加密,互不影响。

void aes_ecb_crypt(const uint8_t* input, uint8_t* output, size_t length,
                   const uint8_t* key, int encrypt) {
    uint8_t expanded_key[176];
    if (encrypt) {
        aes128_expand_key(key, expanded_key);
        for (size_t i = 0; i < length; i += 16) {
            aes128_encrypt(input + i, output + i, expanded_key);
        }
    } else {
        aes128_expand_key_decrypt(key, expanded_key);
        for (size_t i = 0; i < length; i += 16) {
            aes128_decrypt(input + i, output + i, expanded_key);
        }
    }
}

看起来干净利落,甚至可以并行处理。但问题也正出在这里—— 相同的明文块总是生成相同的密文块

想象一下你正在加密一张黑白图像。如果某个区域全是白色像素,那么对应的多个明文块完全相同,加密后也会得到一串重复的密文块。攻击者即使不知道具体内容,也能看出原始数据的结构。这就是所谓的“大象图”效应——加密后的图像轮廓依然可见。

因此,除非你在加密的内容本身就是高熵的(比如另一个加密算法的输出、随机密钥或 nonce),否则绝不该使用 ECB。

那它还有用武之地吗?有。例如,在安全启动流程中,你可以用 ECB 加密一个由硬件 RNG 生成的临时密钥,然后用它来解密后续的固件镜像。此时输入是真正随机的,重复概率几乎为零,ECB 的确定性反而成了优势。

CBC 模式:实用主义的选择

CBC(Cipher Block Chaining)才是大多数场景下的合理选择。它通过引入“链式依赖”打破了 ECB 的重复性缺陷。

其核心思想很简单: 每一个明文块在加密前,先与前一个密文块异或 。第一个块则与一个称为 IV(初始化向量)的随机值异或。

加密过程如下:

C₀ = IV  
Cᵢ = AES_Encrypt(Key, Pᵢ ⊕ Cᵢ₋₁)

解密时反过来:

Pᵢ = AES_Decrypt(Key, Cᵢ) ⊕ Cᵢ₋₁

注意,IV 不需要保密,但必须满足两个条件: 唯一性 不可预测性 。换句话说,同一把密钥下,永远不要重复使用同一个 IV。

下面是 CBC 模式的实现:

void aes_cbc_crypt(const uint8_t* input, uint8_t* output, size_t length,
                   const uint8_t* key, uint8_t* iv, int encrypt) {
    uint8_t expanded_key[176];
    uint8_t prev_block[16];

    aes128_expand_key(key, expanded_key);

    if (encrypt) {
        if (iv == NULL) {
            // 生产环境应使用真随机源,如 HAL_RNG_GenerateRandomNumber()
            memset(prev_block, 0x55, 16); // 示例填充
        } else {
            memcpy(prev_block, iv, 16);
        }
        memcpy(output, prev_block, 16); // 将IV写入输出头部

        for (size_t i = 0; i < length; i += 16) {
            for (int j = 0; j < 16; j++) {
                prev_block[j] ^= input[i + j];
            }
            aes128_encrypt(prev_block, output + 16 + i, expanded_key);
            memcpy(prev_block, output + 16 + i, 16);
        }
    } else {
        memcpy(prev_block, input, 16); // 第一块是IV

        for (size_t i = 16; i < length + 16; i += 16) {
            uint8_t temp[16];
            memcpy(temp, input + i, 16);
            aes128_decrypt(input + i, output + i - 16, expanded_key);
            for (int j = 0; j < 16; j++) {
                output[i - 16 + j] ^= prev_block[j];
            }
            memcpy(prev_block, temp, 16);
        }
    }
}

可以看到,加密输出包含了前置的 IV(16 字节),接收方只需读取前 16 字节即可还原上下文。这也意味着你每次传输的数据会多出 16 字节开销,但对于大多数应用场景来说完全可以接受。

实际系统中的集成建议

在一个典型的嵌入式通信链路中,AES-CBC 通常位于协议栈的应用层之下:

[传感器数据打包]
       ↓
[PKCS#7 填充 + AES-CBC 加密]
       ↓
[加入CRC校验 → 发送至LoRa/BLE/Wi-Fi]

几个关键设计点值得强调:

1. 填充策略

由于 AES 是分组密码,输入长度必须是 16 字节的倍数。推荐使用 PKCS#7 填充:若缺少 N 字节,则补上 N 个值为 N 的字节。

例如,明文缺 3 字节,则末尾添加 0x03 0x03 0x03 。解密后检查最后几个字节是否构成合法填充,再予以移除。

2. IV 的生成与管理

强烈建议使用硬件随机数发生器(RNG)生成 IV。STM32、ESP32 等平台均提供此类外设。切勿用时间戳或计数器这类可预测的方式生成 IV。

此外,IV 应随每次通信重新生成,哪怕是在同一个会话中。

3. 密钥保护

最忌讳的就是把密钥写死在代码里。理想做法是在生产阶段通过安全烧录工具注入密钥,或者配合 ECDH 等密钥协商协议动态生成会话密钥。

如果你的芯片支持 TrustZone 或 Secure Element,优先利用这些硬件机制来保护密钥。

4. 性能优化方向

  • 在性能敏感场景,可启用 T-table 查表法,将 SubBytes + ShiftRows + MixColumns 合并为 4 个 256 项查找表,速度提升可达 3~5 倍。
  • 若目标 MCU 支持硬件 AES 引擎(如 STM32F4/F7/L4+),直接调用底层驱动,不仅能提速,还能有效抵御时序攻击。
  • 配合 DMA 使用,实现数据流的零拷贝处理。

回过头看,AES 本身并不复杂,真正决定安全水位的是 你怎么用它 。ECB 的陷阱提醒我们:密码学不是“用了就安全”,而是“正确使用才安全”。

对于绝大多数嵌入式开发者而言, AES-128 + CBC + 随机 IV + PKCS#7 填充 是一个足够稳健的基础组合。它既不会过度消耗资源,又能抵御常见的被动攻击。

未来若需更高安全性,可逐步引入 CTR 模式实现流式加密,或采用 GCM 模式获得认证加密能力(AEAD),从而同时保障机密性与完整性。

但无论如何演进,理解这些基础模式的原理与边界,始终是构建可信系统的起点。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值