news 2026/9/8 11:45:13

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用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^8+x^4+x^3+x+1为模的有限域里。

理解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^8+x^4+x^3+x+1 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基础上引入了一个随机初始向量IV(16字节),每个明文块在加密前先与上一个密文块异或,让相同明文块在不同位置产生不同密文,还通过一条依赖链把单个比特的错误传播到后续所有块。实现成本很低,安全性显著提升。做生产环境加密时我的建议是:直接默认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。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 11:44:14

汽车评论多标签情感分析实战:从TF-IDF到深度学习融合

简介&#xff1a;这是CCF-BDCI 2018年汽车行业用户观点主题及情感识别挑战赛第7名解决方案的完整Python项目&#xff0c;面向机器学习、自然语言处理方向的竞赛选手和求职开发者&#xff0c;可用来学习如何从用户评论中识别主题和情感倾向。压缩包共39个文件&#xff0c;以31个…

作者头像 李华
网站建设 2026/9/8 11:42:51

智能车视觉组工程复盘:走马观碑赛项的闭环调试与避坑指南

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

作者头像 李华
网站建设 2026/9/8 11:42:51

Ubuntu 20下open62541与Qt构建OPC UA服务器/客户端实践

简介&#xff1a;面向工业自动化与智能制造领域的QT/C开发者&#xff0c;这份资源聚焦Ubuntu 20环境下基于open62541库的OPC UA服务器与客户端搭建&#xff0c;帮助读者跨越协议理解与工程实现的障碍&#xff0c;适合需要快速上手或参考工程结构的初中级开发人员。压缩包共12个…

作者头像 李华
网站建设 2026/9/8 11:42:33

用机器学习做Web日志异常检测:命令行工具实战指南

简介&#xff1a;面向 Web 运维与安全分析场景&#xff0c;这份资源提供了一款基于机器学习的日志统计分析与异常检测命令行工具完整工程&#xff0c;适合正在做项目开发、毕业设计或课程设计的学生及入门开发者参考复现。压缩包共 65 个文件&#xff0c;约 10.58MB&#xff0c…

作者头像 李华
网站建设 2026/9/8 11:41:28

深度学习细胞计数实战:基于PyTorch的密度图回归方案

简介&#xff1a;这是一份基于Python深度学习的细胞数目识别与计数项目资料&#xff0c;源自数字图像处理课程大作业&#xff0c;适合希望入门深度学习图像分割与计数的学习者&#xff0c;也可作为毕业设计、课程设计或工程实训参考。项目基于TensorFlow与Keras框架实现U-Net细…

作者头像 李华
网站建设 2026/9/8 11:40:19

ECC技能包爆火解析:内存纠错、MBIST与RAS监控实战

8个月25万星&#xff0c;一个人维护&#xff0c;还自带争议buff——这个配置无论放在哪个技术社区都足够炸裂。我第一眼看到ECC技能包这个项目冲上热榜的时候&#xff0c;还以为又是哪个潮流框架在搞营销&#xff0c;点进去才发现&#xff0c;它讲的不是Web开发&#xff0c;不是…

作者头像 李华