AES-128加密算法C语言实现:原理、代码与嵌入式实战 📅 发布时间:2026/9/8 12:25:33 👁 浏览次数: 简介AES-128加密算法的C语言实现源码面向嵌入式、物联网及安全通信场景适合有一定C语言基础、希望为STM32等项目加入数据加密能力的开发者AES-128也是目前广泛使用的对称加密标准之一在资源受限环境中仍能保持较高效率。资源包仅含2个文件AES128.c源文件与AES128.h头文件共5KB代码精炼且可移植。实现覆盖字节代换、行位移、列混淆、密钥扩展等全部核心操作并提供ECB模式下的加解密流程可直接编译运行或嵌入现有工程。压缩包整体轻量方便快速部署目前已有5921人学习下载由作者fuyun514分享通过阅读这份代码可以深入理解对称加密的底层运作逻辑包括加密与解密过程如何反向执行四项基本操作、密钥扩展如何生成每轮子密钥以及ECB模式分块处理的特性。同时移植注意事项涵盖字节序适配、内存调整等为在多种MCU平台上复用与二次开发提供清晰指引也可作为学校或培训机构的密码学教学案例。 做嵌入式或者安全相关开发的同行大概率在某个项目里被问过一句话AES-128加密算法不是现成的库一调就行你非要用C语言从头写一遍图什么问这种话的人多半没在资源受限的单片机上接过加密任务。AES-128加密算法用C语言实现这件事在工程上的价值远不止“会写一个算法”这么简单——它牵扯到数据结构怎么设计、查表还是算S盒、轮密钥怎么放、内存对齐怎么做以及后续怎么去对接CBC、GCM这类分组模式。如果用一句话定义AES-128就是把128位明文分成16个字节按字节做10轮字节代换、行移位、列混淆和轮密钥加最终输出128位密文。一套干净的C语言实现代码量基本控制在300到600行之间不依赖任何第三方库非常适合作嵌入式加密模块或者密码学入门研究的底稿。这篇文章写给两类人最合适一类是在单片机、工业控制、通信模组项目里需要内置加密能力但又不想背整个OpenSSL依赖的开发者另一类是学完C语言基础想找一个“既不算太简单又不至于劝退”的综合练习项目把数组、指针、位运算、多维数组一次练明白的同学。下面直接按我实际动手写的思路来讲。1. AES-128的核心原理四步轮函数不是随便定的1.1 状态矩阵把字节摆进棋盘AES处理的不是连续的比特流而是先摆成一个4x4的字节矩阵官方叫State。128位等于16字节按“列优先”的顺序填进去// 原始明文00112233445566778899aabbccddeeff // 状态矩阵按列填入 uint8_t state[4][4] { {0x00, 0x04, 0x08, 0x0c}, {0x01, 0x05, 0x09, 0x0d}, {0x02, 0x06, 0x0a, 0x0e}, {0x03, 0x07, 0x0b, 0x0f} };很多人第一次看会觉得别扭为什么不按行填因为在AES的设计里列是列混淆的最小操作单位行是行移位的最小操作单位这两个变换的几何结构决定了初始填充顺序。这个摆法是FIPS-197标准规定的不是代码里可以随意改的。我见过太多自己实现到一半发现结果不对的排查到最后九成是初始填充或者最后读出顺序摆错了。整个加密过程就是对这个矩阵做10轮变换。每一轮由四个子步骤组成SubBytes、ShiftRows、MixColumns、AddRoundKey最后一轮少做MixColumns。1.2 字节代换SubBytes与S盒SubBytes是AES里唯一的非线性变换也是最核心的“混淆”手段。它做的操作是状态矩阵里每个字节通过S盒映射成另一个字节。S盒不是随机造出来的。它是先在GF(2^8)有限域上求每个字节的乘法逆元再做一次仿射变换得到的。这两个操作合在一起决定了S盒具备良好的非线性避免短周期、避免弱密钥的统计特征这也是AES能扛住各种密码分析的基础。对C语言实现来说运行时去生成S盒是完全没必要的事。直接用预计算的256字节S盒查表就行。解密用的逆S盒也要一并准备好。这里贴出标准S盒前32字节方便你写完以后验证63 7c 77 7b f2 6b 6f c5 30 01 67 2b fe d7 ab 76 ca 82 c9 7d fa 59 47 f0 ad d4 a2 af 9c a4 72 c0如果你的代码输出到这32个字节之前就对不上基本可以断定S盒表错了别急着往下查。1.3 行移位ShiftRows与列混淆MixColumnsShiftRows的逻辑一句话就能说清状态矩阵第0行不动第1行循环左移1字节第2行左移2字节第3行左移3字节。它本身没有计算量意义在于把同一列的字节打散到不同列让信息在后续列混淆里充分扩散。MixColumns才是AES里真正“有数学含量”的操作。每一列的4个字节被当作GF(2^8)域上的多项式系数乘上一个固定多项式再模约减。落到代码里可以用下面这组公式表示加号就是异或乘法是GF(2^8)乘法r0 2*s0 ^ 3*s1 ^ 1*s2 ^ 1*s3; r1 1*s0 ^ 2*s1 ^ 3*s2 ^ 1*s3; r2 1*s0 ^ 1*s1 ^ 2*s2 ^ 3*s3; r3 3*s0 ^ 1*s1 ^ 1*s2 ^ 2*s3;那GF(2^8)乘法怎么算实际工程里几乎不会用通用多项式乘法去做而是用xtime这个技巧。所谓xtime就是把一个字节左移一位如果最高位原来是1就异或0x1b完成模约减。因为AES选用的不可约多项式是x^8 x^4 x^3 x 1对应十六进制就是0x1b。1.4 轮密钥加AddRoundKey与密钥扩展AddRoundKey最简单就是状态矩阵和轮密钥矩阵逐字节异或。它本身没有扩散能力但因为是密钥和数据的碰撞点放在每一轮里反复做能让密钥每一位对最终密文的每一位都产生影响。AES-128的密钥是16字节但10轮加密一共需要11个轮密钥每个16字节。所以需要密钥扩展从初始密钥开始通过“g函数”不断生成下一轮密钥。g函数做三件事把4字节循环左移1字节、逐个过S盒、首字节异或轮常量Rcon。轮常量是一组固定值一般只用到前10个。2. C语言实现前的几个关键设计决策2.1 状态矩阵用一维还是二维状态矩阵可以是uint8_t state[4][4]也可以直接用一个16字节的uint8_t buf[16]自己管理下标。两种我都写过最终推荐用一维数组。原因很简单AES一轮操作的边界是16字节用一维数组可以用memcpy直呼数据块初始填充、轮密钥加、密文输出都少一层坐标换算的混乱二维数组阅读代码时更直观但在实际工程里数据来源是一个缓冲区你总得做一轮“字节坐标重排”这个重排才是最容易出bug的地方。用一维数组下标直接按i 4 * j映射到行和列代码短排查也快。2.2 S盒查表还是现算S盒必须用查表。用公式在运行时生成S盒逻辑复杂且不易验证性能还差。预计算的S盒和逆S盒放成两个static const数组编译后进只读区RAM一点不占。MixColumns就有得选了。可以用xtime现算也可以做成查表。xtime方案省ROM、代码直观适合RAM和Flash都非常紧张的低端MCU查表方案则是一次性把一个列变换的4个表每个表256字节准备好性能能提升好几倍。有人在Cortex-M0上实测纯xtime版本的加密速度只有查表版本的三分之一左右。如果Flash不紧张优先用查表。2.3 轮密钥怎么存放最简单可靠的办法是预先算好全部11个轮密钥存在一个二维数组round_keys[11][16]里。密钥扩展只调用一次后续加解密都能复用。不要在每一轮加密时才去现场扩展密钥那样既费时又让代码逻辑更绕。尤其注意解密时的轮密钥是反着用的但算法并不是简单把加密密钥倒过来就行因为每一轮内部变换的调用次序也不同。预先扩展好全部轮密钥解密时从第10轮往第0轮扫是最清晰的实现方式。3. 实操写一个能跑通的AES-128加密函数3.1 用NIST测试向量搭骨架动手写之前先把标准测试向量准备好。NIST的FIPS-197附录提供了一个经典向量密钥2b7e151628aed2a6abf7158809cf4f3c 明文6bc1bee22e409f96e93d7e117393172a 密文3ad77bb40d7a3660a89ecaf32466ef97整个调试过程就用这一组。如果最后加密结果能对上你的实现就是标准兼容的。建议不要拿自己随便编的字符串去测因为你无法判断结果是“一定对”还是“看着像加密了”。NIST向量是唯一判断标准。3.2 密钥扩展函数的实现密钥扩展是全工程里最容易写串的部分。伪代码逻辑如下第0轮密钥就是初始密钥16字节从第1轮开始每轮先取出上一轮密钥的最后4字节做一次g函数变换再和上一轮密钥前4字节异或得到当前轮的起始4字节剩下的12字节则按每4字节一组与上一轮对应位置的4字节异或。核心代码static void g_function(uint8_t w[4], int round) { uint8_t t w[0]; w[0] sbox[w[1]] ^ rcon[round]; w[1] sbox[w[2]]; w[2] sbox[w[3]]; w[3] sbox[t]; } void aes128_key_expand(const uint8_t key[16], uint8_t round_keys[11][16]) { for (int i 0; i 16; i) { round_keys[0][i] key[i]; } for (int round 1; round 10; round) { uint8_t w[4]; for (int j 0; j 4; j) { w[j] round_keys[round - 1][j 12]; } g_function(w, round); for (int j 0; j 4; j) { round_keys[round][j] round_keys[round - 1][j] ^ w[j]; } for (int j 4; j 16; j) { round_keys[round][j] round_keys[round - 1][j] ^ round_keys[round][j - 4]; } } }rcon数组按轮次索引第一轮用0x01第二轮用0x02第三轮用0x04按指数倍递增到第10轮是0x36只取低字节参与运算。3.3 加密主循环别漏了最后一轮的特殊处理写加密函数时注意轮次主循环长这样void aes128_encrypt_block(uint8_t block[16], const uint8_t round_keys[11][16]) { add_round_key(block, round_keys[0]); for (int round 1; round 9; round) { sub_bytes(block); shift_rows(block); mix_columns(block); add_round_key(block, round_keys[round]); } // 最后一轮没有 MixColumns sub_bytes(block); shift_rows(block); add_round_key(block, round_keys[10]); }很多新手把MixColumns放进了每一轮结果最后一轮多混淆了一次密文死活对不上NIST向量。强烈建议把前9轮和最后一轮分开写或者至少加一个if (round ! 10)判断一眼就能看清楚边界在哪。3.4 解密流程倒过来不等于逆序照抄解密是把SubBytes、ShiftRows、MixColumns、AddRoundKey全部取逆但顺序不是把加密倒过来直接跑。因为InvMixColumns和AddRoundKey之间满足一定的交换关系工程实现一般会专门写一套解密轮函数InvSubBytes、InvShiftRows、InvMixColumns轮密钥从第10轮往第0轮取。如果你只是学习算法原理解密先放一放也可以。但如果你的模块需要“加密-解密”闭环测试那就必须一次把两个方向都调通。调试技巧是先加密出一个密文再拿同样的密钥去解密看能不能还原出原文再和NIST向量交叉验证。4. 常见问题与排查技巧实录4.1 最容易踩的8个坑现象根本原因排查方法加密结果和NIST向量对不上S盒表或者逆S盒表写错打印S盒前32字节和标准值对比只有最后一轮密文错最后一轮忘记跳过MixColumns检查轮循环边界前几轮加密对、后面错密钥扩展中g函数的循环移位或Rcon处理有误展开打印每轮轮密钥前8字节做比对解密结果是一堆乱码轮密钥使用顺序反了确认解密循环从round 10到0取轮密钥char类型参与移位出现符号扩展平台上char默认有符号右移补1全部改用uint8_t加密断电后重新上电结果不对轮密钥在运行时被意外修改把round_keys声明为const或每次上电重新扩展64位平台上运行比预期慢编译器未优化S盒表没对齐加O2/O3优化必要时用memcpy替代强制转换RAM爆了把S盒和轮密钥数组放在了堆栈或全局RAM声明为static const放Flash只读区第二行的场景我实际遇到过一次。一个同事的AES模块加密一串数据前面几个块结果正确后面全错。查了两天最后发现是因为他把密钥扩展函数当成每块只调用一次但工程代码里有一处流程在循环里重复调用了key_expand导致轮密钥被重新覆盖。记住密钥扩展只需要执行一次别放在每块加密的循环里。4.2 调试时的三个有效手段第一逐轮打印。AES的每一轮变换都明确可预测NIST文档里连中间轮的状态都给了。你在每个子步骤后打印状态矩阵和标准中间值做比对一旦哪一步开始不一致问题就定位在那一步。第二打印密钥扩展结果。密钥扩展本身独立于明文只依赖密钥。先把扩展出来的11个轮密钥全部打印出来和论文里的Known Answer Test对比即便加密过程有问题也能先排除密钥扩展的嫌疑。第三用已知S盒值做单元测试。写一个小函数只测SubBytes一个操作输入{0x00, 0x01, 0x02, ...}输出应为{0x63, 0x7c, 0x77, ...}。这个测试能快速揪出S盒表的坐标错误。5. 性能优化与嵌入式移植经验5.1 我实测过的性能数据在STM32F103 72MHz上纯查表实现的AES-128加密一块16字节数据开-O2优化后大约在60微秒左右如果在Cortex-M4 168MHz上大概能压到20微秒以内。作为对照纯xtime现算列混淆的版本在72MHz下要跑到200微秒以上差别非常明显。如果你的目标是批量加密几KB数据这个差距会被放大得非常可观。所以能查表就查表能上硬件加速指令就上硬件加速指令。AES-NI、Cortex-M的硬件加密单元、ESP32的AES协处理器这些都是能调就调纯软件实现要留给没有硬件加速的场合。5.2 四个立竿见影的优化手段表驱动MixColumns是第一个优化点。把列混淆的4组乘法系数提前展开成4个256字节的表分别是0x02、0x03、0x01、0x01对应AES公式每一列的处理从4次xtime级联变成4次查表加异或吞吐量立刻上一个台阶。第二个点是循环展开。AES加密的10轮循环内部操作高度重复手动展开全部分轮是不现实的但至少可以把最内层的字操作写完交给编译器的O2优化去自动展开。实测O0和O2之间性能差距可能有三倍。第三个点是在内存允许的平台上用uint32_t数组操作状态矩阵。一次读4字节少做大量单字节内存访问。但注意大小端问题小端平台直接读取时字节序和AES标准定义的字节序相反处理不当反而引入bug新手慎用。第四个点是轮密钥对齐。把round_keys数组用__attribute__((aligned(4)))或C11的alignas对齐到4字节在Cortex-M这类平台上能显著减少非对齐访问的惩罚。6. 从单块到产品接上CBC模式与PKCS7填充6.1 为什么不建议直接用ECB模式ECB模式就是把每一块明文独立用AES加密块之间互不影响。问题是相同明文块会产生完全相同的密文块这在图形、固件、结构化数据里会泄露信息分布。最典型的例子是加密一张位图ECB密文仍然能看清轮廓。所以实用系统里尽量别裸用ECB。最常见的是CBC模式。CBC先把当前明文块与上一个密文块异或再加密解密则是先解密再与上一个密文块异或。第一个块用的“上一个密文块”叫初始向量IV。6.2 CBC模式的最小实现基于前面的aes128_encrypt_block封装一个CBC加密并不难void aes128_cbc_encrypt(uint8_t *buf, size_t len, const uint8_t key[16], const uint8_t iv[16]) { uint8_t round_keys[11][16]; uint8_t prev[16]; aes128_key_expand(key, round_keys); memcpy(prev, iv, 16); for (size_t off 0; off len; off 16) { for (int i 0; i 16; i) { buf[off i] ^ prev[i]; } aes128_encrypt_block(buf off, round_keys); memcpy(prev, buf off, 16); } }注意三个前提len必须是16的倍数所以加密前要先用PKCS7把数据补齐IV不需要保密但每次加密必须随机且不重复否则会泄露明文差异解密函数的逻辑不一样要先解密再异或上一块密文不能简单套用加密代码。PKCS7填充规则也很简单缺几个字节就补几个字节补的值就是缺的字节数。比如明文28字节需要补4字节就补4个0x04。如果明文正好是16的倍数还要额外补一整块16字节的0x10否则解密时无法判断最后一块到底是数据还是填充。我在实际写这个模块的时候最大的一个坑是逆S盒表里有一个字节的值抄错了。只看最终解密结果完全没法定位因为每个错误都会像雪崩一样扩散到整块数据。后来把明文、密钥、每轮状态全部打印出来跟FIPS-197文档里的中间值逐轮比对才发现是表的问题。所以所有做加解密算法的人第一步永远先用标准测试向量跑通再去做性能调优和封装。最后再分享一个小技巧把加解密函数写完后加一个自检函数开机跑一遍NIST向量不对就报错。嵌入式设备长期运行最怕“表面正常、数据全毁”这个自检能省下无数现场排查的功夫。本文还有配套的精品资源点击获取