简介:这份资源面向需要在 Windows 平台使用 C++ 完成 RSA 加解密开发的初中级程序员,针对网上多数示例为 Linux 版、代码老旧且内容残缺导致难以调试的问题,提供一套可直接编译运行的完整工程。压缩包共 12 个文件,约 450KB,包含 cpp 主程序源码、h 头文件、cbp 工程配置、exe 可执行文件以及 o 目标文件等,覆盖从工程配置到编译产物的完整链路,基于 Code::Blocks 组织,导入后即可对照调试。资源已有 2929 人学习下载,说明其在同类 Windows 版 OpenSSL 实践中具有一定参考价值。读者可从中获得可运行的 RSA 加解密调用示例、Base64 编解码辅助实现以及大整数运算相关代码,便于理解密钥处理、加解密流程与工程依赖配置,并以此为起点排查自身环境中的链接与编译问题,减少在旧版示例上反复试错的成本。
1. 为什么我劝你先跑通 RSA 再加业务:一个 Windows 下的 OpenSSL 落地场景
上周有个做桌面端的朋友找我,说他那套 C++ 客户端要把本地配置里的敏感字段加密后再落盘,本来想自己撸一套大数运算,结果模幂一写就错,填充一改就崩。我直接让他别造轮子,Windows 上装个 OpenSSL,用 EVP 接口把 RSA 跑通,半天就能收工。这份资源讲的就是这件事:在 Windows 环境下用 C++ 调 OpenSSL 实现 RSA 加密解密,覆盖密钥生成、公钥加密私钥解密、分块处理、Base64 落盘这几步。适合两类人——一是要在本地做小数据加密的 C++ 桌面开发者,二是刚接触 OpenSSL、被RSA_public_encrypt那套老接口绕晕的人。下面按我实际拆包的顺序讲,能抄的代码我都贴出来。
2. 环境与密钥:Windows 下把 OpenSSL 装对、把密钥生成对
2.1 选 OpenSSL 而不是自己写大数运算的理由
RSA 的核心是大素数生成、模幂运算和填充方案,这三块任何一块写错都不是「结果不对」而是「安全归零」。自己实现大数运算,光是 Montgomery 模幂的进位处理就能让新手调一周,更别说 PKCS#1 v1.5 或 OAEP 填充里的随机数质量。OpenSSL 把这些都封装好了,而且 Windows 上有预编译包,链接进去就能用。常见做法是直接用官方或社区维护的 Windows 二进制发行版,拿到include和lib两个目录,配到工程里即可。
选型上还有一个关键点:接口用 EVP 还是底层 RSA 函数。老教程里大量出现RSA_public_encrypt/RSA_private_decrypt,这套接口在 OpenSSL 3.x 里已经被标记为废弃,编译会报警告,未来版本可能移除。我一般直接用 EVP 系列(EVP_PKEY_encrypt/EVP_PKEY_decrypt),一套接口同时兼容 RSA 和后续可能换的算法,迁移成本低。这份资源如果用的是老接口,跑通没问题,但新项目建议直接上 EVP。
2.2 Windows 下配置 OpenSSL 工程
假设你把 OpenSSL 解压到了C:\dev\openssl,目录下有include\openssl\*.h和lib\libcrypto.lib、lib\libssl.lib。用 CMake 的话,CMakeLists.txt里这样写:
cmake_minimum_required(VERSION 3.15) project(rsa_demo CXX) set(CMAKE_CXX_STANDARD 17) # 指向 OpenSSL 根目录,按你实际解压路径改 set(OPENSSL_ROOT "C:/dev/openssl") add_executable(rsa_demo main.cpp) target_include_directories(rsa_demo PRIVATE ${OPENSSL_ROOT}/include) target_link_directories(rsa_demo PRIVATE ${OPENSSL_ROOT}/lib) # Windows 下链接 libcrypto 即可,RSA 相关都在里面 target_link_libraries(rsa_demo PRIVATE libcrypto)逻辑说明:target_include_directories让编译器找到openssl/evp.h这些头文件;target_link_directories告诉链接器去哪找.lib;libcrypto是加密算法库,RSA、AES、摘要都在里面,libssl是 TLS 协议层,这里用不上。参数上唯一要改的就是OPENSSL_ROOT,换成你自己的路径。
提示:如果你用的是 MSVC 且 OpenSSL 是 64 位包,工程平台必须也选 x64,否则链接阶段会报一堆
LNK2019找不到符号,这个坑我见过太多次。
2.3 生成一对 RSA 密钥并落盘
密钥生成用openssl genpkey命令行最省事,也可以代码里生成。命令行方式:
# 生成 2048 位私钥,PEM 格式 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem # 从私钥导出公钥 openssl rsa -in private.pem -pubout -out public.pemrsa_keygen_bits:2048是密钥长度,2048 位是当前桌面场景的底线,1024 位已经不安全,4096 位会让加密解密慢一个数量级,本地配置加密用 2048 足够。-pubout表示导出公钥而不是私钥。生成后private.pem里是BEGIN PRIVATE KEY,public.pem里是BEGIN PUBLIC KEY,这两个文件就是后续加解密的输入。
代码里读密钥用PEM_read_PrivateKey和PEM_read_PUBKEY,配合BIO_new_file打开文件。这里有个细节:读私钥时如果 PEM 带密码,需要传回调函数,不带密码就传nullptr。读进来的是EVP_PKEY*,用完必须EVP_PKEY_free,否则内存泄漏在长时间运行的客户端里会慢慢累积。
3. 加密与解密:EVP 接口的完整调用链和分块处理
3.1 公钥加密的完整代码
RSA 加密有个硬限制:单次能加密的明文长度不能超过密钥长度减去填充开销。2048 位密钥配 PKCS#1 v1.5 填充,单次最多加密 245 字节;配 OAEP(SHA-256)则更少,约 190 字节。所以业务数据稍长就必须分块。先看单块加密的核心代码:
#include <openssl/evp.h> #include <openssl/pem.h> #include <vector> #include <cstdio> // 用公钥加密一段数据,返回密文 std::vector<unsigned char> rsaEncrypt(EVP_PKEY* pubKey, const unsigned char* plain, size_t plainLen) { EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pubKey, nullptr); if (!ctx) return {}; std::vector<unsigned char> cipher; size_t outLen = 0; // 初始化加密上下文 if (EVP_PKEY_encrypt_init(ctx) <= 0) { EVP_PKEY_CTX_free(ctx); return {}; } // 使用 OAEP 填充,摘要用 SHA-256 if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) { EVP_PKEY_CTX_free(ctx); return {}; } if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0) { EVP_PKEY_CTX_free(ctx); return {}; } // 第一次调用传 nullptr 拿密文长度 if (EVP_PKEY_encrypt(ctx, nullptr, &outLen, plain, plainLen) <= 0) { EVP_PKEY_CTX_free(ctx); return {}; } cipher.resize(outLen); if (EVP_PKEY_encrypt(ctx, cipher.data(), &outLen, plain, plainLen) <= 0) { EVP_PKEY_CTX_free(ctx); return {}; } cipher.resize(outLen); EVP_PKEY_CTX_free(ctx); return cipher; }逻辑说明:EVP_PKEY_CTX_new创建一个绑定公钥的上下文;EVP_PKEY_encrypt_init初始化;EVP_PKEY_CTX_set_rsa_padding设填充方式,这里选 OAEP 而不是 PKCS#1 v1.5,因为后者存在已知的填充预言攻击面,新代码没理由再用;EVP_PKEY_CTX_set_rsa_oaep_md指定 OAEP 内部摘要,SHA-256 是当前默认选择。两次EVP_PKEY_encrypt调用是 OpenSSL 的惯用法:第一次拿输出长度,第二次真正写数据。参数上plainLen必须小于单块上限,否则第二次调用返回失败,错误码可以用ERR_get_error取。
3.2 私钥解密与错误处理
解密是加密的镜像,填充参数必须和加密端完全一致,否则解密直接失败:
std::vector<unsigned char> rsaDecrypt(EVP_PKEY* privKey, const unsigned char* cipher, size_t cipherLen) { EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(privKey, nullptr); if (!ctx) return {}; std::vector<unsigned char> plain; size_t outLen = 0; if (EVP_PKEY_decrypt_init(ctx) <= 0) { EVP_PKEY_CTX_free(ctx); return {}; } // 填充方式必须与加密端一致 if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) { EVP_PKEY_CTX_free(ctx); return {}; } if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0) { EVP_PKEY_CTX_free(ctx); return {}; } if (EVP_PKEY_decrypt(ctx, nullptr, &outLen, cipher, cipherLen) <= 0) { EVP_PKEY_CTX_free(ctx); return {}; } plain.resize(outLen); if (EVP_PKEY_decrypt(ctx, plain.data(), &outLen, cipher, cipherLen) <= 0) { EVP_PKEY_CTX_free(ctx); return {}; } plain.resize(outLen); EVP_PKEY_CTX_free(ctx); return plain; }这里最容易翻车的是填充参数不一致:加密用 OAEP、解密用 PKCS#1 v1.5,解密会返回失败但不告诉你原因,只能靠ERR_print_errors_fp(stderr)把错误栈打出来看。另一个细节是cipherLen必须等于密钥长度(2048 位就是 256 字节),传短了直接失败。返回值用std::vector而不是裸指针,是为了让调用方不用管释放,但空 vector 表示失败,调用方必须判空,不能直接当明文用。
3.3 长数据分块与 Base64 落盘
超过单块上限的数据要切块,每块单独加密,密文块拼接后再整体 Base64 编码存文件。分块大小按填充方式算:OAEP + SHA-256 下,2048 位密钥单块明文上限是256 - 2*32 - 2 = 190字节。代码里我一般写个循环:
// 按 190 字节切块加密,密文块拼接 const size_t CHUNK = 190; std::vector<unsigned char> allCipher; for (size_t off = 0; off < plainLen; off += CHUNK) { size_t n = std::min(CHUNK, plainLen - off); auto block = rsaEncrypt(pubKey, plain + off, n); if (block.empty()) return {}; // 任一块失败整体失败 allCipher.insert(allCipher.end(), block.begin(), block.end()); }解密端按密钥长度(256 字节)切密文块,逐块解密再拼接。Base64 用 OpenSSL 的EVP_EncodeBlock/EVP_DecodeBlock,注意EVP_DecodeBlock返回的长度包含填充的=,实际有效长度要自己减掉尾部=的个数,这个坑不踩一次很难记住。落盘时密文是二进制,直接写文件容易在文本编辑器里乱码,Base64 后存文本更稳,也方便排查。
注意:RSA 只适合加密密钥或小数据。业务里的大文件、长配置,正确做法是随机生成一个 AES 密钥加密数据,再用 RSA 公钥加密这个 AES 密钥,也就是混合加密。拿 RSA 硬怼几 MB 数据,分块循环能跑但性能惨不忍睹。
4. 避坑与排查:五个让我返工过的具体问题
4.1 链接报 LNK2019 找不到符号
现象:编译通过,链接阶段一堆unresolved external symbol,全是EVP_*或RSA_*。原因基本是两个:工程平台位数和 OpenSSL 包位数不一致(x86 工程链 x64 库),或者只加了 include 路径没加 lib 路径。解决:先确认 OpenSSL 包是 32 位还是 64 位,工程平台对齐;再检查target_link_libraries里有没有libcrypto,路径有没有写对。MSVC 下还可以用dumpbin /headers libcrypto.lib看机器类型确认位数。
4.2 解密返回失败但没有任何提示
现象:EVP_PKEY_decrypt返回小于等于 0,程序里只看到失败,不知道哪错了。原因:OpenSSL 的错误信息在错误栈里,不主动取就看不到。解决:在失败分支加ERR_print_errors_fp(stderr);,把错误栈打到控制台,常见输出会直接告诉你「padding check failed」或「data too large for key size」,一眼定位是填充不一致还是数据超长。
4.3 中文或二进制数据加密后解密乱码
现象:英文正常,中文解密出来是乱码或截断。原因:把std::string的c_str()当明文传,遇到\0就截断,或者长度用了strlen而不是size()。解决:全程用const unsigned char*加显式长度,长度一律取size(),不要碰strlen。加密前把std::string转成std::vector<unsigned char>,解密后再按字节还原,中间不经过任何以\0结尾的假设。
4.4 每次加密结果都不一样,误以为出错
现象:同样的明文、同样的公钥,两次加密出来的密文完全不同,有人以为加密坏了。原因:OAEP 和 PKCS#1 v1.5 填充都带随机数,这是设计使然,正是它让相同明文产生不同密文,抵御重放和比对攻击。解决:不用解决,这是正确行为。验证时应该加密后立刻解密比对明文,而不是比对两次密文是否相同。
4.5 私钥文件被误提交进版本库
现象:代码仓库里出现了private.pem,安全审计直接亮红灯。原因:生成密钥时放在工程目录,提交时没加忽略规则。解决:密钥文件放工程目录外,.gitignore里加*.pem,私钥在生产环境用权限控制或密钥管理服务托管。本地开发用的测试密钥也要和线上密钥严格分开,别拿测试私钥去解线上数据。
5. 进阶:用混合加密把 RSA 用在对的地方
RSA 跑通之后,真正该做的是把它放进混合加密的骨架里,而不是拿它直接怼业务数据。我的习惯是:每次加密会话随机生成 32 字节 AES-256 密钥,用 AES-GCM 加密实际数据(GCM 自带完整性校验,比 CBC 少一个填充坑),再用 RSA-OAEP 公钥加密这个 AES 密钥,最后把「RSA 密文长度 + RSA 密文 + GCM 的 nonce + 密文 + tag」按固定格式拼成一个二进制包,整体 Base64 落盘。解密端反过来:先解出 AES 密钥,再用它解数据。这样单次 RSA 只加密 32 字节,永远不碰分块上限,性能和安全都到位。
验证环节我一般写一个自测函数,覆盖三种情况:短明文(小于单块上限)、刚好等于上限、超过上限触发分块,每种都做「加密→解密→逐字节比对」,再故意改一个密文字节确认解密失败。这个自测跑通,基本能排除填充参数不一致、长度算错、分块边界这三类高频问题。参数上唯一要盯的是 OAEP 摘要和 GCM tag 长度,两端必须写死成同一组常量,别一边 SHA-256 一边 SHA-1。
有个血泪教训:早期我在一个项目里把填充方式写成了可配置项,结果测试环境和生产环境配置不一致,上线后解密全挂,排查了半天才发现是配置文件里一个字符串的差别。从那以后我每次封加密模块,都把填充方式和摘要算法写成编译期常量,配置里只留密钥路径,不给运行时改填充的机会。希望这些能帮你在 Windows 上少走几步弯路。
本文还有配套的精品资源,点击获取