简介:面向单片机与嵌入式开发者的AES128加解密C语言实现包,聚焦资源受限平台上的对称加密落地问题,涵盖ECB与CBC两种工作模式及PKCS7Padding补齐与去填充逻辑,适合具备一定C基础、需要在固件中集成数据加密功能的初中级嵌入式工程师参考。压缩包共2个文件,包含1个c源文件与1个h头文件,源码与接口声明分离,便于直接移植进现有工程,整体包体约5KB,体积轻量、对Flash与RAM占用友好。资源已有7335人学习下载,具备一定的实践参考价值。内容围绕密钥扩展、分块加密、CBC异或链接与末块填充校验等关键环节展开,读者可据此理解ECB模式在重复明文下的模式泄露风险,以及CBC模式中初始向量对密文差异性的作用,并对照代码梳理解密流程与填充字节的移除判断,为在低资源设备上权衡效率与安全性提供一份可直接研读和二次改造的参考实现。
1. 一份纯 C 的 AES128 实现,在 RAM 只有几 KB 的单片机上值不值
去年帮人看一个采集终端的固件,主控是 Cortex-M0+,Flash 64KB、RAM 8KB,平台下发的配置帧 48 字节,要求链路上不能明文跑。团队第一反应是上通用加密库,交叉编译出来的静态库直接把 Flash 撑爆;换裁剪版本,链接脚本又开始报段溢出。最后落到这份 AES128 ECB、CBC 模式加密解密的 C 语言实现上:纯 C,不依赖 malloc 和操作系统,一个 176 字节轮密钥上下文加 16 字节 IV 就能跑起来。它面向的就是单片机/嵌入式——没有文件系统、没有堆、中断随时会打断你的环境。适合手上有 8 位或 32 位 MCU、只需要对定长小数据块做对称加解密、又不愿意为几十 KB 的库付出 Flash 代价的人。但先别急着抄,ECB 和 CBC 的坑不在算法里,在你怎么用它。
2. 源码结构拆解:算法层、模式层、填充层怎么分文件
拿到一份嵌入式加密代码,我习惯先看它怎么分层,而不是先看 S 盒长什么样。分层决定了你能不能只裁掉一半、能不能在出问题时定位到底是算法错还是模式用错。这份实现的骨架按职责切成了四块:算法核心、模式封装、填充处理、裸机例程。不同版本的命名会有出入,但依赖方向基本是单向的——模式层调算法层,业务调模式层,反过来不存在。
2.1 四个文件的职责边界
目录铺开大概是这样:
aes128/ ├── aes.h 对外头文件:上下文结构体、错误码、全部函数声明 ├── aes.c 算法核心:S 盒、逆 S 盒、密钥扩展、单分组加解密 ├── aes_mode.c ECB / CBC 模式封装,负责 IV 的保存与更新 ├── pkcs7.c 补齐与去补齐,独立成文件方便单独测 ├── pkcs7.h └── main.c 裸机例程:一组明文分别跑 ECB 和 CBC 对照aes.c里只有“一个 16 字节块进、一个 16 字节块出”的逻辑,它不知道 ECB 和 CBC 的存在,也不知道填充是什么。这种做法看起来啰嗦,好处是你可以拿标准测试向量单独验证它,出问题的时候不用怀疑模式层。aes_mode.c是唯一持有 IV 的地方,它调用单分组函数,把前一块的结果链接到下一块。pkcs7.c更独立,输入输出都是字节数组,不碰密钥,可以直接在 PC 上写单元测试跑通再搬进工程。
有一点值得注意:这份代码没有把填充直接塞进 CBC 加密函数里。有些精简实现为了省事,做成cbc_encrypt_with_padding()一把梭,调用是省事了,但解密侧一旦填充校验失败,你分不清是密钥错、IV 错还是数据被截断。拆开之后,每一步的返回值都能单独判断,这是排查问题时最值钱的东西。
2.2 对外 API:从上下文到单分组再到模式封装
头文件是整个项目里最该先读的部分。它定义的上下文结构体只有 192 字节,放 RAM 里压力不大,也可以做成全局静态变量避免占栈。
#ifndef AES128_H #define AES128_H #include <stdint.h> #define AES_BLOCK_SIZE 16u #define AES128_KEY_SIZE 16u #define AES128_ROUNDS 10u /* 返回值:0 成功,非 0 为错误码,业务层按需打印 */ #define AES_OK 0u #define AES_ERR_LEN 1u /* 长度不是 16 的整数倍 */ #define AES_ERR_PARAM 2u /* 空指针或输出缓冲区容量不足 */ #define AES_ERR_PADDING 3u /* 去填充校验失败 */ typedef struct { uint8_t roundKey[176]; /* 11 组 x 16 字节轮密钥 */ uint8_t iv[AES_BLOCK_SIZE]; /* CBC 链接值,ECB 不用 */ } aes128_ctx_t; void aes128_init(aes128_ctx_t *ctx, const uint8_t key[AES128_KEY_SIZE]); void aes128_set_iv(aes128_ctx_t *ctx, const uint8_t iv[AES_BLOCK_SIZE]); void aes128_encrypt_block(const aes128_ctx_t *ctx, const uint8_t in[16], uint8_t out[16]); void aes128_decrypt_block(const aes128_ctx_t *ctx, const uint8_t in[16], uint8_t out[16]); uint32_t aes128_ecb_encrypt(aes128_ctx_t *ctx, const uint8_t *in, uint32_t len, uint8_t *out); uint32_t aes128_ecb_decrypt(aes128_ctx_t *ctx, const uint8_t *in, uint32_t len, uint8_t *out); uint32_t aes128_cbc_encrypt(aes128_ctx_t *ctx, const uint8_t *in, uint32_t len, uint8_t *out); uint32_t aes128_cbc_decrypt(aes128_ctx_t *ctx, const uint8_t *in, uint32_t len, uint8_t *out); int32_t pkcs7_pad(const uint8_t *in, uint32_t len, uint8_t *out, uint32_t cap); int32_t pkcs7_unpad(uint8_t *buf, uint32_t len); #endif /* AES128_H */aes128_init只在上电或换密钥时调一次,它把 16 字节原始密钥扩展成 176 字节轮密钥并存在上下文里,同时把 IV 清零。aes128_encrypt_block和aes128_decrypt_block是内部用的单分组接口,签名里带const是为了让编译器知道 ctx 不会被改动,配合-O2有时能省掉一次内存回写。四个模式函数统一返回uint32_t错误码,而不是返回长度——嵌入式里长度由调用方自己知道,错误码才是容易漏判的东西。pkcs7_pad返回补齐后的总长度,失败返回负数,这是全篇唯一用有符号返回值的地方,因为长度本身可能为 0。
调用顺序上,一个完整的加密流程是:aes128_init→aes128_set_iv(CBC 才需要)→pkcs7_pad→aes128_cbc_encrypt。解密反过来:aes128_cbc_decrypt→pkcs7_unpad。IV 每次加密都要重新设置,这个后面单独讲。
2.3 编译开关:把不用的模式和表从 Flash 里裁掉
嵌入式项目里最怕“全都要”。这套代码留了几个宏,放在单独的配置头里,按板子的 Flash 预算取舍。
/* aes_cfg.h —— 按 Flash 预算裁剪 */ #define AES128_ECB_EN 1 /* 只在加密密钥、随机数时打开 */ #define AES128_CBC_EN 1 /* 协议帧加密必开 */ #define AES128_DECRYPT_EN 1 /* 只做上行加密的节点可以关掉,省逆 S 盒和逆列混合 */ #define AES128_USE_T_TABLE 0 /* 1:T 表换速度;0:256B S 盒 + xtime 换空间 */ #define AES128_CTX_STATIC 1 /* 1:上下文做成静态变量,不占栈 */几个量级心里要有数:S 盒 256 字节,逆 S 盒再 256 字节;AES128_DECRYPT_EN关掉之后,逆 S 盒和整套逆轮函数都不参与链接,代码能小三分之一。T 表方案把轮变换压成 4 次查表和异或,速度快,但 4 张 1KB 表加起来 4KB,解密还要再来 4KB,对 64KB Flash 的芯片是奢侈;S 盒加xtime的写法代码量在 1.5KB 到 2.5KB 之间,速度大概慢三到五倍,普通 48MHz 的 M0 加密一帧 48 字节完全够用。
编译时我一般直接写死优化等级,别指望默认配置:
arm-none-eabi-gcc -mcpu=cortex-m0plus -mthumb -Os \ -ffunction-sections -fdata-sections \ -DAES128_DECRYPT_EN=1 -DAES128_USE_T_TABLE=0 \ aes.c aes_mode.c pkcs7.c main.c -o aes_demo.elf \ -Wl,--gc-sections -Wl,-Map=aes_demo.map-ffunction-sections配合--gc-sections是把没被引用的函数整个丢掉,宏开关管的是源码级裁剪,链接器管的是死代码清除,两者一起用才算干净。-Os而不是-O2,是因为这类代码的瓶颈在查表和异或,不在指令调度,-Os出来的体积差别很明显。编完一定要打开.map看一眼aes.o占了多大、roundKey落在.bss还是.data,这一步比读十遍源码有用。
3. 算法核心落地:密钥扩展与 10 轮变换的 C 写法
AES-128 的结构不复杂:16 字节密钥扩展成 11 组轮密钥,明文和第一组异或,然后跑 9 轮完整变换,最后第 10 轮少一步列混合。真正容易写错的是状态矩阵的行列顺序和每一步的先后,不是数学本身。这一章按执行顺序拆,每段代码都能单独对照测试向量验证。
3.1 密钥扩展:44 个字、176 字节轮密钥怎么排
密钥扩展把 4 个 32 位字扩成 44 个,每 4 个字是一组轮密钥,正好 11 组。第 0 组就是原始密钥,后面每一组由前一组递推。
static const uint8_t sbox[256] = { /* 标准 S 盒,256 字节 */ }; static const uint8_t rcon[10] = { 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1B,0x36 }; void aes128_init(aes128_ctx_t *ctx, const uint8_t key[AES128_KEY_SIZE]) { uint8_t *rk = ctx->roundKey; uint8_t tmp[4]; memcpy(rk, key, AES128_KEY_SIZE); /* 第 0 组轮密钥 = 原始密钥 */ for (uint8_t i = 4; i < 44; i++) { memcpy(tmp, rk + (i - 1) * 4, 4); /* 取上一个字 */ if ((i & 3u) == 0u) { uint8_t t = tmp[0]; /* RotWord:循环左移一个字节 */ tmp[0] = sbox[tmp[1]] ^ rcon[i / 4u - 1u]; /* SubWord + Rcon */ tmp[1] = sbox[tmp[2]]; tmp[2] = sbox[tmp[3]]; tmp[3] = sbox[t]; } rk[i * 4u + 0u] = rk[(i - 4u) * 4u + 0u] ^ tmp[0]; rk[i * 4u + 1u] = rk[(i - 4u) * 4u + 1u] ^ tmp[1]; rk[i * 4u + 2u] = rk[(i - 4u) * 4u + 2u] ^ tmp[2]; rk[i * 4u + 3u] = rk[(i - 4u) * 4u + 3u] ^ tmp[3]; } memset(ctx->iv, 0, AES_BLOCK_SIZE); }循环从第 4 个字开始,到第 43 个字结束。当i是 4 的倍数时,要对上一个字做三步:RotWord 把[a0,a1,a2,a3]变成[a1,a2,a3,a0],SubWord 对四个字节逐个查 S 盒,最后第一个字节再异或轮常量rcon。代码里先把tmp[0]存进t,是因为后面tmp[0]会被覆盖,而原来的tmp[0]要挪到tmp[3]去查表——这个顺序写反了,密钥扩展就是错的,而且错得很隐蔽:前几轮可能看着正常,跑到第 10 轮才全乱。
rcon只有 10 个值,对应 10 次轮密钥生成,0x1B和0x36是 GF(2^8) 里左移溢出的结果,不用推。整个函数没有堆分配、没有递归,RAM 只用了tmp[4]四个字节,栈深度可以忽略。
3.2 字节代换与列混合:256 字节 S 盒和 xtime 的组合
轮变换里有四个操作,S 盒负责非线性,行移位负责扩散,列混合负责在列方向混合,轮密钥异或负责加秘密。行移位是纯字节搬运,写起来最不容易错;列混合涉及 GF(2^8) 乘法,是体积和速度的取舍点。
static uint8_t xtime(uint8_t x) { /* 乘 2:左移一位,最高位为 1 时异或 0x1B(AES 规定的不可约多项式低 8 位) */ return (uint8_t)((x << 1) ^ ((x >> 7) * 0x1Bu)); } static void shift_rows(uint8_t s[16]) { uint8_t t; /* 状态按列存:s[r + 4*c],第 r 行左移 r 字节 */ t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t; t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t; t = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = s[3]; s[3] = t; } static void mix_columns(uint8_t s[16]) { for (uint8_t c = 0; c < 4; c++) { uint8_t *p = s + c * 4; uint8_t a0 = p[0], a1 = p[1], a2 = p[2], a3 = p[3]; uint8_t all = a0 ^ a1 ^ a2 ^ a3; /* 2*a0 ^ 3*a1 ^ a2 ^ a3 的等价写法,每列只调两次 xtime */ p[0] = a0 ^ all ^ xtime(a0 ^ a1); p[1] = a1 ^ all ^ xtime(a1 ^ a2); p[2] = a2 ^ all ^ xtime(a2 ^ a3); p[3] = a3 ^ all ^ xtime(a3 ^ a0); } }xtime是整个算法里被调用最多的辅助函数。AES 的列混合要把每个字节乘 2 和乘 3,乘 3 拆成“乘 2 再异或原值”。代码里(x >> 7)取出最高位,左移之后如果原来是 1,就要异或0x1B把溢出的位折回来,这就是 GF(2^8) 的模运算。用乘法代替查表的好处是省 1KB 表空间,代价是每次多几条移位指令。
shift_rows的写法很容易看晕,记住状态是列优先存储的:索引0,1,2,3是第一列,4,5,6,7是第二列。第 0 行不动,第 1 行(索引 1、5、9、13)左移一格,第 2 行(2、6、10、14)左移两格,第 3 行(3、7、11、15)左移三格。最后那行看着像右移,其实是左移三格等价于右移一格,用四次赋值实现三个元素轮换,省一个临时数组。
mix_columns里那个all技巧值得单独说:标准公式是2a0 ^ 3a1 ^ a2 ^ a3,把3a1拆成2a1 ^ a1,再提取公因式,就变成a0 ^ (a0^a1^a2^a3) ^ 2(a0^a1),每列共享一个all,只需要两次xtime调用。这是我在资源紧张的板子上最喜欢用的写法,省的是指令周期,不是可读性——注释写清楚,半年后自己还看得懂。
3.3 加解密主循环:每一步的先后顺序不能动
单分组加密就三段:初始轮密钥异或、9 轮完整变换、最后 1 轮去掉列混合。
void aes128_encrypt_block(const aes128_ctx_t *ctx, const uint8_t in[16], uint8_t out[16]) { uint8_t s[16]; memcpy(s, in, 16); /* 状态矩阵,列优先 */ add_round_key(s, ctx->roundKey); /* 第 0 组 */ for (uint8_t round = 1; round < AES128_ROUNDS; round++) { sub_bytes(s); /* 查 S 盒 */ shift_rows(s); /* 行移位 */ mix_columns(s); /* 列混合 */ add_round_key(s, ctx->roundKey + round * 16); } sub_bytes(s); /* 第 10 轮:无列混合 */ shift_rows(s); add_round_key(s, ctx->roundKey + 160); memcpy(out, s, 16); }ctx->roundKey + round * 16是轮密钥偏移,第round组从第round*16字节开始,第 10 组在 160 字节处,正好 176 字节用完。局部数组s[16]在栈上,加上函数调用开销,这个函数的栈占用不到 64 字节,但如果你在中断里也调它,中断栈要单独预留。
解密有两种实现路线。一种是完全逆着来:先用第 10 组轮密钥异或,然后 9 轮逆列混合、逆行移位、逆字节代换、加轮密钥,最后一轮去掉逆列混合。另一种是等价逆密码,把逆列混合提前合并进密钥扩展,每轮顺序变成正规的加密形式,代价是密钥扩展要做一次逆列混合。嵌入式里大多选第一种,代码直观,方便对着加密函数逐行核对。
static uint8_t gmul(uint8_t a, uint8_t b) { /* 通用 GF(2^8) 乘法,慢但代码小;只给逆列混合用 */ uint8_t p = 0; while (b) { if (b & 1u) p ^= a; a = xtime(a); b >>= 1; } return p; } static void inv_mix_columns(uint8_t s[16]) { for (uint8_t c = 0; c < 4; c++) { uint8_t *p = s + c * 4; uint8_t a0 = p[0], a1 = p[1], a2 = p[2], a3 = p[3]; p[0] = gmul(a0,14) ^ gmul(a1,11) ^ gmul(a2,13) ^ gmul(a3, 9); p[1] = gmul(a0, 9) ^ gmul(a1,14) ^ gmul(a2,11) ^ gmul(a3,13); p[2] = gmul(a0,13) ^ gmul(a1, 9) ^ gmul(a2,14) ^ gmul(a3,11); p[3] = gmul(a0,11) ^ gmul(a1,13) ^ gmul(a2, 9) ^ gmul(a3,14); } }gmul的循环最多跑 8 次,比查 256 字节的乘法表慢,但省下 64KB 的查表空间(真要建 256x256 表那是灾难)。逆列混合的系数是 14、11、13、9,和前向的 2、3、1、1 对应,这四个数都是 GF(2^8) 里的固定乘数,不用推导,照抄标准即可。如果板上 Flash 够,把gmul换成 4 张小表能快一个数量级,这是典型的空间换时间。
4. ECB 与 CBC 模式封装:IV 管理和 PKCS7Padding 补齐
算法层跑通只是及格线,模式层才是实际项目里翻车最多的地方。ECB 和 CBC 的差别不在加密函数,而在“上一块的输出怎么参与下一块的输入”,另外还有一个绕不开的 PKCS7Padding 补齐算法——AES 只认 16 字节整数倍,业务数据很少正好整除。
4.1 ECB 模式:什么时候能用,什么时候必翻车
ECB 是最直白的模式:每块独立加密,块与块之间没有任何关系。
uint32_t aes128_ecb_encrypt(aes128_ctx_t *ctx, const uint8_t *in, uint32_t len, uint8_t *out) { if (in == NULL || out == NULL) return AES_ERR_PARAM; if ((len % AES_BLOCK_SIZE) != 0u) return AES_ERR_LEN; /* 必须整块 */ for (uint32_t off = 0; off < len; off += AES_BLOCK_SIZE) { aes128_encrypt_block(ctx, in + off, out + off); } return AES_OK; }参数说明:in和out都按字节数组走,长度必须是 16 的整数倍,否则直接返回AES_ERR_LEN而不是悄悄截断——悄悄截断是嵌入式里最难查的 bug 之一。ctx里的 IV 在 ECB 下完全不参与,传进去只是为了复用同一个上下文结构体。
ECB 的问题不在实现,在语义:相同的 16 字节明文块,永远得到相同的密文块。如果业务数据里有大量重复结构,比如每帧开头的设备号、固定长度的状态字,密文里就能直接看出模式。我的判断标准很粗暴:加密对象如果是密钥、随机数、单块令牌这类“本来就该是随机”的数据,ECB 够用;只要涉及多块结构化数据,一律上 CBC。这条线别抱侥幸心理。
4.2 CBC 模式:IV 从哪来、怎么跟着密文走
CBC 的做法是每块明文先和上一块密文异或,再送进加密函数;第一块异或的对象就是 IV。
uint32_t aes128_cbc_encrypt(aes128_ctx_t *ctx, const uint8_t *in, uint32_t len, uint8_t *out) { uint8_t blk[AES_BLOCK_SIZE]; if (in == NULL || out == NULL) return AES_ERR_PARAM; if ((len % AES_BLOCK_SIZE) != 0u) return AES_ERR_LEN; for (uint32_t off = 0; off < len; off += AES_BLOCK_SIZE) { for (uint8_t i = 0; i < AES_BLOCK_SIZE; i++) blk[i] = in[off + i] ^ ctx->iv[i]; /* 和链接值异或 */ aes128_encrypt_block(ctx, blk, out + off); memcpy(ctx->iv, out + off, AES_BLOCK_SIZE); /* 密文回写,作为下一块的链接值 */ } return AES_OK; }关键在最后那行memcpy:加密完一块,立刻把密文写回ctx->iv,下一块就用它做异或。所以 CBC 的加密是串行的,没法并行,这在单核 MCU 上不是问题。IV 必须每次会话重新生成,不能写死成 16 个 0——固定 IV 等于把 CBC 退化成了 ECB 的变种,同一明文每次加密结果都一样,前面的功夫白费。板子上如果有硬件随机数发生器,直接读 16 字节做 IV;没有的话,用密钥加一个递增计数器做一次 ECB 加密派生,也比全零强。
解密方向要多一步备份:
uint32_t aes128_cbc_decrypt(aes128_ctx_t *ctx, const uint8_t *in, uint32_t len, uint8_t *out) { uint8_t blk[AES_BLOCK_SIZE]; uint8_t next_iv[AES_BLOCK_SIZE]; if (in == NULL || out == NULL) return AES_ERR_PARAM; if ((len % AES_BLOCK_SIZE) != 0u) return AES_ERR_LEN; for (uint32_t off = 0; off < len; off += AES_BLOCK_SIZE) { memcpy(next_iv, in + off, AES_BLOCK_SIZE); /* 先存密文块,稍后 IV 要用 */ aes128_decrypt_block(ctx, in + off, blk); /* 解密 */ for (uint8_t i = 0; i < AES_BLOCK_SIZE; i++) out[off + i] = blk[i] ^ ctx->iv[i]; /* 再和上一块密文异或 */ memcpy(ctx->iv, next_iv, AES_BLOCK_SIZE); } return AES_OK; }next_iv这个临时变量不能省。因为in和out有可能是同一块缓冲区(原地解密),如果先解密再回头去读in里的密文,那块数据已经被覆盖了。把密文先备份出来,是这个函数里唯一一处“看起来多余但不能删”的代码。
IV 的传输也有讲究。CBC 的 IV 不需要保密,但要保证接收方拿到的是发送方这次真正用的值,通常做法是把 16 字节 IV 明文拼在密文前面,接收方先取出 IV、aes128_set_iv(),再解密后面的数据。别图省事把 IV 塞进aes128_ctx_t就完事,跨帧复用上下文会导致上一帧的链接值污染下一帧。
4.3 PKCS7Padding 补齐:补齐规则与去补齐校验
AES 只接受 16 字节整数倍,业务数据长度是任意的,所以加密前要补、解密后要去。PKCS7Padding 补齐算法的规则一句话说完:缺几个字节就补几个“缺的字节数”。
int32_t pkcs7_pad(const uint8_t *in, uint32_t len, uint8_t *out, uint32_t cap) { uint8_t pad = (uint8_t)(AES_BLOCK_SIZE - (len % AES_BLOCK_SIZE)); /* 1..16 */ if (in == NULL || out == NULL) return -1; if ((uint32_t)len + pad > cap) return -1; /* 输出缓冲区不够,直接失败 */ if (in != out) memcpy(out, in, len); memset(out + len, pad, pad); /* 补 pad 个 pad 值 */ return (int32_t)(len + pad); } int32_t pkcs7_unpad(uint8_t *buf, uint32_t len) { uint8_t pad; if (buf == NULL || len == 0u) return -1; if ((len % AES_BLOCK_SIZE) != 0u) return -1; pad = buf[len - 1u]; if (pad == 0u || pad > AES_BLOCK_SIZE) return -1; /* 越界直接判非法 */ for (uint32_t i = 0; i < pad; i++) { if (buf[len - 1u - i] != pad) return -1; /* 逐字节校验,别只看最后一个 */ } return (int32_t)(len - pad); }最容易错的是len正好是 16 的整数倍时。16 - (len % 16)算出来是 16,也就是补一整块 0x10,这是对的;如果写成16 - len % 16再特判成 0,接收端去填充时读到 0 就会判非法。补齐函数返回补齐后的总长度,调用方拿这个值去调 CBC 加密,别自己再算一遍。
去填充的逐字节校验不能省。只检查最后一个字节、不检查前面pad-1个字节,等于把合法性判断交给运气——数据被截断或者密钥错了,最后一个字节碰巧是0x05,你就返回一个错误长度,后面的业务代码拿着错位的数据继续跑,症状会飘到很远的地方去。这段循环最多 16 次,开销可以忽略。
一个完整的调用例子:
uint8_t frame[64]; uint8_t cipher[64]; int32_t n; aes128_init(&ctx, key); aes128_set_iv(&ctx, iv); /* 每帧重新设置 */ n = pkcs7_pad(plain, plain_len, frame, sizeof(frame)); if (n < 0) { /* 缓冲区不够,报错 */ } if (aes128_cbc_encrypt(&ctx, frame, (uint32_t)n, cipher) != AES_OK) { /* 长度不是 16 倍数,理论上走不到这里 */ }顺序别搞反:先补齐再加密,先解密再去填充。中间任何一步失败都要
本文还有配套的精品资源,点击获取