C语言从零实现AES-128加密算法:原理详解与代码实战

📅 2026/7/28 10:41:26
C语言从零实现AES-128加密算法:原理详解与代码实战
1. 项目概述与核心价值最近在整理一些旧项目时发现很多涉及数据安全传输和存储的模块其核心都离不开一个老朋友——AES加密算法。无论是嵌入式设备间的通信还是桌面应用对配置文件的保护AES因其高效、安全且被广泛支持的特性成为了事实上的对称加密标准。虽然现在有很多成熟的开源库如OpenSSL, mbedTLS可以直接调用但亲手用C语言从零实现一遍AES的加密和解密过程对于深入理解其算法原理、内存操作以及提升对密码学的直观感受有着不可替代的价值。这不仅仅是完成一个功能更像是一次对计算机底层运算和密码学设计的深度探索。这个项目适合所有对密码学感兴趣、希望夯实C语言功底或者需要在资源受限环境如无标准库的MCU中实现加密功能的开发者。通过这个过程你将不仅仅得到一段可运行的AES代码更能透彻理解字节代换SubBytes、行移位ShiftRows、列混合MixColumns和轮密钥加AddRoundKey这四大核心步骤是如何环环相扣将一段明文转化为密文的。我们会从AES-128密钥长度128位这个最常用的版本开始一步步拆解并提供可直接集成使用的代码模块。2. AES-128算法原理深度拆解在动手写代码之前我们必须先搞清楚AES到底在做什么。AESAdvanced Encryption Standard高级加密标准是一种分组密码算法它把待加密的数据分成固定长度的“块”BlockAES固定为128位即16字节然后用一个密钥Key这里用128位对这个块进行多轮复杂的变换。2.1 状态矩阵与初始映射AES算法内部并不直接处理一维的字节数组而是将其视为一个4x4的二维矩阵称为“状态”State。这个状态矩阵是按列优先的顺序填充的。假设我们的16字节明文输入是in[0], in[1], ..., in[15]那么状态矩阵state[r][c]r为行c为列的填充方式如下state[0][0] in[0], state[1][0] in[1], state[2][0] in[2], state[3][0] in[3] // 第一列 state[0][1] in[4], state[1][1] in[5], state[2][1] in[6], state[3][1] in[7] // 第二列 ... 以此类推理解这个映射关系至关重要因为后续所有操作都是基于这个4x4状态矩阵进行的。加密过程结束后我们再按同样的列优先顺序将状态矩阵还原为一维字节数组输出。2.2 轮函数详解四大核心步骤对于AES-128完整的加密过程包含10轮Round运算。第1轮到第9轮每轮都包含四个步骤最后一轮第10轮缺少列混合步骤。解密过程则是这些步骤的逆序逆运算。1. 字节代换SubBytes这是一个非线性变换是AES能够抵抗各种密码分析的关键。它通过一个被称为S盒Substitution-box的查找表将状态矩阵中的每一个字节替换成另一个字节。这个S盒是经过精心设计的提供了良好的非线性特性。在代码实现中我们会预先计算好这个256字节的S盒数组查表即可完成替换效率极高。逆操作查的是逆S盒InvSubBytes。注意S盒的设计是公开的但其背后涉及有限域GF(2^8)上的求逆和仿射变换。对于我们实现而言直接使用标准的规定值即可无需自己计算生成但理解其数学背景能让你明白为何这样设计。2. 行移位ShiftRows这是一个线性变换目的是让字节在行间扩散。操作很简单第0行不移位。第1行循环左移1个字节。第2行循环左移2个字节。第3行循环左移3个字节。 这个操作打破了每列字节的独立性让加密效果更快地扩散到整个数据块。逆操作是循环右移相应的位数。3. 列混合MixColumns这是加密过程中最复杂的步骤它对状态矩阵的每一列进行独立的变换。可以将其理解为在GF(2^8)有限域上将每一列与一个固定的多项式c(x)进行模乘运算。这个运算可以表示为一个矩阵乘法。同样我们会通过查表使用事先计算好的xtime表或组合查表法来高效实现而不是在现场进行复杂的有限域运算。逆操作是乘以另一个固定的多项式d(x)。4. 轮密钥加AddRoundKey这是最简单的一步将当前的状态矩阵与一个本轮专用的“轮密钥”Round Key进行逐字节的异或XOR操作。轮密钥是从初始的主密钥通过一个称为“密钥扩展”Key Expansion的算法派生出来的一系列128位密钥。每一轮使用的轮密钥都不同这大大增加了安全性。2.3 密钥扩展Key Expansion密钥扩展算法将输入的128位主密钥扩展成11个128位的轮密钥用于初始轮密钥加和后续10轮运算。扩展过程也涉及S盒替换、轮常量Rcon异或等操作。这部分需要单独的函数来实现并且通常会在加密/解密初始化时一次性计算好所有轮密钥并存储起来避免在每轮中重复计算。3. C语言实现的核心模块与数据结构设计理解了原理我们就可以开始设计代码结构了。一个清晰、模块化的设计会让调试和理解变得容易。3.1 基础类型与状态定义首先我们定义一些基础类型和状态矩阵。为了清晰和可移植性我们使用uint8_t来表示字节。#include stdint.h // 用于uint8_t等类型 // AES-128 密钥长度和块大小字节 #define AES_KEYLEN 16 #define AES_BLOCKLEN 16 // 状态矩阵4行Nb列Nb固定为4代表4个字即16字节 typedef uint8_t state_t[4][4];状态矩阵state_t是一个4x4的二维数组我们将围绕它进行操作。3.2 核心变换函数的实现1. SubBytes 与 InvSubBytes我们首先定义标准的S盒和逆S盒数据较长此处仅示意static const uint8_t sbox[256] { 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, // ... 剩余240个值需补全完整的256字节 }; static const uint8_t inv_sbox[256] { 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, // ... 剩余240个值 }; void SubBytes(state_t *state) { for (int i 0; i 4; i) { for (int j 0; j 4; j) { (*state)[i][j] sbox[(*state)[i][j]]; } } } void InvSubBytes(state_t *state) { for (int i 0; i 4; i) { for (int j 0; j 4; j) { (*state)[i][j] inv_sbox[(*state)[i][j]]; } } }2. ShiftRows 与 InvShiftRows实现行移位需要注意边界处理使用循环移位。void ShiftRows(state_t *state) { uint8_t temp; // 第1行循环左移1位 temp (*state)[1][0]; (*state)[1][0] (*state)[1][1]; (*state)[1][1] (*state)[1][2]; (*state)[1][2] (*state)[1][3]; (*state)[1][3] temp; // 第2行循环左移2位 - 等价于交换两对字节 temp (*state)[2][0]; (*state)[2][0] (*state)[2][2]; (*state)[2][2] temp; temp (*state)[2][1]; (*state)[2][1] (*state)[2][3]; (*state)[2][3] temp; // 第3行循环左移3位 - 等价于循环右移1位 temp (*state)[3][3]; (*state)[3][3] (*state)[3][2]; (*state)[3][2] (*state)[3][1]; (*state)[3][1] (*state)[3][0]; (*state)[3][0] temp; } void InvShiftRows(state_t *state) { uint8_t temp; // 第1行循环右移1位 temp (*state)[1][3]; (*state)[1][3] (*state)[1][2]; (*state)[1][2] (*state)[1][1]; (*state)[1][1] (*state)[1][0]; (*state)[1][0] temp; // 第2行循环右移2位 - 交换与ShiftRows相同 temp (*state)[2][0]; (*state)[2][0] (*state)[2][2]; (*state)[2][2] temp; temp (*state)[2][1]; (*state)[2][1] (*state)[2][3]; (*state)[2][3] temp; // 第3行循环右移3位 - 等价于循环左移1位 temp (*state)[3][0]; (*state)[3][0] (*state)[3][1]; (*state)[3][1] (*state)[3][2]; (*state)[3][2] (*state)[3][3]; (*state)[3][3] temp; }3. MixColumns 与 InvMixColumns这是实现中最易出错的部分。我们采用查表法优化。首先实现有限域GF(2^8)上的xtime函数乘以x即{02}static inline uint8_t xtime(uint8_t x) { return ((x 1) ^ (((x 7) 1) * 0x1b)); }然后实现单列的混合运算。加密时每一列[a0, a1, a2, a3]^T经过如下矩阵乘法[02 03 01 01] [a0] [01 02 03 01] * [a1] [01 01 02 03] [a2] [03 01 01 02] [a3]我们可以手动计算每一步但更高效的做法是使用预先计算好的T表这里为简化展示直接计算版本void MixColumns(state_t *state) { for (int i 0; i 4; i) { // 处理每一列 uint8_t t (*state)[0][i]; uint8_t Tmp (*state)[0][i] ^ (*state)[1][i] ^ (*state)[2][i] ^ (*state)[3][i]; uint8_t Tm (*state)[0][i] ^ (*state)[1][i]; Tm xtime(Tm); (*state)[0][i] ^ Tm ^ Tmp; Tm (*state)[1][i] ^ (*state)[2][i]; Tm xtime(Tm); (*state)[1][i] ^ Tm ^ Tmp; Tm (*state)[2][i] ^ (*state)[3][i]; Tm xtime(Tm); (*state)[2][i] ^ Tm ^ Tmp; Tm (*state)[3][i] ^ t; Tm xtime(Tm); (*state)[3][i] ^ Tm ^ Tmp; } }解密时的逆列混合矩阵不同实现也更为复杂通常直接使用另一个基于xtime和有限域乘法的计算序列或者使用组合的查表法。为了代码清晰我们可以单独实现InvMixColumns函数其核心是乘以{0e, 0b, 0d, 09}等系数这可以通过多次调用xtime和异或来实现。4. AddRoundKey这一步最简单但要注意轮密钥的排列顺序。轮密钥也是一个4x4的矩阵直接按字节异或。void AddRoundKey(state_t *state, const uint8_t *round_key) { for (int i 0; i 4; i) { for (int j 0; j 4; j) { (*state)[i][j] ^ round_key[i 4 * j]; // 注意轮密钥的索引方式 } } }这里有一个关键细节轮密钥在内存中通常是以一维数组uint8_t round_key[16]形式存储的其列优先顺序与状态矩阵的填充顺序一致。所以round_key[0]对应状态state[0][0]round_key[4]对应state[0][1]以此类推。上面代码中的索引i 4 * j正是为了匹配这个顺序。3.3 密钥扩展算法的实现密钥扩展是独立的且至关重要的模块。它将16字节的主密钥扩展成11个轮密钥共176字节。// 轮常量表 Rcon 实际只需要前10个值 static const uint8_t Rcon[11] { 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 }; void KeyExpansion(const uint8_t *key, uint8_t *round_keys) { uint8_t temp[4]; int i 0; // 第一个轮密钥就是原始密钥 for (i 0; i AES_KEYLEN; i) { round_keys[i] key[i]; } // 生成后续轮密钥 while (i (AES_BLOCKLEN * (AES_NR 1))) { // AES_NR10 共11轮密钥 // 取前一个轮密钥的最后4个字节 for (int j 0; j 4; j) { temp[j] round_keys[i - 4 j]; } if (i % AES_KEYLEN 0) { // 密钥扩展核心函数RotWord, SubWord, Rcon // 1. 循环左移一个字节 (RotWord) uint8_t t temp[0]; temp[0] temp[1]; temp[1] temp[2]; temp[2] temp[3]; temp[3] t; // 2. S盒替换 (SubWord) for (int j 0; j 4; j) { temp[j] sbox[temp[j]]; } // 3. 与轮常量异或 temp[0] ^ Rcon[i / AES_KEYLEN]; } // 对于AES-256这里还有额外判断AES-128不需要 // 生成新的4个字节与前一个轮密钥的对应4字节异或 for (int j 0; j 4; j) { round_keys[i] round_keys[i - AES_KEYLEN] ^ temp[j]; i; } } }这段代码是AES-128密钥扩展的标准实现。RotWord是循环左移SubWord是S盒替换然后与Rcon异或最后再与上一轮密钥的对应部分异或得到新的轮密钥材料。理解这个过程有助于明白轮密钥之间的非线性关系是如何建立的。4. 加密与解密流程的完整整合有了所有的基础函数我们现在可以将它们组装成完整的加密和解密流程。我们需要一个上下文结构体来保存扩展后的轮密钥。4.1 定义AES上下文结构typedef struct { uint8_t round_keys[(AES_NR 1) * AES_BLOCKLEN]; // 存储扩展后的所有轮密钥 } AES_ctx;4.2 加密主函数Cipher加密函数遵循标准的AES流程初始轮密钥加 - 9轮完整轮函数 - 最后一轮无MixColumns。void AES_EncryptBlock(AES_ctx *ctx, const uint8_t *input, uint8_t *output) { state_t state; uint8_t round 0; // 1. 将输入明文拷贝到状态矩阵列优先 for (int i 0; i 4; i) { for (int j 0; j 4; j) { state[i][j] input[i 4 * j]; } } // 2. 初始轮密钥加 AddRoundKey(state, (ctx-round_keys[round * AES_BLOCKLEN])); round; // 3. 前9轮完整运算 for (; round AES_NR; round) { SubBytes(state); ShiftRows(state); MixColumns(state); AddRoundKey(state, (ctx-round_keys[round * AES_BLOCKLEN])); } // 4. 第10轮最后一轮无MixColumns SubBytes(state); ShiftRows(state); AddRoundKey(state, (ctx-round_keys[round * AES_BLOCKLEN])); // round此时为10 // 5. 将状态矩阵拷贝到输出列优先 for (int i 0; i 4; i) { for (int j 0; j 4; j) { output[i 4 * j] state[i][j]; } } }4.3 解密主函数InvCipher解密是加密的逆过程步骤顺序相反且每一步都是其逆操作。注意由于列混合的逆操作InvMixColumns更复杂有一种优化策略叫“等效解密”但为了清晰理解我们先实现标准逆运算。void AES_DecryptBlock(AES_ctx *ctx, const uint8_t *input, uint8_t *output) { state_t state; uint8_t round AES_NR; // 从最后一轮开始 // 1. 将输入密文拷贝到状态矩阵 for (int i 0; i 4; i) { for (int j 0; j 4; j) { state[i][j] input[i 4 * j]; } } // 2. 初始轮密钥加使用最后一轮密钥 AddRoundKey(state, (ctx-round_keys[round * AES_BLOCKLEN])); round--; // 3. 前9轮完整逆运算 for (; round 0; --round) { InvShiftRows(state); InvSubBytes(state); AddRoundKey(state, (ctx-round_keys[round * AES_BLOCKLEN])); InvMixColumns(state); } // 4. 第1轮最后一轮逆运算无InvMixColumns InvShiftRows(state); InvSubBytes(state); AddRoundKey(state, (ctx-round_keys[0])); // 使用第0轮密钥即原始扩展后的第一个轮密钥 // 5. 将状态矩阵拷贝到输出 for (int i 0; i 4; i) { for (int j 0; j 4; j) { output[i 4 * j] state[i][j]; } } }注意解密时轮密钥的使用顺序是反的。初始轮密钥加用的是第10轮密钥然后依次递减。4.4 初始化函数我们需要一个函数来初始化上下文主要是执行密钥扩展。void AES_Init(AES_ctx *ctx, const uint8_t *key) { KeyExpansion(key, ctx-round_keys); }5. 工作模式与填充方案初步探讨我们上面实现的是最基础的电子密码本ECB模式下的单块加解密。ECB模式简单但有一个致命缺点相同的明文块会被加密成相同的密文块。对于有重复模式的数据如图像这会泄露信息。因此在实际应用中我们不会直接使用ECB。5.1 常用工作模式简介为了加密超过一个块的数据我们需要选择一种工作模式。常见的模式有CBC密码块链接最常用的模式之一。每个明文块在加密前先与前一个密文块进行异或。需要一个初始化向量IV来启动这个过程。IV不需要保密但必须不可预测通常随机生成。CBC模式能提供更好的安全性。CTR计数器将计数器加密后与明文异或得到密文。它可以将分组密码转换为流密码支持并行计算和随机访问非常高效。GCM伽罗瓦/计数器模式在CTR基础上增加了消息认证功能GMAC能同时提供加密和完整性校验是现代TLS协议中的宠儿。实操心得在资源允许的情况下优先考虑使用CBC或CTR模式替代ECB。如果自己实现CBC需要处理好IV的生成和传递并且注意解密时错误的IV会导致第一个明文块解密错误但后续块正确错误传播特性。5.2 填充PaddingAES块大小是16字节。如果要加密的数据长度不是16的整数倍就需要填充。最简单的填充方式是PKCS#7缺n个字节就填充n个值为n的字节。例如如果最后一块缺3字节就填充0x03 0x03 0x03。解密后读取最后一个字节的值n然后移除最后n个字节即可得到原始数据。实现一个简单的PKCS#7填充和去填充函数是集成到完整加密流程中的必要步骤。6. 测试、验证与性能考量代码写完了怎么知道它对不对我们需要一个可靠的测试向量进行验证。6.1 使用标准测试向量验证NIST美国国家标准与技术研究院提供了官方的AES测试向量。我们可以找一个AES-128 ECB模式的测试用例来验证我们的实现。例如一个经典的测试向量密钥2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c明文32 43 f6 a8 88 5a 30 8d 31 31 98 a2 e0 37 07 34密文39 25 84 1d 02 dc 09 fb dc 11 85 97 19 6a 0b 32我们可以编写一个简单的测试函数将密钥和明文输入我们的AES_EncryptBlock然后逐字节比较输出是否与标准密文一致。解密测试同理。6.2 常见问题与调试技巧在实现过程中我踩过不少坑这里分享几个最常见的状态矩阵索引错误这是最易出错的地方。务必牢记列优先的填充和读取顺序。一个快速检查方法是用全零的明文和密钥加密如果第一步AddRoundKey后状态矩阵全零说明映射可能错了因为密钥扩展后第一轮密钥就是原密钥全零异或全零还是全零。密钥扩展错误如果加解密结果不对但单步跟踪加密过程发现第一轮之后状态值就与预期不符很可能是密钥扩展错了。重点检查RotWord、SubWord和Rcon异或的顺序和实现特别是Rcon的索引i / AES_KEYLEN。逆算法不匹配确保InvSubBytes、InvShiftRows、InvMixColumns的实现完全正确。一个有效的验证方法是随机生成一个状态矩阵先进行SubBytes再进行InvSubBytes看是否能恢复原值。对ShiftRows和MixColumns也做类似测试。字节序问题我们的实现假设数据在内存中就是普通的字节数组。如果你的测试数据是从文件或网络中以十六进制字符串形式读入需要正确转换。性能优化我们目前的实现是教育性质的注重清晰。在实际追求性能的场景可以考虑使用查表法将SubBytes、ShiftRows、MixColumns合并成基于T表的查找操作如使用4个1KB的T表这能极大提升速度但会增加代码大小。使用处理器指令集现代x86/x64 CPU支持AES-NI指令集ARMv8也有加密扩展指令。这些硬件加速比任何软件实现都快几个数量级。在支持的环境下应优先使用这些指令。6.3 集成到实际项目中的建议当你确认自己的AES实现正确后如果想集成到项目中建议封装良好的API提供类似aes128_cbc_encrypt/decrypt这样的函数内部处理好IV和填充。注意内存安全确保密钥等敏感数据在使用后及时从内存中清除例如使用memset_s或类似函数。考虑使用经过审计的库对于生产环境除非有极特殊需求如无libc的裸机环境否则强烈建议使用久经考验的库如mbed TLS、libsodium或OpenSSL。自己实现的密码学代码很难保证没有侧信道攻击等漏洞。7. 从ECB扩展到CBC模式的实现示例为了让你更好地理解如何将基础块加密应用于实际模式我们简要勾勒一下CBC模式的加密和解密流程。假设我们已经有了一个可靠的、经过测试的AES_EncryptBlock和AES_DecryptBlock函数。CBC加密流程将明文数据按16字节分块最后一块可能需要PKCS#7填充。准备一个16字节的随机初始化向量IV。对于第一块明文先与IV进行异或然后进行AES加密得到第一块密文。对于后续的每一块明文先与前一块得到的密文进行异或然后再进行AES加密。最终输出的密文由IV需要与密文一起传输或存储和所有加密后的块组成。CBC解密流程读取IV和密文数据。对于第一块密文先进行AES解密然后再与IV进行异或得到第一块明文。对于后续的每一块密文先进行AES解密然后再与前一块密文进行异或得到明文。移除最后一块的填充得到原始数据。这里有一个关键点CBC解密的异或操作对象是密文而不是解密后的中间结果。这是因为加密时的异或是在加密函数之前进行的。这种结构使得解密过程可以并行化虽然我们这里的示例是串行的。实现CBC模式会让你对分组密码的工作模式有更深刻的认识。它有效地消除了ECB模式中相同明文产生相同密文的问题因为每一块明文的加密都依赖于前一块的密文或IV。8. 总结与资源推荐从头实现AES是一次收获巨大的旅程。它强迫你关注每一个字节的流动理解每一行代码背后的数学原理。虽然最终代码可能只有几百行但其中蕴含的关于对称密码设计、有限域运算和代码优化的知识却非常深厚。在调试通过自己的AES实现后我强烈建议你做以下几件事来巩固和扩展尝试实现AES-192和AES-256主要改动在于密钥扩展算法和轮数分别为12轮和14轮。这能帮你理解算法如何适应不同密钥长度。实现CTR模式相比CBCCTR模式不需要填充且可以并行加密/解密实现起来是另一种有趣的体验。阅读标准文档FIPS PUB 197是AES的官方标准虽然数学性较强但对于理解算法设计初衷极有帮助。分析优秀开源实现去读一读Tiny-AES-c一个极简的C语言AES实现或者mbed TLS中AES模块的源代码看看别人是如何组织代码、进行优化和保证安全的。最后记住密码学是一个“魔鬼在细节中”的领域。自己实现的代码用于学习和理解无妨但在关乎真实数据安全的生产环境中请务必依赖那些经过广泛审查和测试的权威密码学库。把轮子造一遍是为了知道车为什么能跑而不是为了在所有路上都用自己的轮子。