从零实现AES:深入理解对称加密核心原理与C++工程实践
1. 项目缘起为什么需要自己动手实现AES在C开发中尤其是涉及数据安全、网络通信、本地配置存储的场景加密是一个绕不开的话题。AESAdvanced Encryption Standard作为目前全球最主流的对称加密算法其重要性不言而喻。你可能在项目中直接调用过OpenSSL、Crypto等库的AES接口一行代码就完成了加密解密感觉非常方便。但不知道你有没有遇到过这样的困惑当加密结果和另一个系统比如Java后端、Python脚本对不上时排查起来异常痛苦或者当需要一些非标准用法比如混合编码输入输出时发现库的接口不够灵活。这就是我决定动手从头实现一个AES加密解密工具的原因。知其然更要知其所以然。通过亲手实现一遍AES的ECB、CBC两种模式支持128/192/256三种密钥长度并处理字符串、十六进制、二进制文件等多种输入输出格式你不仅能彻底掌握AES的工作机制更能获得一种“底层掌控感”。以后遇到任何加密相关的疑难杂症你都能从原理层面快速定位而不是在黑盒API面前束手无策。这个项目不是要替代成熟的加密库而是为你打造一把理解对称加密的“万能钥匙”。2. AES核心原理快速透视从状态矩阵到轮密钥加在开始写代码之前我们必须先打破对AES的“黑盒”印象。AES加密的本质是对一个16字节128位的“状态State”矩阵进行多轮的可逆变换。理解这个“状态”的流转过程是看懂一切代码的基础。2.1 状态矩阵与初始回合AES将明文数据块视为一个4x4的字节矩阵称为状态State。例如明文字节序列 P0, P1, ..., P15 按列优先顺序填充到矩阵中| P0 P4 P8 P12 | | P1 P5 P9 P13 | | P2 P6 P10 P14 | | P3 P7 P11 P15 |加密过程就是对这个State矩阵进行Nr轮Nr取决于密钥长度AES-128为10轮192为12轮256为14轮的迭代运算。每一轮包含四个基本步骤字节替换SubBytes、行移位ShiftRows、列混合MixColumns、轮密钥加AddRoundKey。第一轮开始前会先进行一次AddRoundKey使用第0个轮密钥最后一轮则省略MixColumns步骤。2.2 四大核心变换详解字节替换SubBytes这是一个非线性变换通过一个预定义的S盒S-Box完成。State中的每一个字节都被替换为S盒中对应位置的新字节。例如State[i][j] S_box[State[i][j]]。S盒的设计基于有限域GF(2^8)上的乘法逆元运算和仿射变换是AES提供混淆Confusion特性的关键让输出和输入之间不存在简单的线性关系。在代码实现上我们直接用一个256字节的查找表来实现效率最高。行移位ShiftRows这是一个线性变换对State的每一行进行循环左移。第0行不移位第1行左移1个字节第2行左移2个字节第3行左移3个字节。这个操作增加了扩散Diffusion特性使得一个字节的变化能在多轮后影响到整个状态矩阵的多个字节。 移位后的状态矩阵示意箭头表示移动方向原行0: [a00, a01, a02, a03] - 不变 原行1: [a10, a11, a12, a13] - [a11, a12, a13, a10] 原行2: [a20, a21, a22, a23] - [a22, a23, a20, a21] 原行3: [a30, a31, a32, a33] - [a33, a30, a31, a32]列混合MixColumns这是最复杂的一步将State的每一列视为GF(2^8)上的一个四项多项式与一个固定的多项式 c(x) {03}x^3 {01}x^2 {01}x {02} 进行模 x^41 乘法。这个运算可以表示为矩阵乘法| b0 | | 02 03 01 01 | | a0 | | b1 | | 01 02 03 01 | * | a1 | | b2 | | 01 01 02 03 | | a2 | | b3 | | 03 01 01 02 | | a3 |这里的乘法和加法都是定义在GF(2^8)上的。在实际代码中我们同样可以通过查表列混合表或直接计算来实现。它极大地增强了扩散效果。轮密钥加AddRoundKey这是最简单的一步将当前的状态矩阵与当前轮的轮密钥也是一个4x4矩阵进行逐字节的异或XOR操作。State[i][j] ^ RoundKey[i][j]。轮密钥是从原始密钥通过密钥扩展算法派生出来的。2.3 密钥扩展算法一把钥匙开多把锁原始的密钥16/24/32字节并不直接用于每一轮。密钥扩展算法Key Expansion的作用是生成一个长度为4*(Nr1)字的扩展密钥数组一个字4字节其中每一轮使用连续4个字作为该轮的轮密钥。 以AES-128为例原始密钥为16字节4个字w[0], w[1], w[2], w[3]。扩展算法递归地生成后续的w[i]如果 i 不是4的倍数则 w[i] w[i-4] ^ w[i-1]。如果 i 是4的倍数则 w[i] w[i-4] ^ T(w[i-1])。其中T函数包含将w[i-1]循环左移一个字节、字节替换用S盒、再与轮常数Rcon[j]j为轮数进行异或。 这个算法确保了轮密钥之间具有足够的非线性关系避免了从部分轮密钥推导出原始密钥。解密过程就是加密过程的逆序依次执行逆变换逆轮密钥加、逆列混合InvMixColumns、逆行移位InvShiftRows、逆字节替换InvSubBytes。逆列混合对应的固定多项式为 d(x) {0b}x^3 {0d}x^2 {09}x {0e}。注意理解这些数学原理对于调试至关重要。当你的加密结果与标准库不一致时可以逐步对比每一轮变换后的状态矩阵从而精准定位是S盒、行移位、列混合还是密钥扩展出了错。我建议在开发初期为每一个变换函数编写独立的单元测试用标准测试向量如NIST发布的FIPS-197附录C的示例进行验证。3. 工程架构与核心类设计理解了原理我们开始搭建代码骨架。一个好的架构能让支持六种模式ECB/CBC x 128/192/256和多种IO格式变得清晰可控。我将项目核心分为三个部分核心算法模块AESCore、模式控制器AESMode、输入输出处理器IOHandler。3.1 AESCore类算法的纯粹实现这个类只关心最纯粹的AES变换不涉及分组模式也不关心数据从哪里来到哪里去。它的接口非常干净。class AESCore { public: enum KeySize { AES_128 16, AES_192 24, AES_256 32 }; AESCore(KeySize size); bool setKey(const unsigned char* key); // 设置密钥并执行密钥扩展 void encryptBlock(unsigned char* inout); // 原地加密一个16字节块 void decryptBlock(unsigned char* inout); // 原地解密一个16字节块 // ... 内部实现细节SubBytes, ShiftRows, MixColumns, KeyExpansion 等私有函数 private: KeySize m_keySize; int m_nr; // 轮数 unsigned char m_roundKey[240]; // 最大扩展密钥空间 (AES-256: 15轮 * 16字节) // S盒、逆S盒、列混合表等静态查找表 };为什么这样设计将算法核心隔离出来使得单元测试变得极其容易。你可以单独测试encryptBlock对一个已知明文和密钥的输出是否正确。这也是软件设计中“单一职责原则”的体现。3.2 AESModeController类组织加密模式这个类负责管理分组密码模式ECB、CBC和填充Padding。它是用户主要交互的接口之一。class AESModeController { public: enum Mode { ECB, CBC }; enum Padding { PKCS7, ZERO }; // 常见的填充方式 AESModeController(AESCore::KeySize keySize, Mode mode); void setKey(const unsigned char* key, int length); void setIV(const unsigned char* iv); // CBC模式需要初始化向量IV std::vectorunsigned char encrypt(const std::vectorunsigned char plaintext, Padding pad PKCS7); std::vectorunsigned char decrypt(const std::vectorunsigned char ciphertext, Padding pad PKCS7); // ... 处理CBC模式的链式操作以及填充的添加与移除 private: AESCore m_aesCore; Mode m_mode; std::vectorunsigned char m_iv; // 内部方法applyPadding, removePadding, ecbEncrypt, cbcEncrypt等 };关键点分析填充Padding。因为AES是分组密码一次处理16字节。如果明文长度不是16的整数倍就需要填充。PKCS#7是最常用的标准缺n个字节就填充n个值为n的字节。例如如果最后缺3字节则填充0x03 0x03 0x03。解密后需要准确移除这些填充字节。ZERO填充则用0x00填充但需要额外记录原始数据长度否则无法区分末尾的0是填充还是有效数据。3.3 IOHandler命名空间灵活应对各种数据源这是让工具变得好用的关键。我们需要处理字符串UTF-8、十六进制字符串“A1B2C3”、二进制文件。我将它们设计为一组独立的函数而不是类因为它们是无状态的工具。namespace IOHandler { // 字符串 - 字节向量 (默认UTF-8) std::vectorunsigned char stringToBytes(const std::string str); std::string bytesToString(const std::vectorunsigned char bytes); // 十六进制字符串 - 字节向量 std::vectorunsigned char hexStringToBytes(const std::string hex); std::string bytesToHexString(const std::vectorunsigned char bytes, bool uppercase false); // 文件 - 字节向量 bool readFile(const std::string filepath, std::vectorunsigned char content); bool writeFile(const std::string filepath, const std::vectorunsigned char content); // 自动检测输入类型简单启发式是否全是0-9a-fA-F且长度为偶数 enum InputType { BINARY, HEX_STRING, PLAIN_STRING }; InputType detectInputType(const std::string input); }十六进制处理的坑hexStringToBytes函数要处理用户可能输入的空白字符空格、换行、大小写混用甚至“0x”前缀。一个健壮的实现需要先清理字符串再逐两个字符用sscanf或查表转换为一个字节。反之bytesToHexString则要确保输出格式统一便于复制和比对。4. 六种模式的具体实现与对比“六种模式”实质是2种分组模式 x 3种密钥长度的排列组合。我们分别看看ECB和CBC的实现差异。4.1 ECB模式最简单的电子密码本ECBElectronic Codebook模式最简单将明文分割成独立的16字节块每个块用相同的密钥独立加密。std::vectorunsigned char AESModeController::ecbEncrypt(const std::vectorunsigned char plaintext) { std::vectorunsigned char padded applyPadding(plaintext, m_padding); std::vectorunsigned char ciphertext(padded.size()); for (size_t i 0; i padded.size(); i 16) { unsigned char block[16]; memcpy(block, padded[i], 16); m_aesCore.encryptBlock(block); // 核心加密调用 memcpy(ciphertext[i], block, 16); } return ciphertext; }ECB的致命弱点相同的明文块必然产生相同的密文块。这对于图像、音频等具有重复模式的数据是灾难性的。即使加密了数据的模式依然可见。因此在绝大多数实际应用中不推荐使用ECB模式。但作为学习和测试基准它不可或缺。4.2 CBC模式带反馈的链式加密CBCCipher Block Chaining模式解决了ECB的模式重复问题。它在加密前先将当前明文块与前一个密文块进行异或然后再加密。第一个块则与一个初始化向量IV进行异或。std::vectorunsigned char AESModeController::cbcEncrypt(const std::vectorunsigned char plaintext) { std::vectorunsigned char padded applyPadding(plaintext, m_padding); std::vectorunsigned char ciphertext(padded.size()); unsigned char prevBlock[16]; memcpy(prevBlock, m_iv.data(), 16); // 使用IV作为第一个“前驱密文块” for (size_t i 0; i padded.size(); i 16) { unsigned char block[16]; memcpy(block, padded[i], 16); // XOR with previous ciphertext block (or IV) for (int j 0; j 16; j) { block[j] ^ prevBlock[j]; } m_aesCore.encryptBlock(block); memcpy(ciphertext[i], block, 16); memcpy(prevBlock, block, 16); // 更新前驱块为当前密文块 } return ciphertext; }解密过程则是逆过程先解密当前块再与前一个密文块或IV异或得到明文。CBC模式的关键IV必须是随机的、不可预测的且不需要保密但每次加密都应更换。一个常见的错误是使用固定IV或全零IV这会削弱安全性。错误传播CBC模式中一个密文块在传输中损坏会导致对应明文块以及下一个明文块的解密失败因为下一个块解密需要用到当前损坏的密文块做异或。但这在某些场景下被视为一种“完整性”的弱验证。实操心得在测试CBC模式时务必验证其“链式”特性。你可以尝试修改密文中间的某一个字节观察解密后对应明文块及后续一个块都变成了乱码而ECB模式下只会影响一个块。这是理解分组模式差异最直观的实验。5. 从字符串到文件输入输出处理的实战细节工具的好用与否很大程度上取决于IO处理的鲁棒性和便利性。我们的目标是让用户可以通过命令行轻松指定输入是字符串、十六进制文本还是文件并指定输出格式。5.1 命令行参数解析设计一个典型的用法可能是./aes_tool -m cbc -k 256 -i Hello World --iv random -o hex ./aes_tool -m ecb -k 128 -f input.bin -o output.enc我们需要一个灵活的解析器。我推荐使用getoptPOSIX或argparse第三方库但对于自包含项目一个简单的循环也能胜任。关键是要清晰定义参数-m, --mode:ecb或cbc-k, --keylength:128,192,256-i, --input: 直接输入字符串-f, --file: 输入文件路径--iv: 指定IV十六进制字符串或使用random生成--key: 密钥字符串或十六进制。注意实际项目中密钥应从安全渠道获取这里仅为演示。-o, --output-format:bin(二进制),hex(十六进制文本),base64(可选扩展)-d, --decrypt: 解密模式5.2 密钥与IV的生成与管理密钥输入支持直接输入字符串如-k mySecretKey程序内部将其转换为字节序列注意字符编码。更安全的方式是输入十六进制密钥如-k 2b7e151628aed2a6abf7158809cf4f3c。对于AES-128十六进制字符串长度应为32字符16字节。IV的生成对于CBC加密如果用户未提供IV必须生成一个密码学安全的随机IV。在C11及以上可以使用random库中的std::random_device和std::uniform_int_distribution来生成随机字节。切勿使用rand()或时间戳作为IV它们不具备密码学安全性。std::vectorunsigned char generateRandomIV() { std::vectorunsigned char iv(16); std::random_device rd; std::uniform_int_distributionunsigned short dist(0, 255); for (auto byte : iv) { byte static_castunsigned char(dist(rd)); } return iv; }一个重要提示IV需要随密文一起存储或传输因为解密时需要同样的IV。通常的做法是将IV拼接在密文前面例如前16字节是IV后面是真正的密文。5.3 文件操作与大数据处理对于大文件不能一次性读入内存。我们的工具虽然演示了完整流程但在处理大文件时应采用流式处理。bool encryptFile(const std::string inputPath, const std::string outputPath, AESModeController aes) { std::ifstream inFile(inputPath, std::ios::binary); std::ofstream outFile(outputPath, std::ios::binary); if (!inFile || !outFile) return false; // 如果是CBC模式生成并写入IV std::vectorunsigned char iv; if (aes.getMode() AESModeController::CBC) { iv generateRandomIV(); outFile.write(reinterpret_castconst char*(iv.data()), iv.size()); aes.setIV(iv.data()); } const size_t bufferSize 1024 * 16; // 16KB缓冲区保持是16字节的倍数 std::vectorunsigned char buffer(bufferSize); std::vectorunsigned char plainBlock(16); size_t plainBlockPos 0; while (inFile.read(reinterpret_castchar*(buffer.data()), bufferSize) || inFile.gcount() 0) { size_t bytesRead inFile.gcount(); for (size_t i 0; i bytesRead; i) { plainBlock[plainBlockPos] buffer[i]; if (plainBlockPos 16) { // 加密一个完整块 aes.encryptBlockInPlace(plainBlock.data()); // 假设有原地加密接口 outFile.write(reinterpret_castconst char*(plainBlock.data()), 16); plainBlockPos 0; } } } // 处理最后的不完整块填充 if (plainBlockPos 0) { // ... 应用PKCS7填充到最后一个块 aes.encryptBlockInPlace(plainBlock.data()); outFile.write(reinterpret_castconst char*(plainBlock.data()), 16); } return true; }这种流式处理方式可以加密任意大小的文件内存占用恒定。6. 完整代码走读与关键函数剖析由于篇幅限制这里无法贴出全部上千行代码但我会剖析几个最核心、最容易出错的函数并解释其实现要点。完整的代码工程建议采用模块化文件组织aes_core.cpp,aes_modes.cpp,io_handler.cpp,main.cpp。6.1 密钥扩展KeyExpansion的实现这是AES正确性的基石。以AES-128为例void AESCore::keyExpansion(const unsigned char* key) { unsigned char temp[4]; // 拷贝原始密钥到扩展密钥数组的前4个字 for (int i 0; i 4; i) { m_roundKey[i*4] key[i*4]; m_roundKey[i*41] key[i*41]; m_roundKey[i*42] key[i*42]; m_roundKey[i*43] key[i*43]; } // 生成后续的轮密钥 for (int i 4; i 4 * (m_nr 1); i) { // 临时变量 前一个字 for (int j 0; j 4; j) { temp[j] m_roundKey[(i-1)*4 j]; } if (i % 4 0) { // 对前一个字进行T函数变换RotWord SubWord Rcon // 1. 循环左移一个字节 unsigned char k temp[0]; temp[0] temp[1]; temp[1] temp[2]; temp[2] temp[3]; temp[3] k; // 2. 字节替换S盒 for (int j 0; j 4; j) { temp[j] sbox[temp[j]]; } // 3. 与轮常数异或 temp[0] ^ rcon[i/4]; } // 生成当前字w[i] w[i-4] ^ temp for (int j 0; j 4; j) { m_roundKey[i*4 j] m_roundKey[(i-4)*4 j] ^ temp[j]; } } }关键点rcon是轮常数数组定义在别处rcon[1] 0x01, rcon[2] 0x02, rcon[3] 0x04, ...后续每个值是前一个值在GF(2)上乘以{02}。对于AES-192和AES-256密钥扩展的逻辑略有不同主要体现在i % Nk 0的判断上Nk是原始密钥的字数128为4192为6256为8并且AES-256在i % Nk 4时也需要进行一次SubWord操作。必须严格按照标准实现。6.2 列混合MixColumns的查表优化直接计算列混合涉及大量的有限域乘法和异或性能较差。工业级实现都采用查表法。我们可以预先计算好一个“列混合表”也称为T表。 原理是将列混合的矩阵乘法运算转化为对状态矩阵每个字节的查表与异或。具体来说对于状态矩阵的一列[a0, a1, a2, a3]^T结果列[b0, b1, b2, b3]^T可以通过四个查找表T0, T1, T2, T3计算得出b0 T0[a0] ^ T1[a1] ^ T2[a2] ^ T3[a3] b1 T0[a1] ^ T1[a2] ^ T2[a3] ^ T3[a0] b2 T0[a2] ^ T1[a3] ^ T2[a0] ^ T3[a1] b3 T0[a3] ^ T1[a0] ^ T2[a1] ^ T3[a2]每个T表有256个项每个项是一个32位字。这样原本需要16次有限域乘法和12次异或的一列运算变成了4次查表和4次异或性能提升巨大。在代码中这些表是静态常量数组。解密时使用对应的逆表Td0, Td1, Td2, Td3。6.3 PKCS7填充的添加与移除这是一个看似简单但容易出错的细节。std::vectorunsigned char applyPadding(const std::vectorunsigned char data, PaddingType type) { if (type ! PKCS7) { /* 处理其他填充 */ } size_t blockSize 16; size_t padLen blockSize - (data.size() % blockSize); if (padLen 0) padLen blockSize; // 如果刚好对齐额外填充一个完整块 std::vectorunsigned char padded(data); padded.insert(padded.end(), padLen, static_castunsigned char(padLen)); return padded; } std::vectorunsigned char removePadding(const std::vectorunsigned char paddedData, PaddingType type) { if (type ! PKCS7) { /* 处理其他填充 */ } if (paddedData.empty()) return paddedData; unsigned char padLen paddedData.back(); // 安全检查padLen必须在1到16之间且最后padLen个字节的值都必须等于padLen if (padLen 0 || padLen 16) { throw std::runtime_error(Invalid PKCS7 padding.); } for (size_t i paddedData.size() - padLen; i paddedData.size(); i) { if (paddedData[i] ! padLen) { throw std::runtime_error(Invalid PKCS7 padding.); } } return std::vectorunsigned char(paddedData.begin(), paddedData.end() - padLen); }踩坑提醒解密后移除填充时必须进行严格的有效性检查。恶意构造的密文可能导致padLen值超出范围如果不检查就直接截断可能会造成数据损坏或安全漏洞如Padding Oracle Attack的潜在前提。7. 测试、验证与性能考量自己实现的加密算法必须经过严苛的测试才能让人放心使用。7.1 使用标准测试向量验证NIST FIPS-197文档的附录C提供了完整的测试向量包括AES-128/192/256的加密解密示例。这是验证我们算法实现正确性的黄金标准。你需要编写测试函数将标准密钥、明文输入你的算法逐字节比对输出密文是否一致。同样用密文和密钥解密看是否能还原明文。一个实用的调试技巧当测试失败时不要只对比最终结果。应该编写一个“中间状态输出”函数在每一轮加密后打印出State矩阵的内容与标准文档中提供的中间值进行比对。这样可以快速定位是哪个变换SubBytes, ShiftRows, MixColumns, AddRoundKey出了问题。7.2 与OpenSSL结果交叉比对除了标准测试向量还可以用OpenSSL命令行工具作为参照。例如# 使用OpenSSL ECB模式加密 echo -n Hello World123456 | openssl enc -aes-128-ecb -K $(echo -n my16bytekey.... | xxd -p) -nosalt | xxd -p # 使用我们自己的工具加密 ./aes_tool -m ecb -k 128 -i Hello World123456 --key my16bytekey.... -o hex确保两者的输出密文的十六进制表示完全一致。对于CBC模式还需要保证IV一致。这个过程能验证整个流程包括填充处理是否正确。7.3 性能分析与优化思路一个纯教育实现的AES性能通常远低于高度优化的库如Intel AES-NI指令集加速。但我们可以做一些优化查表法如前所述使用T表实现MixColumns和SubBytes的合并运算是最大的性能提升点。循环展开在加密/解密的主循环中手动展开几轮循环可以减少循环开销。因为AES轮数是固定的10,12,14完全可以展开。内存对齐确保状态矩阵和轮密钥在内存中对齐到16字节边界某些平台下能提升内存访问速度。避免动态内存分配在核心的encryptBlock函数内部使用栈上数组而非new或vector减少开销。你可以使用简单的计时函数来对比优化前后的速度。但请记住安全永远比性能更重要。在未经过充分审计和测试之前切勿将自实现的加密算法用于生产环境。这个项目的首要目标是教育和理解。8. 常见问题排查与安全警示在实现和使用过程中你肯定会遇到各种“坑”。这里总结几个典型问题。8.1 密文比对失败编码与填充的陷阱问题描述你的程序加密“Hello World”得到的十六进制结果和网上某个在线工具的结果不一样。排查步骤确认密钥和IV确保双方使用的密钥字节序列完全一致。你是将字符串“key”直接转换为ASCII字节还是进行了其他哈希处理在线工具可能默认进行了某种转换。确认明文“Hello World”是否包含末尾的换行符字符串编码是UTF-8还是GBK对于中文字符差异会更大。最稳妥的方式是使用十六进制明文进行测试。确认模式与填充双方都是ECB模式吗都是PKCS7填充吗有些旧工具可能使用ZeroPadding或NoPadding。确认数据块对于CBC模式IV是否一致IV是否被拼接在密文前建议始终先用标准测试向量纯十六进制表示的密钥、明文、IV验证核心算法的正确性排除算法实现本身的问题。然后再处理字符串编码等外围问题。8.2 解密后出现乱码或多余字符这几乎肯定是填充移除环节出了问题。检查填充验证逻辑你的removePadding函数是否严格执行了有效性检查如果直接信任padLen并从末尾截断当密文被篡改或解密密钥错误时解出的“明文”的最后一个字节可能是任意值比如0x23程序会错误地截断最后35个字节导致大量乱码。区分二进制与文本如果你加密的是文本解密后得到字节序列需要正确转换为字符串。如果加密时输入的是带BOM的UTF-8文本解密后也要按相同方式解读。CBC模式的IV解密时使用的IV必须和加密时完全一致。如果IV是随机的并保存在文件头解密时你是否正确读取了前16字节作为IV8.3 关于安全性的重要警示请务必理解以下几点本实现用于学习这个自己实现的AES库目的是教学和深入理解。它没有经过专业密码学家的审计可能包含微妙的实现漏洞如侧信道攻击绝对不应用于任何真实的生产系统、网络通信或敏感数据保护。使用权威库在实际项目中请使用经过长期实战检验的库如OpenSSL, libsodium, Crypto等。它们经过了优化和安全性审查。密钥管理是关键加密系统的安全性很大程度上取决于密钥如何生成、存储、分发和销毁。算法公开且坚固但密钥泄露则全盘皆输。模式选择如无特殊兼容性要求避免使用ECB。对于新项目更推荐使用认证加密模式如GCMGalois/Counter Mode它在提供保密性的同时还能提供完整性认证。通过这个从零实现AES的项目你获得的不只是一段能运行的代码而是对对称加密筋骨脉络的深刻认知。下次当你再调用AES_encrypt这样的函数时你脑海中将清晰地浮现出字节在S盒中替换、矩阵在行间移位、列在有限域中混合的场景。这种理解是单纯调用API永远无法给予的。