新闻详情

新闻详情

首页 / 资讯中心 / 详情

用C语言实现AES-128:从原理到工程实践的完整指南

发布时间:2026/9/8 11:45:22来源:尧图网络
用C语言实现AES-128:从原理到工程实践的完整指南
简介面向嵌入式开发者与密码学学习者的 AES-128 加密算法 C 语言实现包源自 STM32 平台数据保护与传输加密的常见需求适合直接集成到 MCU 工程中也可用于理解对称加密从原理到落地的完整过程。压缩包共 2 个文件包括一个 C 源文件和一个配套头文件整体约 5KB结构清晰、没有多余依赖。源文件内部实现了字节代换、行位移、列混淆、密钥扩展等核心步骤覆盖加解密主体流程头文件则声明了调用接口、常量定义与数据结构便于其他模块快速接入。该实现可作为独立模块嵌入项目也可对照代码逐步理解块密码各轮运算的细节为后续实现 CBC、GCM 等其他模式打下基础移植适配时只需根据目标平台的字节序和硬件加速能力做少量调整即可在 STM32 等环境中高效运行。已有 5921 人学习下载对需要落地 AES-128 或巩固加密原理的开发者具有直接的参考价值。1. 从最小需求说起为什么要用C语言写AES-128我最早接触AES-128并不是因为论文要求而是当时接手的一个嵌入式项目需要给设备通信数据做加密。那会儿板子资源紧张到离谱主控芯片主频几十兆赫兹内存以KB算压根跑不动OpenSSL这类重型库就算交叉编译过去也有一堆依赖和许可证问题。翻了一圈资料发现AES-128这种算法恰好是硬件资源友好型用标准C语言几百行就能实现既不依赖操作系统也不依赖任何第三方库一把编译链就能跑起来天然适合资源受限场景。从那时候起我就开始琢磨C语言实现AES-128这件事后来陆续优化过好几版踩了不少坑这里把完整思路整理一遍权当给自己留个笔记也希望能帮到正在折腾同样事情的人。AES-128的核心参数值得先交代清楚分组长度128位16字节密钥长度128位16字节迭代轮数10轮。128位密钥意味着密钥空间高达2的128次方以当前算力暴力破解基本是不可能完成的任务。需要特别留意的是“分组长度”和“密钥长度”这两个概念——AES的兄弟版本AES-192和AES-256密钥长度分别是192和256位但它们的分组长度仍然是128位只有迭代轮数分别增加到12轮和14轮。这个参数关系在实现时必须记住否则不同版本之间很容易混淆。适合看这篇文章的人大概有两类一类是做嵌入式、单片机、网络协议栈的开发者需要在没有现成库里自己实现加密逻辑另一类是刚接触现代密码学、想搞懂算法内部到底怎么运作的学生或技术人员。这两种人需求不完全一样前者追求能用、跑得快、占得少后者追求理解透彻、能推导、能改。这篇内容尽量同时满足两边原理讲清楚代码也给到位。2. 先解决认知问题AES-128到底在做什么网上讲AES的教程不少但很多上来就扔一堆术语看了等于没看。实际上AES-128做的事情可以这样理解它把16字节的明文当作一个4×4的二维数组每个格子是一个字节然后对这个数组进行一系列搅拌操作搅拌的关键参数就是16字节的密钥。每一次搅拌叫做一轮AES-128总共搅拌10轮。这里的“搅拌”不是普通的洗牌而是由四个固定操作组成的变换链每轮依次执行字节代换SubBytes、行移位ShiftRows、列混合MixColumns、轮密钥加AddRoundKey。第10轮特殊一些不执行列混合。这四个操作各自解决不同层面的问题单独看都很简单组合起来就是一道坚固的屏障。字节代换是非线性变换这是整个算法里最关键的步骤它通过一个叫做S盒的查找表把每个字节映射为另一个字节。为什么要非线性因为如果整个变换都是线性的那么攻击者可以列出一系列线性方程来破解密钥非线性变换直接把这条路堵死。行移位是把状态矩阵的每一行按不同偏移量循环左移目的是让数据在不同列之间充分扩散。列混合是在每一列上做数学变换把某一位的变化扩散到整列。轮密钥加就是拿扩展出来的轮密钥和状态矩阵做异或。如果你玩了几年密码学会发现这些操作背后有一个指导思想叫“混淆与扩散”这是Shannon在1949年提出的理论框架AES的设计完美践行了这两个原则。字节代换负责混淆让密文和明文、密钥之间的关系高度复杂化行移位和列混合负责扩散让明文中一个比特的变化尽可能快地传播到整个状态矩阵。轮密钥加则确保每一轮的变换都受到密钥的调控——没有这一步加密过程就不依赖密钥也就毫无意义了。2.1 状态矩阵先把数据摆上操作台实现AES的第一步是把输入输出组织成矩阵结构。AES处理的最小信息单位是字节16字节的明文不是线性排列的而是以列为主序填入一个4×4矩阵。比如明文的前4个字节填充第0列接下来的4个字节填充第1列以此类推。// 以列主序方式建立状态矩阵 // state[r][c] 表示第 r 行、第 c 列 for (int c 0; c 4; c) { for (int r 0; r 4; r) { state[r][c] input[c * 4 r]; } }很多初学者包括当年的我容易在这里栽跟头直接把明文顺序一行一行填进去结果密文和标准测试向量全对不上。记住C语言的二维数组在内存中是行优先存储的但AES状态矩阵在逻辑上是列优先定义的读标准文档时特别注意这个差别。这是第一个需要刻在脑子里的坑。2.2 字节代换与S盒一张决定安全底线的表S盒是AES中唯一一张固定的查找表也是算法中唯一的非线性部分总共256个字节。它的生成背后有一套数学逻辑先计算每个字节在GF(2^8)有限域上的乘法逆元再做一次仿射变换两个步骤叠加保证了非线性度高、差分均匀性好。实际实现过程中不需要每次都现场算逆元直接把查表代码写死即可。static const uint8_t sbox[256] { 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, // ... 共256个字节 }; void sub_bytes(uint8_t state[4][4]) { for (int r 0; r 4; r) for (int c 0; c 4; c) state[r][c] sbox[state[r][c]]; }这个查表操作本质上是拿字节值当作数组下标直接取替换后的值不需要任何计算。不过要提醒一句S盒的256个字节在整个AES协议里是全局共享的任何一个字节写错都会导致加密结果完全错误所以从网上粘贴S盒时务必跟FIPS 197标准文档逐一核对。我最开始用的一个来源把第36个字节写成了0x20排错排了一整天。2.3 行移位让扩散发生在行与行之间行移位操作不涉及任何计算纯粹是挪动字节位置。规则只有一条第0行不动第1行循环左移1字节第2行循环左移2字节第3行循环左移3字节。void shift_rows(uint8_t state[4][4]) { uint8_t tmp; // 第1行左移1位 tmp state[1][0]; state[1][0] state[1][1]; state[1][1] state[1][2]; state[1][2] state[1][3]; state[1][3] tmp; // 第2行左移2位、第3行左移3位同理 }2.4 列混合与GF(2^8)域工程中最容易绕晕的数学部分列混合是在每一列上执行一个固定的线性变换把列中每个字节重新组合。它的数学表示是拿一个固定矩阵去乘状态矩阵的每一列所有运算都发生在GF(2^8)——一个以不可约多项式x^8x^4x^3x1为模的有限域里。理解GF(2^8)域可以借用模运算的概念平时我们做整数运算有模12的时钟算术9点加4点是1点因为超出了12就绕回来GF(2^8)域里也是类似逻辑只不过每个数是8位二进制数加法就是按位异或乘法是多项式乘法后取模。这让域中的乘法和普通乘法完全不同它需要先乘完再做一次模约减。代码实现时不需要真的去做多项式除法可以用查表法——预计算一个256×16的乘法表直接查。更常见的做法是直接实现域乘法函数代码量更小但每次列混合都要调用几十次性能略低。两种方案各有适用场景嵌入式更倾向查表PC上随便选哪种都行。uint8_t gmul(uint8_t a, uint8_t b) { uint8_t p 0; for (int i 0; i 8; i) { if (b 1) p ^ a; uint8_t hi_bit a 0x80; a 1; if (hi_bit) a ^ 0x1b; // 模不可约多项式 x^8x^4x^3x1 b 1; } return p; }0x1b这个常数对应的就是不可约多项式去掉最高项后的低8位这是AES数学结构里一个标志性数字。函数逻辑虽然短但这是列混合操作的重心所在。2.5 轮密钥加每一轮都要与密钥打交道轮密钥加是AES四个操作中最简单的一个把扩展出来的16字节轮密钥按列拼成4×4矩阵与状态矩阵逐字节异或。需要注意的是AES-128总共需要11组16字节的轮密钥——初始白化层用1组10轮迭代每轮用1组所以密钥扩展总共要产出176字节的数据。void add_round_key(uint8_t state[4][4], uint8_t *round_key) { for (int c 0; c 4; c) for (int r 0; r 4; r) state[r][c] ^ round_key[c * 4 r]; }3. 密钥扩展从16字节的种子长出176字节的轮密钥AES的密钥扩展承担了一个关键任务把用户提供的16字节原始密钥扩展成11轮各自独立的轮密钥避免每轮都用同一组密钥导致加密强度下降。扩展算法从原始密钥出发以一个4字节为单位逐字生成新密钥字每个新字依赖前一个字和相隔4个位置的字每满4个字节时就要做一次特殊的g函数变换。g函数包含三个步骤循环左移一个字节、逐字节过S盒、异或一个轮常数Rcon。Rcon是每轮不同的固定值它的作用是打破轮与轮之间的对称性防止不同轮产生相同模式的密钥字。void key_expansion(const uint8_t *key, uint8_t *round_keys) { // 先复制原始密钥到轮密钥缓冲区 for (int i 0; i 16; i) round_keys[i] key[i]; int bytes_generated 16; uint8_t temp[4]; while (bytes_generated 176) { for (int i 0; i 4; i) temp[i] round_keys[bytes_generated - 4 i]; if (bytes_generated % 16 0) { // g函数左移、S盒、Rcon异或 uint8_t temp_byte temp[0]; temp[0] sbox[temp[1]] ^ rcon[bytes_generated / 16]; temp[1] sbox[temp[2]]; temp[2] sbox[temp[3]]; temp[3] sbox[temp_byte]; } for (int i 0; i 4; i) { round_keys[bytes_generated] round_keys[bytes_generated - 16] ^ temp[i]; bytes_generated; } } }这段代码的核心逻辑是“每隔16字节做一次g函数变换”对应到代码里就是bytes_generated % 16 0这个条件。如果把这个条件漏掉扩展出来的密钥会完全不对但程序本身不会报错属于最难排查的“逻辑隐身型bug”。建议实现完第一件事就用标准测试向量里的扩展密钥做比对。4. 加密主流程十个轮次的代码骨架怎么搭把前面几个原语模块拼装起来整个加密流程就清晰了。AES-128加密共10轮轮次处理逻辑如下void aes128_encrypt(const uint8_t *plaintext, const uint8_t *key, uint8_t *ciphertext) { uint8_t state[4][4]; uint8_t round_keys[176]; // 初始化状态矩阵列主序 for (int c 0; c 4; c) for (int r 0; r 4; r) state[r][c] plaintext[c * 4 r]; key_expansion(key, round_keys); // 第0轮只用轮密钥加白化 add_round_key(state, round_keys); // 第1到9轮完整四部曲 for (int round 1; round 9; round) { sub_bytes(state); shift_rows(state); mix_columns(state); add_round_key(state, round_keys round * 16); } // 第10轮跳过列混合 sub_bytes(state); shift_rows(state); add_round_key(state, round_keys 10 * 16); // 输出同样列主序 for (int c 0; c 4; c) for (int r 0; r 4; r) ciphertext[c * 4 r] state[r][c]; }注意两个容易出错的细节一是第0轮的白化操作要提前在循环外做不能把循环从0开始否则初始密钥加会被错误地安排在列混合之前二是最后一轮不能执行列混合这一点和很多其他分组算法的规则不同是最容易被初学者忽略的边界条件。丢了这个排除条件加密结果也会跟标准向量完全对不上。从工程角度讲这样一个实现放在任何C99兼容编译器上都能编译不依赖标准库之外的内容只需要stdint.h定义uint8_t类型连内存动态分配都不需要。我后来在STM32、ESP32以及x86 Linux上交叉编译运行过同一份代码除了字节序处理差异外核心代码几乎没改过。5. 解密流程对称性背后藏着什么AES的加解密共用同一套密钥扩展逻辑解密时只需要把加密的四个操作全部取逆并且反序执行。具体来说解密轮次依次是逆轮密钥加、逆行移位、逆字节代换、逆列混合轮密钥的使用顺序是倒过来的——第10轮的轮密钥最先用初始密钥最后用。5.1 逆字节代换从逆S盒开始解密要用到另一个查找表叫做逆S盒InvSBox它和S盒的关系是sbox[inv_sbox[x]] x。实现上和加密完全对称查同一张表就能完成逆映射。某些开源实现里用了数学计算来实时生成逆S盒省存储但增加计算嵌入式场景我更推荐直接静态定义256字节的查找表。5.2 逆行移位往右挪回去逆行移位方向和加密相反第0行不动第1行循环右移1字节第2行右移2字节第3行右移3字节。也可以理解为循环左移4-行号字节效果等价。5.3 逆列混合GF域上的“除法”逆列混合的矩阵和加密不同系数不再只是1、2、3而是变成了9、11、13、14这几个数。这也是很多实现里把解密代码单独写一份的原因——如果试图共用列混合函数参数一换就很容易搞混。域乘法函数gmul可以复用只需改变传入的系数即可。void inv_mix_columns(uint8_t state[4][4]) { for (int c 0; c 4; c) { uint8_t a0 state[0][c], a1 state[1][c]; uint8_t a2 state[2][c], a3 state[3][c]; state[0][c] gmul(a0, 14) ^ gmul(a1, 11) ^ gmul(a2, 13) ^ gmul(a3, 9); state[1][c] gmul(a0, 9) ^ gmul(a1, 14) ^ gmul(a2, 11) ^ gmul(a3, 13); state[2][c] gmul(a0, 13) ^ gmul(a1, 9) ^ gmul(a2, 14) ^ gmul(a3, 11); state[3][c] gmul(a0, 11) ^ gmul(a1, 13) ^ gmul(a2, 9) ^ gmul(a3, 14); } }解密的主流程就是上述操作的逆序组合代码框架和加密一模一样循环次数也是10轮最后一轮同样跳过逆列混合——注意是“加密的最后一轮”对应到解密是逆序后的第0轮。6. 验证与调试用一个测试向量确认实现正确实现完一套AES代码最重要的事情不是写注释而是验证它对不对。AES官方文档FIPS 197的附录B给出了一个非常经典的标准测试向量明文、密钥、加密结果都有明文值拿这个向量跑一遍就知道实现是否正确。以标准向量为例密钥为2B7E151628AED2A6ABF7158809CF4F3C明文为6BC1BEE22E409F96E93D7E117393172A正确的加密结果为3AD77BB40D7A3660A89ECAF32466EF97。我每写完一版AES实现第一件事就是把这段数据喂进去比对输出任何一字节对不上都说明实现有bug。这个方法也在密钥扩展阶段适用——FIPS 197附录A还提供了每轮轮密钥的完整序列可以逐轮检查扩展是否正确。除了标准向量还要注意几个边界场景全0密钥加密全0明文的结果是66E94BD4EF8A2C3B884CFA59CA342B2E可以用来做额外校验连续性测试也很重要用同一把密钥加密多组不同的明文确认输出没有明显的相似性最直接的检查是加解密还原测试——先加密一段数据再进行解密结果必须等于原文。这三个维度都过了你的实现基本就稳了。7. 工程化经验从跑通到上生产还差这几步一套能跑的标准实现和一套能上生产的实现中间隔着的不是一个等级的工作量。基于我踩过的坑有几点值得单独拿出来说。7.1 数据填充模式不是算法的事AES规定分组长度是16字节但业务数据很少恰好是16字节的倍数。最后一段数据不够16字节时怎么办这就涉及填充模式。最常见的PKCS#7填充规则是缺几个字节就补几个值为几的字节。缺1个补1个0x01缺3个补3个0x03如果数据恰好是16字节的倍数也必须额外补满16个0x10——否则解密时无法判断末尾哪些是真实数据哪些是填充。这个“强制补一整个块”的设计让很多人费解但它恰恰是为了消除歧义。7.2 工作模式从ECB到CBC的必然选择AES本身只能加密固定16字节的块要把任意长度的消息加密成一个整体需要定义“工作模式”。ECB模式把每个16字节块独立加密实现极其简单但存在一个安全隐患——相同的明文块会产生相同的密文块攻击者可以从中分析出明文模式图片加密后甚至还能看出原图的轮廓这直接暴露了数据分布特征。CBC模式在ECB基础上引入了一个随机初始向量IV16字节每个明文块在加密前先与上一个密文块异或让相同明文块在不同位置产生不同密文还通过一条依赖链把单个比特的错误传播到后续所有块。实现成本很低安全性显著提升。做生产环境加密时我的建议是直接默认CBC模式并且IV必须每次随机生成绝不能写死否则加密强度会大打折扣。7.3 性能优化从功能正确到效率可用标准C语言的逐字节实现已经能覆盖多数场景但在高吞吐网络或资源极度紧张的MCU上可以做一些针对性优化。最常用的是T表法把SubBytes、ShiftRows、MixColumns三步合并在一个查表操作中完成查找表预先算好每轮每列只需要4次查表和4次异或性能可以提升几倍。代价是表比较大4张1KB的表占用4KB存储空间小型MCU上需要权衡。另一个容易忽略的优化点是编译器参数——开启-O2级别的优化通常能带来20%以上的性能提升这在调试版本和发布版本之间会产生明显差异。7.4 安全性边界别指望AES解决所有问题最后说一个在工程里经常被忽略的点AES只负责数据机密性不负责完整性验证和身份认证。攻击者可以篡改密文即使解密后数据变成乱码接收方也未必能立刻识破。正确的做法是采用GCM等带认证的AEAD模式或者手动配合HMAC做消息认证。使用AES-ECB或AES-CBC协作的系统一定要有独立于加密之外的完整性校验机制这才算是完整方案。8. 写在最后的一个小把戏调试AES代码时有个很实用的小技巧利用标准向量做“逐步比对”。不要只在最终结果上比对把第一轮的AddRoundKey之后、第一轮SubBytes之后、第一轮ShiftRows之后的状态矩阵逐阶段打印出来与FIPS 197附录B中列出的中间值对照能快速定位到底哪个环节出了问题。很多时候bug出在第几轮、哪个操作单元一眼就能看出来比自己盲猜快得多。这套方法几乎适用于所有有标准参考实现的密码算法也不局限于AES。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

SpringBoot学生选课管理系统毕设实战:选课约束与数据库设计详解 2026/9/8 12:30:27

SpringBoot学生选课管理系统毕设实战:选课约束与数据库设计详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Function Calling设计取舍笔记:从参数约束到错误恢复的实战经验 2026/9/8 12:30:27

Function Calling设计取舍笔记:从参数约束到错误恢复的实战经验

不知道你有没有这种经历:模型明明已经接好了外部工具,结果调用的时候要么参数填得乱七八糟,要么该调函数的时候不调、不该调的时候硬调。我在做一个内部助手项目时,为了给模型加上查天气、查库存、创建工单这几个基础能力&#xf…

阅读更多 →
UG NX挖掘机工作装置三维建模与虚拟装配实战指南 2026/9/8 12:30:27

UG NX挖掘机工作装置三维建模与虚拟装配实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
嵌入式C语言面试:指针、数组与野指针高频考点全解析 2026/9/8 12:30:27

嵌入式C语言面试:指针、数组与野指针高频考点全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Claude Code四大痛点终结者:开源增强方案全解析 2026/9/8 12:30:27

Claude Code四大痛点终结者:开源增强方案全解析

最近这一两个月,我几乎每天都要在终端里跟 Claude Code 打交道。代码审查、快速重构、写提交信息、补单元测试,确实能省不少事。但用着用着,问题也一个接一个冒出来:上下文窗口说爆就爆,API 账单肉眼可见地涨&#xff…

阅读更多 →
AI学术写作工具测评:9款利器助你高效完成开题报告 2026/9/8 12:27:27

AI学术写作工具测评:9款利器助你高效完成开题报告

开题季又到了,实验室里弥漫着一股焦虑又亢奋的气氛。我翻了翻最近的后台留言,问得最多的一个问题是:"师兄,开题报告到底该用什么AI工具?怎么别人三天就搞定了,我搞了一周还在文献综述里打转&#xff1…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞