Windows下C++ OpenSSL RSA加密实战:从密钥生成到分块与避坑

发布时间:2026/10/26 13:52:54
Windows下C++ OpenSSL RSA加密实战:从密钥生成到分块与避坑 简介这份资源面向需要在 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告诉链接器去哪找.liblibcrypto是加密算法库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 KEYpublic.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 字节配 OAEPSHA-256则更少约 190 字节。所以业务数据稍长就必须分块。先看单块加密的核心代码#include openssl/evp.h #include openssl/pem.h #include vector #include cstdio // 用公钥加密一段数据返回密文 std::vectorunsigned 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::vectorunsigned 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::vectorunsigned 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::vectorunsigned 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::vectorunsigned 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::vectorunsigned 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 上少走几步弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询