ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++实现AES加密解密:从算法原理到代码实践

C++实现AES加密解密:从算法原理到代码实践 简介本资源是一份面向C初学者与信息安全入门者的简易AES加解密算法实现项目聚焦密码学原理落地与核心算法手写实践帮助读者理解对称加密机制、掌握SubBytes/ShiftRows/MixColumns等AES核心轮函数的C编码实现。压缩包共4个文件325KB含关键实现代码main.cpp、原理与步骤详解的PDF说明文档、简洁清晰的README.md使用指南及标准MIT许可证文件结构精炼、即下即用。已有552人学习下载适合课程设计、密码学实验或安全编程练手——读者可直接编译运行观察128位明文在自定义密钥下的完整加解密流程深入理解S盒查表、密钥扩展、逆向解密逻辑等关键细节并基于源码进一步拓展CBC/ECB模式或集成到实际应用中。 最近在捣鼓一个本地工具需要给一批配置文件做对称加密又不方便在服务器上额外装第三方库索性就用C自己实现了一套AES加解密。写完之后回头看这个活儿比想象中琐碎S盒生成、列混合、密钥扩展、分组模式、填充规则每一步都有坑。这篇就把我的实现过程整理出来从算法原理到可运行的C代码再到实际踩坑记录给同样需要在C里实现或调用AES的朋友一个完整的参考。这篇内容适合谁看如果你正在学习AES算法原理想用C从零实现一遍加深理解或者你需要在嵌入式环境、无第三方依赖的C项目里做加解密再或者你只是写完代码后想搞清楚为什么一定要传Key和IV、为什么密文要Base64编码这篇都能帮到你。实现上我尽量保持代码结构清晰、依赖为零C11及以上就能直接编译跑起来。1. AES核心概念与方案选型1.1 AES到底在加密什么AES全称Advanced Encryption Standard是一种分组对称加密算法。分组的意思是说它每次处理的明文长度是固定的——128位也就是16个字节。如果你的数据不是16字节的整数倍就必须在加密之前做填充Padding这就是后文要讲的PKCS#7填充规则的由来。AES的密钥长度有三种128位16字节、192位24字节、256位32字节对应我们常说的AES-128、AES-192、AES-256。密钥越长安全强度越高但计算轮数也越多。以AES-128为例它要对数据块执行10轮变换AES-192是12轮AES-256是14轮。每轮包括字节代换SubBytes、行移位ShiftRows、列混合MixColumns、轮密钥加AddRoundKey四个步骤。在写代码前先想清楚方案密钥长度选128位还是256位如果业务数据需要长期保存且敏感度较高建议AES-256如果只是临时传输或学习用途AES-128足够。工作模式选ECB还是CBCECB模式下同样的明文会得到同样的密文容易泄露数据模式特征只适合加密长度固定的随机数据比如密钥本身CBC模式引入了IV初始向量相同明文在不同IV下密文不同更适合加密文本和文件内容。填充规则选PKCS#7还是ZeroPaddingPKCS#7是通用标准OpenSSL、Java的AES库默认都支持ZeroPadding对二进制数据不友好因为数据末尾的0x00会被误混淆。1.2 加密模式的选择要结合场景ECB模式有个很著名的缺点因为每个分组独立加密同样的明文字块会得到同样的密文字块。我记得之前有人拿它加密一张企鹅图片加密后企鹅轮廓还能隐约看出来因为颜色块相同的区域产生的密文块也相同。所以ECB基本只在我加密一段随机生成的AES密钥时才用它因为随机数据本身没有模式可暴露。CBC模式是目前用得最多的。它在每个分组加密前先让明文分组和上一个密文分组做异或第一个分组则和IV做异或。这样即使两个分组明文相同因为前一个密文不同加密后的密文也不同。CBC模式下解密的并行性不好加密也一样必须按顺序依赖前一个分组这是它天然的特性。CTR模式把AES变成一个流密码每一块先加密计数器的值再和明文异或得到密文。CTR的好处是加密解密完全对称而且可以并行计算性能很好。热词里有人提到aes/gcm/pkcs5padding和aes/gcm/nopaddingGCM本质上是CTR模式加上GMAC认证它在内部用的是CTR计数器所以不需要传统意义的填充。GCM模式在C里用OpenSSL实现比较方便纯手写GMAC会涉及到多项式乘法代码量不小。我在这个项目里实现ECB和CBC两种模式覆盖90%的应用场景同时把填充逻辑独立成模块后续想扩展CTR也只是加一个分组加密调用的出口不破坏已有代码结构。1.3 填充规则为什么这么重要分组加密要求明文长度必须是16字节的整数倍如果不够就需要填充。PKCS#7的规则很朴素缺几个字节就补几个值为“缺少数”的字节。缺1字节就补一个0x01缺5字节就补五个0x05如果明文长度恰好是16字节的整数倍还要额外填充16个0x10。这个额外填充很关键否则解密时你无法区分“明文结尾本来就带的0x01”和“填充出来的0x01”。解密时只需要读取最后一个字节假设值是n就检查最后n个字节是否都是n然后丢弃。如果校验收到的填充不合法直接返回异常不要尝试继续解密——这是很多CVE的根源。我用一个简单的结构体来表示填充后的结果struct PaddedData { std::vectorunsigned char data; size_t paddingCount; };为什么用unsigned char而不是char因为char在C里有没有符号是编译器决定的做字节级的位运算时容易出问题。统一用unsigned char后续做S盒替换、行移位都不用担心符号扩展。2. C工程环境与代码结构设计2.1 开发环境的快速搭建我在Windows上用VS Code写这个项目配置起来其实很简单。前提是已经装好了MinGW-w64或MSVC编译器VS Code里装好C/C扩展然后创建.vscode/tasks.json和.vscode/launch.json分别配置编译命令和调试参数。tasks.json里我用的编译命令是这样的{ version: 2.0.0, tasks: [ { label: build aes, type: shell, command: g, args: [ -stdc11, -Wall, -O2, src/main.cpp, src/aes.cpp, -o, build/aes_demo.exe ], group: { kind: build, isDefault: true } } ] }-Wall打开警告写C密码学代码时警告一定要当成错误处理因为很多位运算的隐式转换会产生警告而这些警告在字节运算中往往意味着逻辑错误。也可以用CMake来组织如果项目后续会加入更多文件CMake更清晰cmake_minimum_required(VERSION 3.10) project(AesDemo) set(CMAKE_CXX_STANDARD 11) add_executable(aes_demo src/main.cpp src/aes.cpp)选择哪种都行核心是把代码拆分好不要全堆在一个文件里。2.2 接口设计思路在设计接口时有两条路一条是直接暴露函数另一条是封装成类。我这次选择用类和静态方法混合的方式好处是调用方不需要关心内部状态加解密过程不持有跨调用的敏感数据用完即走降低安全风险。基本接口如下class Aes { public: enum Mode { ECB, CBC }; // 加密输入明文、密钥、IVCBC模式、模式输出密文 static std::vectorunsigned char Encrypt( const std::vectorunsigned char plaintext, const std::vectorunsigned char key, const std::vectorunsigned char iv, Mode mode); // 解密输入密文、密钥、IVCBC模式、模式输出明文 static std::vectorunsigned char Decrypt( const std::vectorunsigned char ciphertext, const std::vectorunsigned char key, const std::vectorunsigned char iv, Mode mode); };为什么IV只在CBC模式需要ECB模式就不传因为ECB没有异或依赖传了也没用API层面就要约束清楚。为什么不用std::string直接传明文因为加密结果可能包含任意字节值std::string虽然能存二进制但语义上是字符串容易在后续处理中引入编码问题也容易让使用者误以为返回的是可打印字符。std::vectorunsigned char才是字节流的正解。还有一个细节Aes::Decrypt内部不负责识别密文是否被篡改CBC模式的解密若密文被改了只会影响当前块和下一块的解密结果。如果需要完整性检测应该在密文中附加MAC或者直接用GCM模式。2.3 为什么没用OpenSSL很多人第一反应是直接用OpenSSL我为什么自己写有几个原因嵌入式或受限环境里不一定允许引入OpenSSL很多物联网芯片的SDK体积和证书要求都不允许。学习价值从零实现一遍能彻底理解AES内部机制排查问题时更有底。可控性OpenSSL版本差异大不同版本对Provider的配置方式不同写一套兼容代码有时候比自己实现还要费劲。但要注意如果你只是做业务功能不追求学习直接用OpenSSL或其他成熟库是更稳妥、更安全的选择。自己写AES有一个很大的风险——侧信道攻击和测试不充分可能导致安全漏洞这个我心里有数。所以我在代码里明确注释本项目适合学习与低敏感度场景生产环境请优先使用OpenSSL。3. 核心实现密钥扩展与轮函数3.1 S盒与字节代换AES的S盒本质上是一个256字节的查找表它把每个字节映射为另一个字节。这个映射由两步构成第一步在GF(2^8)有限域上计算乘法逆元0映射为0第二步做仿射变换。手算S盒不现实标准实现里一般直接把S盒预计算出来。计算S盒的代码void GenerateSbox(unsigned char sbox[256]) { unsigned char inv[256]; unsigned char p 1, q 1; // 乘法逆元计算 do { p p ^ (p 1) ^ (p 0x80 ? 0x1B : 0x00); inv[p] q; q ^ q 1; q ^ q 0x80 ? 0x1B : 0x00; } while (p ! 1); inv[0] 0; // 仿射变换 for (int i 0; i 256; i) { unsigned char x inv[i], y x; y ^ (x 1) | (x 7); y ^ (x 2) | (x 6); y ^ (x 3) | (x 5); y ^ (x 4) | (x 4); sbox[i] y ^ 0x63; } }这个生成过程看起来有点绕但它是理解AES密码学性质的关键。S盒的非线性是AES安全性的基石如果S盒是线性的整个算法就可以用线性代数攻破。逆S盒就是S盒的反函数映射解密时直接查。我建议你把S盒生成后打出来存成静态数组而不是每次启动都计算一遍。虽然计算一次只要几微秒但静态数组可靠性更高也方便审查。3.2 行移位与列混合字节代换之后是行移位。AES把16字节状态排列成4x4矩阵行移位就是第0行不动第1行循环左移1字节第2行左移2字节第3行左移3字节。void ShiftRows(unsigned char state[4][4]) { unsigned char 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位 std::swap(state[2][0], state[2][2]); std::swap(state[2][1], state[2][3]); // 第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)上的多项式与固定多项式{03}x^3 {01}x^2 {01}x {02}相乘后模x^4 1。在C里实现一般用查表法定义乘2xtime和乘3两个辅助函数unsigned char xtime(unsigned char x) { return (x 1) ^ (x 0x80 ? 0x1B : 0x00); } void MixColumns(unsigned char state[4][4]) { for (int c 0; c 4; c) { unsigned char a0 state[0][c]; unsigned char a1 state[1][c]; unsigned char a2 state[2][c]; unsigned char a3 state[3][c]; state[0][c] xtime(a0) ^ (xtime(a1) ^ a1) ^ a2 ^ a3; state[1][c] a0 ^ xtime(a1) ^ (xtime(a2) ^ a2) ^ a3; state[2][c] a0 ^ a1 ^ xtime(a2) ^ (xtime(a3) ^ a3); state[3][c] (xtime(a0) ^ a0) ^ a1 ^ a2 ^ xtime(a3); } }这里xtime(a) ^ a等价于乘3因为{03}x {02}x {01}x。列混合是“扩散”的核心它使得一个字节的改动经过多轮后影响所有字节。3.3 密钥扩展AES每轮需要一个轮密钥轮密钥从主密钥通过密钥扩展算法生成。AES-128需要11轮子密钥初始轮加10轮每轮16字节所以总共需要176字节的扩展密钥。密钥扩展的核心逻辑void KeyExpansion(const unsigned char* key, unsigned char* roundKeys, int keyBytes, int rounds) { int nk keyBytes / 4; // 以4字节为单位的密钥长度 int totalWords 4 * (rounds 1); // 先把主密钥拷贝到前nk个字 for (int i 0; i nk; i) { roundKeys[4 * i 0] key[4 * i 0]; roundKeys[4 * i 1] key[4 * i 1]; roundKeys[4 * i 2] key[4 * i 2]; roundKeys[4 * i 3] key[4 * i 3]; } // 扩展 for (int i nk; i totalWords; i) { unsigned char temp[4]; temp[0] roundKeys[4 * (i - 1) 0]; temp[1] roundKeys[4 * (i - 1) 1]; temp[2] roundKeys[4 * (i - 1) 2]; temp[3] roundKeys[4 * (i - 1) 3]; if (i % nk 0) { RotWord(temp); SubWord(temp, sbox); temp[0] ^ Rcon[i / nk]; } for (int j 0; j 4; j) { roundKeys[4 * i j] roundKeys[4 * (i - nk) j] ^ temp[j]; } } }RotWord是循环左移一字节SubWord是逐字节查S盒Rcon是轮常数数组{0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36}。密钥扩展这部分容易出错的是Rcon的索引i / nk只在i % nk 0时使用而且Rcon从1开始。如果你写成了Rcon[i]密钥一长就会越界。4. 模式与填充的完整实现4.1 PKCS#7填充的实现填充函数和解填充函数std::vectorunsigned char Pkcs7Pad(const std::vectorunsigned char data, size_t blockSize) { size_t padLen blockSize - (data.size() % blockSize); std::vectorunsigned char result data; result.insert(result.end(), padLen, static_castunsigned char(padLen)); return result; } bool Pkcs7Unpad(std::vectorunsigned char data, size_t blockSize) { if (data.empty() || data.size() % blockSize ! 0) return false; size_t padLen data.back(); if (padLen 0 || padLen blockSize) return false; for (size_t i data.size() - padLen; i data.size(); i) { if (data[i] ! padLen) return false; } data.resize(data.size() - padLen); return true; }解填充一定要严格校验不能只看最后一个字节就删。攻击者可能构造非法的填充字节来干扰解密校验失败时应该返回错误而不是继续处理。4.2 ECB模式实现ECB的实现最简单就是把填充后的数据按16字节分成块逐块调用同一个加密函数std::vectorunsigned char Aes::EncryptECB( const std::vectorunsigned char plaintext, const std::vectorunsigned char key) { std::vectorunsigned char padded Pkcs7Pad(plaintext, 16); std::vectorunsigned char roundKeys ExpandKey(key); std::vectorunsigned char ciphertext(padded.size()); for (size_t i 0; i padded.size(); i 16) { EncryptBlock(padded[i], ciphertext[i], roundKeys); } return ciphertext; }解密时反向调用DecryptBlock注意用的是逆S盒、逆行移位和逆列混合。EncryptBlock的最前面要加一次AddRoundKey然后循环1到10轮前9轮做完整的四个步骤最后一轮不做MixColumns。这个结构不能乱少做一轮或者多做一轮都会导致结果错误。4.3 CBC模式实现CBC加密的核心在于异或链std::vectorunsigned char Aes::EncryptCBC( const std::vectorunsigned char plaintext, const std::vectorunsigned char key, const std::vectorunsigned char iv) { if (iv.size() ! 16) { throw std::invalid_argument(IV must be 16 bytes); } std::vectorunsigned char padded Pkcs7Pad(plaintext, 16); std::vectorunsigned char roundKeys ExpandKey(key); std::vectorunsigned char ciphertext(padded.size()); std::vectorunsigned char prev(16); std::copy(iv.begin(), iv.end(), prev.begin()); for (size_t i 0; i padded.size(); i 16) { for (size_t j 0; j 16; j) { padded[i j] ^ prev[j]; } EncryptBlock(padded[i], ciphertext[i], roundKeys); std::copy(ciphertext[i], ciphertext[i] 16, prev.begin()); } return ciphertext; }CBC解密是加密的逆过程先把当前密文块解密再与上一个密文块异或DecryptBlock(ciphertext[i], decrypted[i], roundKeys); for (size_t j 0; j 16; j) { decrypted[i j] ^ prev[j]; } std::copy(ciphertext[i], ciphertext[i] 16, prev.begin());注意这里prev更新是用原始密文块不是解密后的结果这个顺序反了就全错。我在刚开始实现时犯过这个错解密出来的第一块是乱的后面全跟着乱。4.4 Base64编码的必要性加密结果是一堆二进制的字节直接存到数据库或打印到终端都会出问题。最常见的做法是Base64编码把字节流转成可打印的ASCII字符串。Base64编码规则很简单每组3个字节24位分成4组6位每6位对应一个可打印字符。如果还剩下1或2个字节补号。std::string Base64Encode(const std::vectorunsigned char data) { static const char* table ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/; std::string result; size_t i 0; while (i 2 data.size()) { unsigned int n (data[i] 16) | (data[i1] 8) | data[i2]; result.push_back(table[(n 18) 0x3F]); result.push_back(table[(n 12) 0x3F]); result.push_back(table[(n 6) 0x3F]); result.push_back(table[n 0x3F]); i 3; } if (i 1 data.size()) { unsigned int n data[i] 16; result.push_back(table[(n 18) 0x3F]); result.push_back(table[(n 12) 0x3F]); result.push_back(); result.push_back(); } else if (i 2 data.size()) { unsigned int n (data[i] 16) | (data[i1] 8); result.push_back(table[(n 18) 0x3F]); result.push_back(table[(n 12) 0x3F]); result.push_back(table[(n 6) 0x3F]); result.push_back(); } return result; }Base64编码后密文长度会比原密文长约33%这在数据库列长度设计时要考虑。如果我提前知道要存到VARCHAR(255)那明文就必须控制在160字节以内128字节密文Base64膨胀。5. 验证与测试5.1 标准测试向量写完代码最怕的就是“自己觉得对”。AES有官方测试向量FIPS-197里给了AES-128的经典测试用例密钥000102030405060708090a0b0c0d0e0f明文00112233445566778899aabbccddeeff加密后密文69c4e0d86a7b0430d8cdb78070b4c55a我建议把这个测试向量直接写成单元测试任何一步出错都能立刻暴露#include cassert void TestAes128() { std::vectorunsigned char key { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; std::vectorunsigned char plaintext { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff }; std::vectorunsigned char expected { 0x69, 0xc4, 0xe0, 0xd8, 0x6a, 0x7b, 0x04, 0x30, 0xd8, 0xcd, 0xb7, 0x80, 0x70, 0xb4, 0xc5, 0x5a }; auto result Aes::EncryptECB(plaintext, key); assert(result expected); }注意这里明文正好是16字节填充后等于额外加了一个整块。所以测试时要写两种情况一种是刚好16字节的整数倍验证填充是否正确另一种是任意长度验证加解密往返是否成功。5.2 中文字符串的加解密处理如果你的明文是中文用std::string直接传入可能踩编码坑。在Windows上std::string可能保存的是GBK而Linux上通常是UTF-8。解决方案是明确指定字节流编码或者统一转成UTF-8再做加密。我的做法是提供一个对外的便捷接口接收std::string内部转成字节数组再加密std::string Aes::EncryptString( const std::string plaintext, const std::string key, const std::string iv, Aes::Mode mode) { std::vectorunsigned char ptext(plaintext.begin(), plaintext.end()); std::vectorunsigned char k(key.begin(), key.end()); std::vectorunsigned char i(iv.begin(), iv.end()); auto cipher Encrypt(ptext, k, i, mode); return Base64Encode(cipher); } std::string Aes::DecryptString( const std::string base64Cipher, const std::string key, const std::string iv, Aes::Mode mode) { auto cipher Base64Decode(base64Cipher); std::vectorunsigned char k(key.begin(), key.end()); std::vectorunsigned char i(iv.begin(), iv.end()); auto text Decrypt(cipher, k, i, mode); return std::string(text.begin(), text.end()); }这样做还有一层好处如果生产环境里你的密文是通过OpenSSL加密的只要模式、填充、Key、IV一致你自己实现的解密也能解出来。我实际测试过OpenSSL生成的CBC-PKCS7密文用我的代码能正常解密这在不依赖OpenSSL的环境里很管用。5.3 密钥和IV的生成很多人的困境不是加密本身而是不知道怎么生成密钥和IV。我的建议是使用认证随机数生成器不要手打密钥。在C11标准库里可以用std::random_device但它质量未必满足密码学要求更靠谱的方式是读操作系统的随机源。在Windows上我用BCryptGenRandom在Linux上读/dev/urandom封装成一个函数std::vectorunsigned char GenerateRandomBytes(size_t len) { std::vectorunsigned char buf(len); #ifdef _WIN32 // 用 BCryptGenRandom BCryptGenRandom(nullptr, buf.data(), (ULONG)len, BCRYPT_USE_SYSTEM_PREFERRED_RNG); #else std::ifstream rand(/dev/urandom, std::ios::binary); rand.read(reinterpret_castchar*(buf.data()), len); #endif return buf; }IV要求不可预测CBC模式下用随机数每次生成即可。密钥可以事先固定存储但存储位置要加密保护。不要把私钥硬编码在前面代码里——搜索历史里就有很多人把key 123456直接提交到代码仓库这种加密等于没有。6. 常见问题与排查技巧实录6.1 解密结果乱码的排查顺序遇到解密后乱码按以下顺序排查Key或IV不一致。最常见的错误加密解密用的不是同一套参数。尤其是IV有人加密时随机生成解密时又忘了保存。模式不一致。加密用CBC解密用ECB第一块会解出来后面全乱。编码不统一。明文中文字符串在加密时是UTF-8解密后按GBK显示乱码。填充不匹配。加密用PKCS#7填充解密时如果自己实现了Unpad但忘记校验可能反而解出带填充的脏数据。我建议在调试时打印中间过程——密钥十六进制、IV十六进制、填充后的明文、每轮之后的状态这样能快速定位是算法实现问题还是调用参数问题。6.2 安全性注意点自己实现的AES有一个最大问题没有防御侧信道攻击。标准的查表S盒实现在缓存时间上可能泄漏密钥信息这在面临物理攻击的本地环境中是风险。如果你只是学习或者处理低敏感数据问题不大但生产环境请一定用OpenSSL、Crypto等经过安全审计的库。另外注意密钥的存储——不要把密钥和密文放在同一个文件里加密日志输出时不要把密钥和IV打印出来。我在项目里专门写了一个安全工具类所有敏感数据用完后马上清理void SecureClear(std::vectorunsigned char data) { volatile unsigned char* p data.data(); for (size_t i 0; i data.size(); i) { p[i] 0; } data.clear(); }用volatile是为了防止编译器优化掉清零代码这在处理密钥缓冲区时很有必要。6.3 PKCS#5和PKCS#7的区别搜索里有人提到pkcs5padding这里统一说清楚PKCS#5原本只针对8字节块DES定义AES是16字节块严格说应该叫PKCS#7但很多Java代码里AES/CBC/PKCS5Padding其实就是PKCS#7填充规则完全一样。所以两者在AES语境下可以视为等价。6.4 关于GCM和NOPadding热词里有人问aes/gcm/pkcs5padding和aes/gcm/nopadding的区别GCM是CTR模式加认证CTR模式本身不需要填充所以无论你写PKCS5Padding还是NoPadding对GCM来说都无所谓——它压根不会填充。真正的区别在于你有没有开启认证。如果只做加密不做MACGCM的安全性优势就没了。我在生产项目中如果选GCM一定会校验返回的认证标签。如果想在自己代码里扩展GCM模式关键是要实现GMAC里的GHASH函数这个函数基于GF(2^128)域乘法计算量不小但可以查表优化。考虑到篇幅这边就不展开写GHash的完整实现了有兴趣的可以去看NIST SP 800-38D标准。7. 代码组织与后续扩展建议7.1 最终代码结构我最终的项目结构是这样aes_demo/ ├── CMakeLists.txt ├── include/ │ └── aes.h └── src/ ├── aes.cpp ├── base64.cpp └── main.cppaes.h声明了Aes类和Base64的编解码函数aes.cpp实现了密钥扩展、轮函数、分组加解密和模式逻辑base64.cpp独立处理编码。测试代码写在main.cpp里包含标准测试向量和若干自测用例。7.2 后续还能扩展什么如果你还想继续深入有几个方向CTR模式实现改动不大复用EncryptBlock函数改成加密计数器再异或适合并行加密场景。AES-192/AES-256支持密钥扩展已经写了通用逻辑只需把轮数参数和密钥长度传对。文件加解密大文件需要流式处理分块读取、分块加密注意每个分块的IV或计数器管理。GCM模式在CTR基础上增加GHASH认证安全性更高。我个人建议先把CTR和AES-256补上因为这个工作量不大但能覆盖更多场景需求。7.3 测试覆盖思路测试不能只测加解密往返要测边界空字符串加密保证填充逻辑不会溢出。明文恰好16字节验证PKCS#7会附加一个完整填充块解密后能正确移除。明文长度小于16字节。密钥长度不合法时是否抛出清晰异常。CBC模式IV不是16字节时是否被拦截。把这些用例写在测试代码里每次改动算法实现后跑一遍能防止重构把正确逻辑改坏。我之前在优化列混合性能后就踩过这个坑——用换表法代替xtime结果某个轮数下取值不对跑标准测试向量才抓出来。写在最后这次用C实现AES最大的收获不是代码本身而是把每一轮变换、每一个模式细节都摸透了。AES看起来公式复杂但拆开之后无非是查表、移位、异或的反复组合真正危险的藏在细节里——填充校验、IV管理、密钥生成、模式适配每一样都有讲究。如果你也是自己动手实现一遍我强烈建议写一份测试向量哪怕只是FIPS-197里的那一组都能救命。做完测试之后再去读OpenSSL里EVP接口的源码你会发现很多设计思路和自己写过之后的体会完全对应上了。最后再分享一个实操技巧在调试阶段可以在加密函数内部打印状态矩阵的十六进制值和标准算法的中间结果对比哪个字节变了就知道哪一步写错了。这个方法帮我快速定位过列混合的字节顺序问题希望对你也有用。本文还有配套的精品资源点击获取
返回列表