C++手撕AES-128:从原理到实现,掌握对称加密核心
1. 项目概述与核心价值最近在整理一些涉及网络通信和数据存储的老项目发现很多地方还在用一些过时或者自己写的简单加密方法安全性堪忧。正好有朋友问起如何在C里实现一个靠谱的加密功能特别是AES这让我觉得有必要把这块内容系统地梳理一遍。AESAdvanced Encryption Standard高级加密标准如今已经是全球范围内事实上的对称加密算法标准从你的操作系统内核到浏览器的HTTPS连接再到你手机里的App数据保护背后几乎都有它的身影。对于C开发者来说无论是做客户端软件的数据本地加密还是服务器端网络传输的报文加密掌握AES的实现原理和实操方法都是一项非常硬核且实用的技能。很多人可能觉得加密算法高深莫测或者觉得直接调用OpenSSL、Crypto这些库就完事了。但我的经验是如果你不清楚AES到底是怎么一步步把明文变成密文又在另一端还原回来的你在实际使用中很容易踩坑。比如为什么同样的密钥和明文每次加密结果可能不一样为什么解密时总提示“填充错误”不同模式如CBC、ECB到底该怎么选这些问题仅仅会调用API是远远不够的。所以这篇内容我会带你从零开始用C手撕一遍AES-128的核心流程。目的不是让你去造一个比现有库更优秀的轮子而是通过亲手实现彻底吃透其内部的字节代换、行移位、列混合和轮密钥加这四大核心操作理解其设计精妙之处。当你清楚了这些再回头去用那些成熟的加密库你会更加得心应手也能更从容地应对各种加密相关的调试和问题排查。2. AES加密算法核心原理拆解在动手写代码之前我们必须先搞清楚AES到底在干什么。它是一种对称分组密码算法意思是加密和解密用同一把钥匙并且数据被分成固定大小的块来处理。我们常说的AES-128、AES-192、AES-256指的就是密钥的长度分别是128位、192位和256位。对应的加密轮数也不同分别是10轮、12轮和14轮。为了便于理解和上手实操我们这里以最常用的AES-128为例它处理的数据块大小是128位也就是16个字节。AES的加密过程可以看作是对一个4x4的字节矩阵称为状态State进行多轮迭代变换。初始状态就是你的16字节明文数据按列填充进去的。每一轮变换除了最后一轮稍有不同都包含四个步骤字节代换SubBytes、行移位ShiftRows、列混合MixColumns和轮密钥加AddRoundKey。而解密过程就是这些步骤的逆操作并以相反的顺序执行。2.1 字节代换SubBytes查表实现的非线性变换这是AES中提供非线性特性的核心步骤目的是让输出和输入之间没有明显的线性关系增强算法对抗密码分析的能力。它通过一个预先计算好的S盒Substitution-box来完成。对于加密每个输入字节的高4位作为行索引低4位作为列索引去查找S盒中对应的值进行替换。例如输入字节0x53高4位是5低4位是3那么就在S盒的第5行第3列找到替换值。这个S盒的设计非常精妙它是基于有限域GF(2^8)上的乘法逆元运算再结合一个仿射变换得到的。对于初学者我们完全不需要每次都去现场计算这个逆元计算量很大且容易出错直接用一个256字节的常量数组把整个S盒存起来就行这就是典型的“空间换时间”优化。解密时用的逆S盒InvSubBytes也是同理是一个预先计算好的逆变换查找表。实操心得在代码里我们直接定义两个unsigned char数组S_BOX[256]和INV_S_BOX[256]把标准值填进去。这是最高效、最不容易出错的方式。千万不要在运行时去计算GF(2^8)的逆元除非你在做非常特殊的研究。2.2 行移位ShiftRows矩阵内部的字节搅拌这一步很简单但很重要。它对状态矩阵的每一行进行循环左移。第0行不移位第1行左移1个字节第2行左移2个字节第3行左移3个字节。这样做的目的是让同一列中的字节在后续的列混合步骤中能扩散到不同的列去增强了算法的扩散性。解密时的逆操作InvShiftRows就是循环右移相应的位数。这个操作实现起来就是几行内存拷贝或索引计算的事情非常轻量。2.3 列混合MixColumns列向量的矩阵乘法这是AES中最具数学美感也是最复杂的一步。它把状态矩阵的每一列看作GF(2^8)上的一个4项多项式与一个固定的多项式c(x)进行模x^41乘法。在实际计算中这等价于对每一列进行一个矩阵乘法运算。具体来说对于状态矩阵的某一列[a0, a1, a2, a3]^T经过列混合后得到的新列[b0, b1, b2, b3]^T满足如下关系运算都在GF(2^8)上b0 (2 * a0) ^ (3 * a1) ^ (1 * a2) ^ (1 * a3) b1 (1 * a0) ^ (2 * a1) ^ (3 * a2) ^ (1 * a3) b2 (1 * a0) ^ (1 * a1) ^ (2 * a2) ^ (3 * a3) b3 (3 * a0) ^ (1 * a1) ^ (1 * a2) ^ (2 * a3)这里的乘法和加法都是有限域上的运算。乘法“2 * a”在代码中体现为如果a的最高位是0则结果为a1如果最高位是1则结果为(a1) ^ 0x1B因为AES使用的不可约多项式是x^8 x^4 x^3 x 1即0x11B模运算后产生这个异或项。乘法“3 * a”可以拆解为(2 * a) ^ a。注意事项列混合的逆运算InvMixColumns使用的矩阵不同计算也更复杂一些。同样在实际实现中为了追求极致的性能很多库会使用查表法如使用T-table来加速列混合及其逆运算。我们在教学实现中可以先完成基础版本理解原理是关键。2.4 轮密钥加AddRoundKey最简单的异或操作这一步最简单就是将当前的状态矩阵与当前轮的轮密钥矩阵进行逐字节的异或XOR操作。轮密钥是从初始的主密钥通过密钥扩展算法派生出来的一系列子密钥。正是这一步将密钥与数据关联起来。它的逆操作就是它本身因为(A ^ B) ^ B A。2.5 密钥扩展Key Expansion一把钥匙变出多把AES-128的初始密钥是16字节。但我们需要11个轮密钥第1轮到第10轮再加上一个初始轮密钥加用的每个轮密钥也是16字节。密钥扩展算法就是用来从初始密钥生成这11个轮密钥的过程。它涉及字节的循环左移RotWord、S盒代换SubWord、与轮常数Rcon异或等操作。理解这个过程对于后续自己实现加密解密循环至关重要。3. C实现AES-128核心流程理论铺垫足够多了现在我们来动手实现。我会先给出核心组件的代码然后串联起完整的加密流程。我们采用面向过程的方式以便清晰地展示每一步。3.1 基础定义与常量表首先我们定义一些类型和最重要的S盒、逆S盒以及轮常数数组。#include cstdint #include cstring // for memcpy typedef uint8_t byte; // AES-128 常量定义 const int AES_KEY_SIZE 16; // 字节数 const int AES_BLOCK_SIZE 16; // 字节数 const int NR 10; // AES-128 轮数 // S盒和逆S盒 (此处只展示部分值完整256字节数组需自行补充) const byte S_BOX[256] { 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, // ... 省略中间值需补全完整256个字节 0x16, 0x5e, 0xba, 0x5b, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00 }; const byte INV_S_BOX[256] { 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, // ... 省略中间值需补全完整256个字节 0xb1, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 }; // 轮常数 Rcon[i] (RC[i], 0x00, 0x00, 0x00) RC[1]1, RC[i]2 * RC[i-1] in GF(2^8) const byte Rcon[11] { 0x00, // 占位不使用 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 };3.2 密钥扩展实现密钥扩展是AES正确运行的基础。它将16字节的初始密钥扩展成11个16字节的轮密钥存储在一个线性数组w中。每个轮密钥由4个32位的字word组成。// 密钥扩展函数 void KeyExpansion(const byte key[AES_KEY_SIZE], byte w[4 * (NR 1) * 4]) { byte temp[4]; // 初始密钥拷贝到w的前4个字 for (int i 0; i 4; i) { w[4*i] key[4*i]; w[4*i1] key[4*i1]; w[4*i2] key[4*i2]; w[4*i3] key[4*i3]; } for (int i 4; i 4 * (NR 1); i) { // 将前一个字存入temp temp[0] w[4*(i-1)]; temp[1] w[4*(i-1)1]; temp[2] w[4*(i-1)2]; temp[3] w[4*(i-1)3]; if (i % 4 0) { // 对temp进行 RotWord 和 SubWord并与Rcon异或 // RotWord: 循环左移一个字节 [a0,a1,a2,a3] - [a1,a2,a3,a0] byte tmp temp[0]; temp[0] temp[1]; temp[1] temp[2]; temp[2] temp[3]; temp[3] tmp; // SubWord: 每个字节通过S盒替换 temp[0] S_BOX[temp[0]]; temp[1] S_BOX[temp[1]]; temp[2] S_BOX[temp[2]]; temp[3] S_BOX[temp[3]]; // 与轮常数异或 (Rcon[i/4] 只与第一个字节异或) temp[0] ^ Rcon[i/4]; } // w[i] w[i-4] ^ temp w[4*i] w[4*(i-4)] ^ temp[0]; w[4*i1] w[4*(i-4)1] ^ temp[1]; w[4*i2] w[4*(i-4)2] ^ temp[2]; w[4*i3] w[4*(i-4)3] ^ temp[3]; } }3.3 核心变换函数实现接下来我们实现四个核心的轮函数。为了方便我们用一个4x4的字节数组state[4][4]来表示状态矩阵其中state[r][c]表示第r行第c列r和c从0开始。// 字节代换 void SubBytes(byte state[4][4]) { for (int r 0; r 4; r) { for (int c 0; c 4; c) { state[r][c] S_BOX[state[r][c]]; } } } // 行移位 void ShiftRows(byte state[4][4]) { byte 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; } // 有限域GF(2^8)上的乘法只用于列混合 byte gf_mul(byte a, byte b) { byte p 0; for (int i 0; i 8; i) { if (b 1) { p ^ a; } bool hi_bit (a 0x80); a 1; if (hi_bit) { a ^ 0x1b; // 对应不可约多项式 x^8 x^4 x^3 x 1 } b 1; } return p; } // 列混合 void MixColumns(byte state[4][4]) { byte t[4]; for (int c 0; c 4; c) { // 复制当前列 t[0] state[0][c]; t[1] state[1][c]; t[2] state[2][c]; t[3] state[3][c]; // 矩阵乘法计算新列 state[0][c] gf_mul(0x02, t[0]) ^ gf_mul(0x03, t[1]) ^ t[2] ^ t[3]; state[1][c] t[0] ^ gf_mul(0x02, t[1]) ^ gf_mul(0x03, t[2]) ^ t[3]; state[2][c] t[0] ^ t[1] ^ gf_mul(0x02, t[2]) ^ gf_mul(0x03, t[3]); state[3][c] gf_mul(0x03, t[0]) ^ t[1] ^ t[2] ^ gf_mul(0x02, t[3]); } } // 轮密钥加 void AddRoundKey(byte state[4][4], const byte* roundKey) { for (int c 0; c 4; c) { for (int r 0; r 4; r) { state[r][c] ^ roundKey[c * 4 r]; // 注意轮密钥的存储顺序 } } }实操心得注意AddRoundKey函数中轮密钥的索引方式。我们的轮密钥数组w是按列优先顺序存储的而state矩阵我们约定为state[row][col]。所以roundKey[c*4 r]对应的是轮密钥矩阵第c列第r行的字节。这个顺序一定要统一否则加解密结果肯定对不上。3.4 加密主函数串联现在我们可以把上述所有函数组合起来形成完整的AES-128加密流程。// AES-128 加密一个数据块 (16字节) void AES_EncryptBlock(const byte in[AES_BLOCK_SIZE], byte out[AES_BLOCK_SIZE], const byte key[AES_KEY_SIZE]) { byte state[4][4]; byte w[4 * (NR 1) * 4]; // 扩展密钥 // 1. 密钥扩展 KeyExpansion(key, w); // 2. 初始化状态矩阵 (按列填充) for (int r 0; r 4; r) { for (int c 0; c 4; c) { state[r][c] in[c * 4 r]; } } // 3. 初始轮密钥加 AddRoundKey(state, w[0]); // 4. 前9轮标准轮函数 for (int round 1; round NR; round) { SubBytes(state); ShiftRows(state); MixColumns(state); AddRoundKey(state, w[round * 16]); // 每轮使用对应的16字节轮密钥 } // 5. 最后一轮 (无列混合) SubBytes(state); ShiftRows(state); AddRoundKey(state, w[NR * 16]); // 6. 将状态矩阵写回输出 (按列取出) for (int r 0; r 4; r) { for (int c 0; c 4; c) { out[c * 4 r] state[r][c]; } } }4. 解密流程与工作模式初探有了加密的基础解密就是逆过程。我们需要实现逆字节代换InvSubBytes、逆行移位InvShiftRows、逆列混合InvMixColumns。轮密钥加本身就是逆运算。解密函数的轮密钥使用顺序与加密相反。4.1 解密核心变换实现// 逆字节代换 void InvSubBytes(byte state[4][4]) { for (int r 0; r 4; r) { for (int c 0; c 4; c) { state[r][c] INV_S_BOX[state[r][c]]; } } } // 逆行移位 void InvShiftRows(byte state[4][4]) { byte 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字节 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; } // 逆列混合 void InvMixColumns(byte state[4][4]) { byte t[4]; for (int c 0; c 4; c) { t[0] state[0][c]; t[1] state[1][c]; t[2] state[2][c]; t[3] state[3][c]; // 使用逆混合矩阵系数 {0x0e, 0x0b, 0x0d, 0x09} state[0][c] gf_mul(0x0e, t[0]) ^ gf_mul(0x0b, t[1]) ^ gf_mul(0x0d, t[2]) ^ gf_mul(0x09, t[3]); state[1][c] gf_mul(0x09, t[0]) ^ gf_mul(0x0e, t[1]) ^ gf_mul(0x0b, t[2]) ^ gf_mul(0x0d, t[3]); state[2][c] gf_mul(0x0d, t[0]) ^ gf_mul(0x09, t[1]) ^ gf_mul(0x0e, t[2]) ^ gf_mul(0x0b, t[3]); state[3][c] gf_mul(0x0b, t[0]) ^ gf_mul(0x0d, t[1]) ^ gf_mul(0x09, t[2]) ^ gf_mul(0x0e, t[3]); } }4.2 解密主函数// AES-128 解密一个数据块 (16字节) void AES_DecryptBlock(const byte in[AES_BLOCK_SIZE], byte out[AES_BLOCK_SIZE], const byte key[AES_KEY_SIZE]) { byte state[4][4]; byte w[4 * (NR 1) * 4]; KeyExpansion(key, w); // 扩展密钥与加密共用 // 初始化状态矩阵 for (int r 0; r 4; r) { for (int c 0; c 4; c) { state[r][c] in[c * 4 r]; } } // 解密过程步骤顺序与加密相反且使用逆变换 // 初始轮密钥加 (使用最后一轮密钥) AddRoundKey(state, w[NR * 16]); // 前9轮 for (int round NR-1; round 1; round--) { InvShiftRows(state); InvSubBytes(state); AddRoundKey(state, w[round * 16]); // 注意轮密钥顺序 InvMixColumns(state); } // 最后一轮 InvShiftRows(state); InvSubBytes(state); AddRoundKey(state, w[0]); // 使用最初的轮密钥 // 输出 for (int r 0; r 4; r) { for (int c 0; c 4; c) { out[c * 4 r] state[r][c]; } } }4.3 工作模式与填充上面我们实现的是对单个16字节128位数据块的加密和解密这被称为ECBElectronic Codebook电子密码本模式。但ECB模式有一个严重的安全缺陷相同的明文块会被加密成相同的密文块。这会导致模式泄露对于图像等数据甚至能在密文中看出轮廓。因此在实际应用中我们绝不会使用ECB模式。常用的安全模式有CBCCipher Block Chaining密码分组链接、CTRCounter计数器等。以CBC模式为例它在加密第一个块时会先与一个初始化向量IV进行异或然后再用密钥加密。加密后的密文块又会作为下一个明文块的IV如此链接下去。这样即使明文相同只要IV不同加密结果就完全不同。此外数据长度通常不是16字节的整数倍这就需要填充Padding。常用的有PKCS#7填充规则是如果需要填充N个字节则每个填充字节的值都是N。例如一个15字节的数据需要填充1个字节的0x01一个16字节的数据则需要额外填充一个完整的16字节块每个字节都是0x10。重要提示永远不要在生产环境中使用ECB模式。务必选择CBC、CTR等更安全的模式并确保每次加密使用随机且不可预测的IV。IV不需要保密但必须唯一通常随机生成并随密文一起传输给解密方。5. 常见问题、调试技巧与性能优化自己实现AES的过程是学习密码学原理的绝佳途径但也会遇到各种坑。下面分享一些我踩过的坑和调试经验。5.1 典型问题与排查清单问题现象可能原因排查步骤加密结果与标准库如OpenSSL不一致1. S盒/逆S盒数据错误。2. 行移位方向搞反加密左移解密右移。3. 列混合系数或有限域乘法实现错误。4. 轮密钥加时状态矩阵与轮密钥矩阵的索引对应关系错误。5. 密钥扩展算法实现有误。1. 使用NIST官方测试向量如AES-128的“ECB 128-bit key”测试进行逐轮比对。2. 打印出第一轮加密前后的状态矩阵、轮密钥与标准中间值对比。3. 单独测试gf_mul函数验证gf_mul(0x57, 0x83) 0xc1等已知结果。4. 检查状态矩阵在内存中的布局行优先/列优先是否与算法描述一致。解密后数据与原始明文不符1. 解密流程中逆变换的顺序或实现错误。2. 解密时使用的轮密钥顺序错误。3. 加密和解密使用的S盒/逆S盒不匹配。1. 确保解密流程严格为AddRoundKey-InvShiftRows-InvSubBytes-InvMixColumns最后一轮除外。2. 确认解密轮密钥是从后往前使用的。3. 对同一组密钥和明文加密后立即解密看是否能还原。处理多块数据或非16字节倍数数据时出错1. 工作模式如CBC实现错误IV处理不当。2. 填充算法实现错误或解密后去除填充逻辑有误。3. 内存越界访问。1. 先用ECB模式测试单块加解密是否正确。2. 实现并测试填充函数如PKCS#7单独验证填充和去填充逻辑。3. 在CBC模式下逐步调试确认每一块加密前的异或操作是否正确。程序运行时崩溃或结果随机1. 数组越界特别是S盒访问索引超过255。2. 未初始化的内存被使用。3. 指针操作错误。1. 使用内存检查工具如Valgrind、AddressSanitizer。2. 确保所有数组如状态矩阵、轮密钥数组在使用前已被正确初始化。3. 检查所有数组索引计算特别是w[round * 16]中的round值是否在有效范围内。5.2 调试技巧使用标准测试向量这是最有效的验证方法。你可以从NIST的官方网站或密码学标准文档中找到AES的已知答案测试KAT向量。找一个简单的ECB模式测试向量包含密钥、明文和密文。用你的程序加密明文看结果是否与标准密文完全一致每个字节都一样。如果不对就逐步打印中间状态与标准中间值对比能快速定位到出错的步骤。5.3 性能优化思路我们上面的实现是教学性质的追求清晰易懂。在实际需要高性能的场景有成熟的优化方案查表法T-table这是最常见的优化。将列混合和字节代换合并预先计算好4个256字的查找表每个字32位。这样每一列4字节的变换可以通过4次查表和4次异或完成避免了耗时的有限域乘法运算。AES-NI指令集出现前这是主流优化方式。AES-NI指令集现代x86/x64 CPUIntel Westmere架构及以后AMD Bulldozer及以后都提供了AES-NI指令集如AESENC,AESDEC,AESKEYGENASSIST。使用这些指令加解密速度会有数量级的提升。在C中可以通过编译器内置函数如GCC/Clang的__builtin_ia32_aesenc128或汇编直接调用。并行化在CTR、GCM等模式下由于各数据块的加密不依赖前一块的结果可以很容易地进行并行计算充分利用多核CPU。实操心得对于绝大多数应用直接使用经过高度优化的库如OpenSSL的EVP接口、Crypto、libsodium是最佳选择。它们不仅经过了严格的安全审计和性能优化还正确实现了各种工作模式和填充方案。自己实现的AES其价值在于学习和理解除非有极其特殊的定制化需求且经过严格的安全评审否则不要用于生产环境。6. 从原理到应用在项目中安全使用AES理解了原理也实现了核心算法最后我们聊聊如何在真实的C项目中安全、正确地使用AES加密。这里的关键不再是算法本身而是如何正确地使用它。6.1 密钥管理安全的重中之重算法是公开的安全完全依赖于密钥。密钥管理是加密系统中最脆弱的一环。生成必须使用密码学安全的随机数生成器CSPRNG来生成密钥。在C中可以使用操作系统提供的接口如Windows的BCryptGenRandom或CryptGenRandomLinux/macOS的/dev/urandom或者C11以上的std::random_device但要注意其实现质量。存储绝对不要将密钥硬编码在代码中。对于客户端软件可以考虑使用操作系统提供的密钥链如Windows Credential Manager, macOS Keychain。对于服务器可以使用硬件安全模块HSM或专门的密钥管理服务KMS。传输如果密钥需要在两端共享应使用非对称加密如RSA或密钥协商协议如Diffie-Hellman来安全地交换对称密钥而不是直接发送。6.2 选择正确的工作模式和配置模式选择CBC密码分组链接需要随机且不可预测的IV。解密可以并行但加密无法并行。对填充错误敏感可能引发Padding Oracle攻击需配合MAC使用。CTR计数器将分组密码转换为流密码。不需要填充可以并行加解密。同样需要唯一的Nonce相当于IV。GCMGalois/Counter Mode目前最推荐的模式之一。它同时提供了加密和认证Authenticated Encryption。性能好且内置了完整性校验。初始化向量IV/Nonce必须每次加密都使用一个新的、随机的IV对于CBC或唯一的Nonce对于CTR/GCM。IV不需要保密但必须唯一通常随密文一起传输。认证加密AEAD现代应用应优先选择像GCM、CCM、ChaCha20-Poly1305这样的认证加密模式。它们能同时保证数据的机密性和完整性防止密文被篡改。6.3 使用成熟的密码学库这是最最重要的一条建议。在真实项目中请务必使用成熟的、经过广泛审计的密码学库。自己实现的AES代码很难保证没有侧信道攻击漏洞如通过执行时间泄露密钥信息也很难保证在所有边界条件下都行为正确。OpenSSL功能最全应用最广。使用其EVP高级接口可以轻松选择算法、模式和填充。// 伪代码示例使用OpenSSL EVP进行AES-256-GCM加密 EVP_CIPHER_CTX *ctx EVP_CIPHER_CTX_new(); EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, key, iv); EVP_EncryptUpdate(ctx, ciphertext, len, plaintext, plaintext_len); EVP_EncryptFinal_ex(ctx, ciphertext len, len); EVP_CIPHER_CTX_free(ctx);libsodiumAPI极其简单默认选择安全的算法和参数对开发者更友好不易误用。Crypto功能强大的C库提供了丰富的算法和灵活的接口。使用这些库你只需要关心如何生成和管理密钥、如何设置IV而复杂的算法实现、性能优化和安全防护都交给库来处理。6.4 一个简单的CBC模式封装示例为了将我们的理解串联起来这里给出一个基于我们自实现AES核心函数假设已优化稳定的、简单的CBC模式PKCS#7填充的封装示例。请注意这仍然是教学目的生产环境请用成熟库。#include vector #include algorithm #include cstdlib // for rand, 实际应用应用更安全的随机源 // PKCS#7 填充 void PKCS7_Padding(std::vectorbyte data) { int pad_len AES_BLOCK_SIZE - (data.size() % AES_BLOCK_SIZE); // 如果数据长度正好是块大小的倍数则填充一个完整的块 if (pad_len 0) pad_len AES_BLOCK_SIZE; data.insert(data.end(), pad_len, static_castbyte(pad_len)); } // PKCS#7 去填充 bool PKCS7_UnPadding(std::vectorbyte data) { if (data.empty()) return false; byte pad_len data.back(); if (pad_len 0 || pad_len AES_BLOCK_SIZE || pad_len data.size()) { return false; // 无效的填充 } // 检查填充字节是否都正确 for (size_t i data.size() - pad_len; i data.size(); i) { if (data[i] ! pad_len) return false; } data.resize(data.size() - pad_len); return true; } // AES-128 CBC 加密 bool AES_CBC_Encrypt(const std::vectorbyte plaintext, const byte key[AES_KEY_SIZE], std::vectorbyte ciphertext, std::vectorbyte iv) { if (plaintext.empty()) return false; std::vectorbyte padded_data plaintext; PKCS7_Padding(padded_data); // 填充 // 生成随机IV (16字节) iv.resize(AES_BLOCK_SIZE); // !!!警告此处使用rand()仅为示例生产环境必须使用密码学安全随机数!!! for (int i 0; i AES_BLOCK_SIZE; i) { iv[i] static_castbyte(rand() 0xFF); } ciphertext.clear(); ciphertext.reserve(padded_data.size() AES_BLOCK_SIZE); // 预留空间IV会放在密文前 byte prev_block[AES_BLOCK_SIZE]; memcpy(prev_block, iv.data(), AES_BLOCK_SIZE); // 第一个块的“前一个密文块”是IV size_t blocks padded_data.size() / AES_BLOCK_SIZE; for (size_t i 0; i blocks; i) { byte in_block[AES_BLOCK_SIZE], out_block[AES_BLOCK_SIZE]; // CBC: 明文块先与前一个密文块或IV异或 for (int j 0; j AES_BLOCK_SIZE; j) { in_block[j] padded_data[i * AES_BLOCK_SIZE j] ^ prev_block[j]; } // 然后用AES加密 AES_EncryptBlock(in_block, out_block, key); // 输出密文块并作为下一个块的“前一个密文块” memcpy(prev_block, out_block, AES_BLOCK_SIZE); ciphertext.insert(ciphertext.end(), out_block, out_block AES_BLOCK_SIZE); } // 通常将IV附加在密文前面一起传输 ciphertext.insert(ciphertext.begin(), iv.begin(), iv.end()); return true; } // AES-128 CBC 解密 bool AES_CBC_Decrypt(const std::vectorbyte ciphertext_with_iv, const byte key[AES_KEY_SIZE], std::vectorbyte plaintext) { if (ciphertext_with_iv.size() AES_BLOCK_SIZE * 2 || (ciphertext_with_iv.size() % AES_BLOCK_SIZE) ! 0) { return false; // 密文长度至少为IV一个块且必须是块大小的倍数 } plaintext.clear(); // 提取IV密文前16字节 byte iv[AES_BLOCK_SIZE]; memcpy(iv, ciphertext_with_iv.data(), AES_BLOCK_SIZE); size_t total_blocks ciphertext_with_iv.size() / AES_BLOCK_SIZE; size_t data_blocks total_blocks - 1; // 减去IV块 byte prev_block[AES_BLOCK_SIZE]; memcpy(prev_block, iv, AES_BLOCK_SIZE); for (size_t i 1; i total_blocks; i) { // 从第1块开始第0块是IV const byte* cipher_block ciphertext_with_iv.data() i * AES_BLOCK_SIZE; byte out_block[AES_BLOCK_SIZE], plain_block[AES_BLOCK_SIZE]; // 先用AES解密当前密文块 AES_DecryptBlock(cipher_block, out_block, key); // CBC: 解密后的中间结果与前一个密文块异或得到明文 for (int j 0; j AES_BLOCK_SIZE; j) { plain_block[j] out_block[j] ^ prev_block[j]; } // 更新“前一个密文块”为当前密文块 memcpy(prev_block, cipher_block, AES_BLOCK_SIZE); plaintext.insert(plaintext.end(), plain_block, plain_block AES_BLOCK_SIZE); } // 去除填充 return PKCS7_UnPadding(plaintext); }这个示例展示了如何将我们实现的基础块加密函数组合成一个更实用的CBC模式加密解密流程。它包含了填充、IV处理和CBC的链式操作。请再次注意其中的随机数生成是极不安全的仅用于演示流程。通过这样从原理到实现再到应用和注意事项的完整梳理相信你对AES在C中的实现和应用有了更深入的理解。记住密码学是一个“魔鬼在细节中”的领域自己实现用于学习是极好的但在关乎真实数据安全的生产环境中信任那些久经沙场的成熟库才是更负责任的选择。