C++密码学工具箱:从凯撒密码到AES/RSA的算法实现与工程实践 1. 项目概述从零构建一个C加密解密工具箱最近在整理过去的项目代码翻到了一个自己早年写的加密解密工具集。当时是为了解决一个具体业务场景下的数据安全传输问题从零开始把几种常见的加密算法用C实现了一遍。现在回头看虽然代码有些稚嫩但整个从原理理解到代码落地的过程让我对密码学的基础和C的工程实践有了非常深刻的认识。今天我就把这个“工具箱”的构建思路、核心实现、以及踩过的那些坑系统地梳理分享出来。这个项目本质上是一个用C实现的、涵盖多种经典加密算法的程序库。它不仅能对字符串、文件进行加密和解密更重要的是通过亲手实现这些算法你能彻底搞明白对称加密、非对称加密、哈希函数这些概念到底是怎么在计算机里跑起来的。无论你是正在学习C和数据结构的在校生想通过一个综合项目练手还是已经工作的开发者需要处理一些简单的本地数据加密需求或者为面试中的算法实现题做准备这个项目都能给你带来实实在在的收获。我们会从最简单的凯撒密码和异或加密入手逐步深入到AES、RSA这些工业级算法并在最后探讨如何将它们有机地组合起来构建一个更健壮的应用。2. 核心算法选型与设计思路2.1 为什么选择这几种算法在设计这个工具箱时我并没有追求大而全而是精选了几类最具代表性、学习价值最高的算法。选型的核心思路是由浅入深覆盖密码学的主要分支。首先我纳入了古典密码比如凯撒密码和栅栏密码。它们算法简单非常适合作为入门帮助我们理解“加密”和“解密”这一对基本操作以及“密钥”的概念。虽然它们毫无安全性可言但实现过程能很好地锻炼基本的字符串处理和算法逻辑。紧接着是现代对称加密算法的代表——AES高级加密标准。这是目前全球使用最广泛的加密标准从文件加密到网络通信无处不在。在C中实现AES哪怕是一个简化版本也能让你深刻理解分组密码、轮函数、S盒、列混淆这些核心概念。我选择实现AES-128因为它结构清晰是理解更复杂变种的基础。然后必须包含非对称加密算法。我选择了RSA。它与对称加密完全不同基于大数分解的数学难题是数字签名、密钥交换的基石。实现RSA会让你接触到模幂运算、扩展欧几里得算法等数论知识对理解公钥密码体系至关重要。最后哈希函数也是密码学工具箱不可或缺的一部分。我实现了MD5和SHA-256。哈希函数是单向的用于验证数据完整性如文件校验或安全存储密码加盐哈希。实现它们能让你明白如何将任意长度数据“压缩”成固定长度的摘要。这个组合从古典到现代从对称到非对称再到哈希形成了一个完整的学习路径。在实际项目中它们也常常协同工作例如用RSA加密AES的密钥再用AES加密实际数据。2.2 整体架构设计为了让这个工具箱好用、易扩展我采用了简单的分层和面向对象的设计思想。整个项目结构大致如下CryptoKit/ ├── include/ │ ├── classical/ // 古典密码头文件 │ ├── symmetric/ // 对称加密头文件 (AES) │ ├── asymmetric/ // 非对称加密头文件 (RSA) │ └── hash/ // 哈希函数头文件 (MD5, SHA256) ├── src/ // 对应的源文件 ├── utils/ // 工具函数 (字节数组转换、填充处理等) └── main.cpp // 示例和测试程序我定义了一个基类CryptoAlgorithm它有一个简单的接口class CryptoAlgorithm { public: virtual ~CryptoAlgorithm() default; virtual std::string encrypt(const std::string plaintext) 0; virtual std::string decrypt(const std::string ciphertext) 0; // 对于哈希算法decrypt 方法可以抛出异常或返回空因为哈希不可逆 };然后各个算法的类如AES128RSA继承这个基类。这样在主程序里我可以通过基类指针来操作不同的算法方便管理和测试。当然对于RSA和哈希接口可能需要一些调整比如RSA需要区分公钥加密/私钥解密但核心思路是统一的。工具函数模块非常重要它处理那些琐碎但易错的工作比如将字符串转换成字节数组、进行PKCS#7填充、处理大整数对于RSA、将哈希结果转换成十六进制字符串等等。把这些剥离出来能让核心算法逻辑更加清晰。3. 古典密码实现理解加密的起点3.1 凯撒密码位移的艺术凯撒密码的原理非常简单将明文中的每个字母在字母表上向后或向前移动一个固定数目密钥得到密文。例如密钥为3时A - D, B - E以此类推。实现起来也很直观。关键在于处理好大小写字母和字母表的边界循环。// classical/caesar.h class CaesarCipher : public CryptoAlgorithm { private: int shift_key; // 移位密钥 public: CaesarCipher(int key) : shift_key(key % 26) {} // 密钥对26取模 std::string encrypt(const std::string plaintext) override; std::string decrypt(const std::string ciphertext) override; }; // classical/caesar.cpp std::string CaesarCipher::encrypt(const std::string plaintext) { std::string ciphertext; ciphertext.reserve(plaintext.length()); // 预分配空间提升效率 for (char c : plaintext) { if (isalpha(c)) { // 只处理字母字符 char base isupper(c) ? A : a; // 核心加密公式 (原字符 - 基准 密钥) % 26 基准 char encrypted_char (c - base shift_key) % 26 base; ciphertext.push_back(encrypted_char); } else { ciphertext.push_back(c); // 非字母字符原样保留 } } return ciphertext; } std::string CaesarCipher::decrypt(const std::string ciphertext) { // 解密就是反向位移 int reverse_shift (26 - shift_key) % 26; CaesarCipher decryptor(reverse_shift); return decryptor.encrypt(ciphertext); // 巧用加密函数实现解密 }注意这里在构造函数中对密钥key % 26是为了确保移位值在0-25之间避免无效计算。在decrypt方法中我们巧妙地用“反向密钥”构造一个新的加密器来实现解密避免了重复代码。3.2 栅栏密码排列的把戏栅栏密码的原理是按“之”字形排列明文然后按行读取形成密文。例如明文“HELLO WORLD”栏数为3时排列如下H . . . O . . . R . . . E . L . W . O . L . . . L . . . O . . . D按行读取HOR ELWOL LOD合并后密文为HORELWOLLOD。实现的重点在于模拟这个“之”字形填充和读取的过程。std::string RailFenceCipher::encrypt(const std::string plaintext) { if (rails 1 || plaintext.empty()) return plaintext; // 创建 rail 行字符串模拟栅栏 std::vectorstd::string fence(rails); int current_rail 0; bool going_down true; // 方向标志控制“之”字形移动 for (char c : plaintext) { fence[current_rail] c; // 更新当前行索引 if (going_down) { current_rail; if (current_rail rails - 1) going_down false; } else { current_rail--; if (current_rail 0) going_down true; } } // 合并所有行的字符串 std::string ciphertext; for (const auto rail_str : fence) { ciphertext rail_str; } return ciphertext; }解密过程稍复杂需要先计算出每一行的长度然后按照加密时的路径反向填充字符。这里就不展开代码了核心是逆向模拟填充过程。实操心得实现古典密码时要特别注意输入数据的边界和有效性检查。例如凯撒密码只应对字母进行处理数字和标点应原样保留否则会破坏数据。栅栏密码的栏数rails必须大于1且小于明文长度否则加密无意义或直接返回原文。这些检查虽然简单却是写出健壮代码的基础。4. 对称加密之王AES-128实现详解4.1 AES算法核心轮函数拆解AES-128处理128位16字节的数据块密钥也是128位。其加密过程主要包含四个步骤重复执行10轮最后一轮略有不同字节替换SubBytes 通过一个预定义的S盒Substitution-box进行非线性字节替换。这是AES混淆性的主要来源。S盒是一个256字节的查找表实现时直接查表即可。unsigned char s_box[256] {0x63, 0x7c, ... }; // 标准的AES S盒 void SubBytes(unsigned char state[4][4]) { for (int i 0; i 4; i) { for (int j 0; j 4; j) { state[i][j] s_box[state[i][j]]; } } }行移位ShiftRows 将状态矩阵的每一行循环左移。第0行不移第1行左移1字节第2行左移2字节第3行左移3字节。这一步增加了扩散性。列混淆MixColumns 将状态矩阵的每一列视为在有限域GF(2^8)上的多项式与一个固定多项式进行模乘。这是算法中最复杂的部分涉及有限域运算。为了效率通常也使用查表法预计算好的表来实现。轮密钥加AddRoundKey 将当前状态与一轮密钥由初始密钥通过密钥扩展算法生成进行简单的按位异或XOR操作。解密过程就是加密的逆过程步骤相反且使用逆S盒和逆列混淆变换。4.2 密钥扩展与数据填充密钥扩展我们需要从最初的128位密钥生成11个128位的轮密钥第0轮用于初始轮密钥加后面10轮每轮一个。扩展算法使用Rcon轮常数数组和S盒通过递归定义的方式生成。这部分代码逻辑固定但繁琐需要仔细实现。数据填充AES是分组密码只能处理固定16字节的数据块。如果明文不是16字节的整数倍就需要填充。我采用了最常用的PKCS#7填充方式如果需要填充N个字节则每个填充字节的值都是N。例如如果最后一块差3字节就填充0x03 0x03 0x03。解密后读取最后一个字节的值即可知道需要移除多少填充字节。// utils/padding.cpp std::string PKCS7Padding(const std::string data, size_t block_size) { size_t padding_len block_size - (data.length() % block_size); if (padding_len 0) padding_len block_size; // 如果刚好对齐填充一整块 char pad_char static_castchar(padding_len); return data std::string(padding_len, pad_char); } std::string PKCS7Unpadding(const std::string data) { if (data.empty()) return data; unsigned char pad_len static_castunsigned char(data.back()); // 简单的有效性检查 if (pad_len 0 || pad_len data.size()) { throw std::runtime_error(Invalid PKCS#7 padding.); } // 检查填充字节是否都正确 for (size_t i data.size() - pad_len; i data.size(); i) { if (static_castunsigned char(data[i]) ! pad_len) { throw std::runtime_error(Invalid PKCS#7 padding bytes.); } } return data.substr(0, data.size() - pad_len); }4.3 C实现要点与优化在C中实现AES为了可读性我最初使用unsigned char state[4][4]的二维数组来表示状态矩阵。但在性能关键的部分可以考虑使用一维数组并利用指针操作或者使用SIMD指令集如AES-NI进行终极优化——不过那属于另一个层次的探索了。一个完整的AES加密流程伪代码std::string AES128::encrypt(const std::string plaintext) { // 1. 填充明文 std::string padded_data PKCS7Padding(plaintext, 16); std::string ciphertext; ciphertext.reserve(padded_data.size()); // 2. 密钥扩展 KeySchedule key_schedule expandKey(this-key); // 3. 分块加密 for (size_t i 0; i padded_data.size(); i 16) { unsigned char block[16]; memcpy(block, padded_data[i], 16); unsigned char state[4][4]; // 将一维块加载到状态矩阵 for (int r 0; r 4; r) { for (int c 0; c 4; c) { state[r][c] block[r 4*c]; // AES是列优先存储 } } // 初始轮密钥加 AddRoundKey(state, key_schedule.roundKeys[0]); // 进行10轮标准轮函数前9轮 for (int round 1; round 10; round) { SubBytes(state); ShiftRows(state); MixColumns(state); AddRoundKey(state, key_schedule.roundKeys[round]); } // 最后一轮无MixColumns SubBytes(state); ShiftRows(state); AddRoundKey(state, key_schedule.roundKeys[10]); // 将状态矩阵写回块 for (int r 0; r 4; r) { for (int c 0; c 4; c) { block[r 4*c] state[r][c]; } } ciphertext.append(reinterpret_castchar*(block), 16); } return ciphertext; }注意事项自己实现的AES主要用于学习和理解绝对不要用于真正的安全产品中。工业级应用必须使用经过严格审计和测试的密码学库如OpenSSL、libsodium等。这些库经过了无数专家的审查并且可能使用了CPU的硬件加速指令如AES-NI其安全性和性能都是自己实现的版本无法比拟的。5. 非对称加密基石RSA算法的原理与实现5.1 RSA背后的数学原理RSA的安全性基于大整数分解的困难性。整个过程围绕三个核心数字n,e,d。密钥生成选择两个大质数p和q计算n p * q。n的长度就是密钥长度如2048位。计算欧拉函数φ(n) (p-1)*(q-1)。选择一个整数e满足1 e φ(n)且e与φ(n)互质。通常取65537因为它二进制表示中1很少计算效率高。计算e对于φ(n)的模反元素d即满足(e * d) % φ(n) 1。d就是私钥的一部分。公钥为(n, e)私钥为(n, d)。p和q必须彻底销毁。加密与解密加密用公钥ciphertext (plaintext ^ e) % n解密用私钥plaintext (ciphertext ^ d) % n这里的plaintext和ciphertext都需要是小于n的整数。所以实际中我们需要先将字符串明文转换成一个大整数加密后再转换回来。5.2 大整数运算与模幂计算C的标准整数类型如long long远远不足以处理RSA所需的大整数几百位甚至上千位。因此我们需要一个大整数库。在这个学习项目中我选择了一个相对简单的头文件库如bigint.h来处理大数运算。在实际工程中则会使用GMPGNU Multiple Precision Arithmetic Library这类专业库。核心难点在于高效计算(base ^ exponent) % modulus即模幂运算。直接先求幂再取模对于大数来说是不可能的中间结果会巨大无比。必须使用快速模幂算法。// 快速模幂算法 (Modular Exponentiation) BigInt modPow(const BigInt base, const BigInt exponent, const BigInt modulus) { BigInt result 1; BigInt b base % modulus; BigInt e exponent; while (e 0) { // 如果当前指数位为1则乘上当前的底数 if (e % 2 1) { result (result * b) % modulus; } // 底数平方 b (b * b) % modulus; // 指数右移一位相当于除以2 e e / 2; } return result; }这个算法将指数exponent用二进制表示将时间复杂度从 O(n) 降到了 O(log n)是RSA能够实用的关键。5.3 C中的RSA类设计由于RSA的公钥和私钥操作不对称我设计的接口与对称加密略有不同。class RSA { private: BigInt n_; // 模数 BigInt e_; // 公钥指数 BigInt d_; // 私钥指数 bool has_private_key_; // 标记是否持有私钥 public: // 构造函数1生成新密钥对 RSA(int key_size_bits 2048); // 构造函数2从已有参数加载 RSA(const BigInt n, const BigInt e, const BigInt d 0); // 获取公钥组件 std::pairBigInt, BigInt getPublicKey() const { return {n_, e_}; } // 获取私钥组件如果有 bool getPrivateKey(BigInt d) const; // 使用公钥加密 std::string encryptPublic(const std::string plaintext) const; // 使用私钥解密或签名 std::string decryptPrivate(const std::string ciphertext) const; // 使用私钥加密即签名 std::string encryptPrivate(const std::string plaintext) const; // 使用公钥解密即验证签名 std::string decryptPublic(const std::string ciphertext) const; // 辅助函数将字符串转换为适合RSA加密的大整数块 static std::vectorBigInt stringToBigIntBlocks(const std::string str, size_t block_size); static std::string bigIntBlocksToString(const std::vectorBigInt blocks); };在实际加密字符串时由于n的大小限制明文需要被分割成多个小于n的块分别加密。这涉及到分组加密模式的选择虽然RSA本身不是分组密码但处理长数据时类似。一个简单的方法是使用PKCS#1 v1.5或OAEP等填充方案它们不仅解决了分块问题还增加了算法的安全性防止确定性加密带来的问题。重要警告和AES一样这个自己实现的RSA也仅用于学习目的。真正的RSA实现极其复杂需要应对各种侧信道攻击如时序攻击需要使用安全的随机数生成器来生成质数并且密钥管理、填充方案都至关重要。生产环境务必使用成熟的密码学库。6. 哈希函数实现MD5与SHA-2566.1 MD5算法流程剖析MD5将输入数据按512位64字节分组最终产生一个128位16字节的哈希值。其核心是一个包含四轮循环的压缩函数每轮使用不同的非线性函数F, G, H, I和一组常数表。实现步骤填充对输入数据先补一个0x80字节然后补0直到长度满足(长度 % 64) 56最后8字节用于存储原始消息长度的低64位按小端序。这一步确保总长度是512位的整数倍。初始化变量设置四个32位的链接变量A, B, C, D为固定的初始值。处理分组对每个512位分组将分组划分为16个32位字。进行四轮主循环共64步每步对A, B, C, D中的三个进行非线性操作然后加上第四个、一个数据子分组、一个常数并进行循环左移和加法。每轮结束后将结果累加到链接变量A, B, C, D上。输出将所有分组处理完毕后将最终的A, B, C, D按小端序连接起来就是128位的MD5哈希值。// md5.h 中的核心压缩函数片段 void MD5::processBlock(const uint8_t block[64]) { uint32_t a state[0], b state[1], c state[2], d state[3]; uint32_t M[16]; // 将64字节块解码为16个32位字小端序 for (int i 0; i 16; i) { M[i] (block[i*4]) | (block[i*41] 8) | (block[i*42] 16) | (block[i*43] 24); } // 第一轮循环16步 FF(a, b, c, d, M[0], 7, 0xd76aa478); FF(d, a, b, c, M[1], 12, 0xe8c7b756); // ... 省略后续63步 // 其中 FF, GG, HH, II 是四个非线性函数宏定义 // 更新状态 state[0] a; state[1] b; state[2] c; state[3] d; }6.2 SHA-256更安全的哈希SHA-256属于SHA-2家族输出256位32字节哈希值。它比MD5更复杂、更安全。其流程与MD5类似但分组大小为512位使用不同的初始化常量、更多的运算步骤64步以及更复杂的逻辑函数Ch, Maj, Σ0, Σ1等。SHA-256的压缩函数核心是维护一个8个32位字a, b, c, d, e, f, g, h的状态在64步中不断更新。每一步使用当前数据分组的一个扩展字W[t]和一个固定常数K[t]。// sha256.cpp 中的核心步骤 void SHA256::transform(const uint8_t data[64]) { uint32_t a, b, c, d, e, f, g, h, t1, t2; uint32_t w[64]; // 将前16个字从数据块中拷贝出来大端序 for (int i 0; i 16; i) { w[i] (data[i*4] 24) | (data[i*41] 16) | (data[i*42] 8) | data[i*43]; } // 扩展剩余的48个字 for (int i 16; i 64; i) { uint32_t s0 rightRotate(w[i-15], 7) ^ rightRotate(w[i-15], 18) ^ (w[i-15] 3); uint32_t s1 rightRotate(w[i-2], 17) ^ rightRotate(w[i-2], 19) ^ (w[i-2] 10); w[i] w[i-16] s0 w[i-7] s1; } a state[0]; b state[1]; c state[2]; d state[3]; e state[4]; f state[5]; g state[6]; h state[7]; // 主循环64步 for (int i 0; i 64; i) { uint32_t S1 rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25); uint32_t ch (e f) ^ ((~e) g); uint32_t temp1 h S1 ch k[i] w[i]; // k[i]是常量表 uint32_t S0 rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22); uint32_t maj (a b) ^ (a c) ^ (b c); uint32_t temp2 S0 maj; h g; g f; f e; e d temp1; d c; c b; b a; a temp1 temp2; } // 更新最终状态 state[0] a; state[1] b; // ... 省略其余 }6.3 哈希函数的应用与注意事项实现完这两个哈希函数我们可以很方便地计算字符串或文件的“指纹”。MD5 md5; md5.update(Hello, World!); std::string hash_result md5.finalize(); // 返回16字节二进制数据 std::string hex_digest toHexString(hash_result); // 转换为常见的32位十六进制字符串 // 输出65a8e27d8879283831b664bd8b7f0ad4实操心得与警告MD5已不安全MD5算法早已被证明存在碰撞漏洞即可以人为制造出两个不同数据拥有相同的MD5值。因此绝对不要在任何需要防篡改或唯一标识的安全场景中使用MD5例如数字签名或密码存储。它现在仅适用于一些非关键的校验场景比如检查文件下载是否完整但SHA-256更好。密码存储永远不要直接存储用户密码的哈希值无论是MD5还是SHA-256。必须使用加盐Salt和慢哈希函数如PBKDF2、bcrypt、scrypt或Argon2。md5(password)或sha256(password)的存储方式在数据库泄露时极其脆弱。性能自己实现的哈希函数通常比优化过的库如OpenSSL慢。在需要高性能哈希的场景应使用库函数。7. 综合应用与项目集成7.1 构建一个简单的文件加密工具有了这些基础组件我们就可以将它们组合起来做一个有实际功能的小工具。例如一个命令行文件加密工具它可以用AES加密文件并用RSA来保护AES密钥。设计思路如下程序运行时随机生成一个128位的AES会话密钥。使用强大的随机数生成器如/dev/urandom或C11 random库生成一个初始化向量IV用于AES的CBC模式。使用接收方的RSA公钥加密这个“AES密钥 IV”的组合包。将这个加密后的包写入输出文件的开头。使用生成的AES密钥和IV以CBC模式加密文件的实际内容并将密文追加到输出文件中。解密时流程相反读取文件开头用接收方的RSA私钥解密出AES密钥和IV。使用解密出的AES密钥和IV以CBC模式解密文件的剩余部分。// 简化的加密流程伪代码 void hybridEncryptFile(const std::string inputFile, const std::string outputFile, const RSA recipientPublicKey) { // 1. 生成随机AES密钥和IV AES128::Key randomAesKey generateRandomBytes(16); AES128::IV iv generateRandomBytes(16); // 2. 用RSA公钥加密 (AES密钥 IV) std::string keyMaterial randomAesKey.toString() iv.toString(); std::string encryptedKeyMaterial recipientPublicKey.encryptPublic(keyMaterial); // 3. 写入加密后的密钥材料到输出文件 std::ofstream out(outputFile, std::ios::binary); writeLengthPrefixedString(out, encryptedKeyMaterial); // 4. 用AES-CBC加密文件内容并追加 AES128 aes(randomAesKey); std::ifstream in(inputFile, std::ios::binary); // ... 实现CBC模式的分块加密和写入 ... }这种“混合加密”模式结合了对称加密的高效和非对称加密的便利密钥管理是SSL/TLS、PGP等现代安全协议的基础。7.2 封装成库与API设计为了让这个工具箱更容易被其他C项目使用我们可以将其封装成一个静态库或动态库并提供清晰的API。头文件cryptokit.h可以这样设计#pragma once #include string #include memory namespace CryptoKit { // 算法枚举 enum class CipherAlgorithm { AES_128_CBC, AES_256_GCM /* 未来扩展 */ }; enum class HashAlgorithm { MD5, SHA256 }; // 对称加密接口 class SymmetricCipher { public: static std::unique_ptrSymmetricCipher create(CipherAlgorithm algo); virtual void setKey(const std::vectoruint8_t key) 0; virtual void setIV(const std::vectoruint8_t iv) 0; // 对于需要IV的模式 virtual std::vectoruint8_t encrypt(const std::vectoruint8_t plaintext) 0; virtual std::vectoruint8_t decrypt(const std::vectoruint8_t ciphertext) 0; virtual ~SymmetricCipher() default; }; // 哈希接口 class HashFunction { public: static std::unique_ptrHashFunction create(HashAlgorithm algo); virtual void update(const std::vectoruint8_t data) 0; virtual std::vectoruint8_t finalize() 0; virtual ~HashFunction() default; }; // 工具函数 std::vectoruint8_t generateRandomBytes(size_t count); std::string bytesToHex(const std::vectoruint8_t bytes); std::vectoruint8_t hexToBytes(const std::string hex); // 高级混合加密/解密函数使用预设的RSA密钥 bool encryptFileHybrid(const std::string inputPath, const std::string outputPath, const std::string rsaPublicKeyPem); bool decryptFileHybrid(const std::string inputPath, const std::string outputPath, const std::string rsaPrivateKeyPem); }通过工厂模式创建算法对象并使用智能指针管理资源可以提供安全易用的接口。将底层复杂的古典密码、AES、RSA等实现隐藏在内部。8. 常见问题、调试技巧与安全考量8.1 实现过程中遇到的典型问题在实现这个项目的过程中我踩过不少坑这里记录几个最有代表性的字节序问题 这是最隐蔽的bug来源之一。网络传输、文件存储、不同CPU架构大端序/小端序对多字节数据如int, uint32_t的解释方式不同。MD5/SHA-256的规范中明确规定了消息填充和内部运算使用的字节序MD5是小端序SHA-256是大端序。如果搞反了计算出的哈希值会和标准工具如md5sum,openssl sha256的结果完全不同。务必仔细阅读算法标准文档RFC并在处理数据块和输出最终结果时严格遵守规定的字节序。填充错误 在AES和RSA中填充是必须的但也是容易出错的地方。解密时填充验证失败是最常见的错误。现象AES解密后调用PKCS7Unpadding抛出异常。排查首先检查加密和解密使用的密钥和IV是否完全一致。然后可以编写一个测试函数打印出解密后、去除填充前的最后几个字节看其值是否符合PKCS#7规则。有时是因为加密前的数据已经包含了某些特殊字符干扰了填充判断。大整数运算溢出和性能 自己实现的简单大整数类在RSA加密稍长的数据时可能极慢甚至因为内存分配失败而崩溃。对策学习项目中使用一个经过基本优化的BigInt类即可。如果要做性能测试或处理更长的密钥必须换用GMP等专业库。同时注意RSA加密的数据长度受限于密钥大小例如2048位密钥最多只能加密(key_size_in_bits / 8) - 11字节的原始数据因为需要填充。随机数质量 密码学安全极度依赖随机数。使用rand()或std::default_random_engine生成的随机数对于密钥来说是灾难性的因为它们可预测。正确做法在Linux/macOS上可以读取/dev/urandom。在C11及以上使用std::random_device作为随机源注意检查其熵然后结合梅森旋转算法等生成器。对于生产环境必须使用操作系统提供的密码学安全随机数生成器CSPRNG。8.2 安全编程要点即使作为学习项目养成安全的编程习惯也至关重要清零敏感内存 加密密钥、私钥等敏感数据在使用后应立即从内存中清除防止通过内存转储泄露。void secureZeroMemory(void* ptr, size_t size) { volatile unsigned char* p static_castvolatile unsigned char*(ptr); while (size--) *p 0; } // 使用后 secureZeroMemory(aesKey[0], aesKey.size());避免时间侧信道攻击 比较密码或密钥时使用恒定时间的比较函数避免因比较提前退出而泄露信息。bool constantTimeCompare(const std::string a, const std::string b) { if (a.size() ! b.size()) return false; unsigned char result 0; for (size_t i 0; i a.size(); i) { result | a[i] ^ b[i]; } return result 0; }谨慎处理错误信息 不要将详细的加密错误信息如“密钥长度错误”、“填充错误”直接返回给最终用户这可能会帮助攻击者。记录到日志给用户一个通用的失败提示即可。8.3 进阶学习与优化方向如果你对这个项目感兴趣并想继续深入这里有几个方向支持更多算法和模式 实现AES-192、AES-256以及GCM、CCM等认证加密模式。实现ECC椭圆曲线密码学作为更现代的非对称加密选择。性能优化对于AES查找表可以进一步优化使用预计算的T表。终极优化是使用CPU的AES-NI指令集需要内联汇编或编译器 intrinsics。对于大数运算集成GMP库。使用多线程并行处理大文件的加密/解密注意CBC模式块间有依赖不能直接并行但CTR或ECB模式可以。提供更友好的接口 支持从PEM文件读取RSA密钥支持流式加密/解密处理大文件时无需全部读入内存提供CMake构建脚本等。编写完备的测试 使用已知答案测试KAT向量确保你的实现与标准完全一致。例如NIST和RFC文档都提供了标准的测试向量。回过头看从头实现这些密码学算法是一个极具挑战但也收获满满的过程。它强迫你去理解每一个比特是如何变化的而不仅仅是调用一个黑盒API。这种理解对于调试复杂的加密问题、评估系统安全性至关重要。当然我也再次强调在真正的产品中请信任并使用那些久经沙场、经过严格审计的密码学库比如OpenSSL、libsodium、BoringSSL等。把轮子造一遍是为了理解车为什么会跑而不是为了上路。希望这个项目能成为你探索密码学和C工程实践的一个扎实起点。