Windows下OpenSSL RSA加解密实战:从能跑到敢上线

发布时间:2026/10/26 5:31:11
Windows下OpenSSL RSA加解密实战:从能跑到敢上线 简介这份资源面向需要在 Windows 平台使用 C 完成 RSA 加解密开发的初学者与工程实践者针对网络上大量 Linux 版示例内容残缺、代码老旧、难以调试的问题提供一套可直接编译运行的完整工程。资源共 12 个文件压缩包约 450KB包含 cpp 主程序源码、h 头文件、cbp 工程配置、exe 可执行程序以及 o 目标文件等配合 Code::Blocks 打开工程即可复现加密解密流程省去环境搭建与依赖排查的麻烦。已有 2929 人学习下载说明其在同类 Windows 版 OpenSSL 实践中具备一定参考价值。读者可从中获得可运行的 RSA 加解密调用范例、Base64 编解码辅助实现以及大整数运算相关代码组织方式便于对照理解 OpenSSL 接口在 C 项目中的实际用法并在此基础上快速改造为自己的加解密模块。1. 从一段“能跑但不敢上线”的 RSA 代码说起前阵子接手一个 Windows 桌面端的配置下发工具需求很朴素把本地生成的授权文件加密后传给服务端服务端解出来校验。团队里有人甩过来一份从网上抄的openssl rsa c 加密解密示例编译能过跑起来也能出结果但一上真实数据就露馅——中文变乱码、长一点的字符串直接崩、公钥私钥格式对不上。这类“能跑但不敢上线”的代码恰恰是 OpenSSL 在 Windows 上做 RSA 最容易翻车的地方。这个项目就是围绕这套场景整理出来的一份可编译、可复现的 Windows 版 RSA 加解密工程用 C 调 OpenSSL 的 EVP 接口完成密钥生成、公钥加密、私钥解密配套 CMake 构建脚本和一份最小可跑的测试入口。适合两类人一是刚接触 OpenSSL、想在 Windows 上把 RSA 跑通的新手二是手里有现成业务代码、需要把加解密模块抽出来单独维护的熟手。下面按“它是什么 → 怎么跑起来 → 核心代码怎么读 → 怎么改”的顺序拆。2. 工程结构与构建链路Windows 下把 OpenSSL 接进 CMake2.1 目录长什么样每个文件管什么这份工程刻意做得扁平避免新手在多层目录里迷路。核心就四块源码、构建脚本、依赖说明、测试数据。目录大致如下rsa-openssl-demo/ ├── CMakeLists.txt # 构建入口负责找 OpenSSL、链库 ├── src/ │ ├── main.cpp # 测试入口串起生成密钥→加密→解密 │ ├── rsa_util.h # 加解密接口声明 │ └── rsa_util.cpp # EVP 接口的具体实现 ├── include/ # 预留放第三方头文件时用 ├── data/ │ ├── pub.pem # 运行后生成的公钥 │ └── pri.pem # 运行后生成的私钥 └── README.md # 依赖版本与编译命令rsa_util.h只暴露三个函数生成密钥对、公钥加密、私钥解密参数全部用std::string和std::vectorunsigned char不把 OpenSSL 的BIO、EVP_PKEY这些类型泄漏到业务层。这是有意的设计——业务代码不该关心底层是 RSA 还是国密换算法时只改实现文件。main.cpp则是一个最小闭环读一段明文调加密调解密打印前后对比。很多人拿到示例直接改main里的字符串就以为在测自己的逻辑其实真正的坑都在rsa_util.cpp里。2.2 CMake 怎么找到 OpenSSL链接顺序为什么不能错Windows 上最烦的一步不是写代码是让编译器找到 OpenSSL。常见做法是用find_package让 CMake 自己去探探不到再手动指定根目录。构建脚本的关键片段如下cmake_minimum_required(VERSION 3.16) project(rsa_openssl_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 优先让 CMake 自己找找不到再用 OPENSSL_ROOT_DIR 兜底 find_package(OpenSSL REQUIRED) add_executable(rsa_demo src/main.cpp src/rsa_util.cpp ) target_include_directories(rsa_demo PRIVATE ${OPENSSL_INCLUDE_DIR}) # 链接顺序先 crypto 再 sslWindows 下顺序反了会报未解析符号 target_link_libraries(rsa_demo PRIVATE OpenSSL::Crypto OpenSSL::SSL)find_package(OpenSSL REQUIRED)会去读环境变量OPENSSL_ROOT_DIR所以安装完 OpenSSL 后要么把它加进系统环境变量要么在配置时显式传-DOPENSSL_ROOT_DIRD:/libs/openssl。OpenSSL::Crypto是 CMake 提供的导入目标比手写-lcrypto稳因为它会自动带上正确的库目录和依赖。链接顺序这块是血泪经验Windows 的链接器对符号解析顺序敏感crypto依赖的一些底层符号在ssl里也有同名弱符号顺序写反时表现为一堆unresolved external symbol但错误信息指向的函数看起来跟 RSA 毫无关系很容易误判成版本不匹配。2.3 编译命令与运行验证配置和编译分两步命令不复杂但路径要对# 在工程根目录执行-B 指定构建目录避免污染源码 cmake -B build -DOPENSSL_ROOT_DIRD:/libs/openssl -A x64 # 编译 Release 版Debug 版在 Windows 上会链到不同的运行库 cmake --build build --config Release-A x64是给 Visual Studio 生成器用的指定 64 位平台。如果你的 OpenSSL 是 32 位的这里必须改成-A Win32否则链接阶段会报“模块计算机类型与目标计算机类型冲突”。编译成功后build/Release/rsa_demo.exe就是入口直接双击或在终端里跑它会依次打印生成的密钥长度、密文十六进制、解密后的明文。验证是否真的跑通看两点解密结果和原始明文逐字节相等data/下生成了pub.pem和pri.pem两个文件用文本编辑器打开能看到-----BEGIN PUBLIC KEY-----开头的内容。如果只看到乱码或者文件为空说明写文件那步的BIO没刷新后面排查章节会讲。3. 核心模块拆解EVP 接口下的密钥生成与加解密链路3.1 为什么不用 RSA_public_encrypt 而用 EVPOpenSSL 1.1.1 之后RSA_public_encrypt、RSA_private_decrypt这批低层接口被标记为 deprecated新代码继续用它们会在编译时刷一屏警告而且这些接口不参与引擎调度换硬件加速卡时得重写。EVP 系列是官方推荐的上层封装EVP_PKEY_encrypt/EVP_PKEY_decrypt把填充模式、密钥类型都收进上下文里同一套代码以后换 ECC 也只是换EVP_PKEY的生成方式。这个项目统一走 EVP代价是代码行数多一点但换来的是可维护性。下面按“生成密钥 → 加密 → 解密”三段拆。3.2 生成 RSA 密钥对并落盘成 PEM密钥生成是整条链路的地基长度选错后面全白搭。实现里用EVP_PKEY_CTX控制密钥位数默认 2048#include openssl/evp.h #include openssl/pem.h #include openssl/rsa.h // 生成 RSA 密钥对bits 常用 2048 或 3072 EVP_PKEY* generate_rsa_key(int bits) { EVP_PKEY_CTX* ctx EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr); if (!ctx) return nullptr; EVP_PKEY* pkey nullptr; // 初始化密钥生成上下文设置位数 if (EVP_PKEY_keygen_init(ctx) 0 || EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) 0 || EVP_PKEY_keygen(ctx, pkey) 0) { EVP_PKEY_CTX_free(ctx); return nullptr; } EVP_PKEY_CTX_free(ctx); return pkey; } // 把密钥写成 PEM 文件is_private 决定写公钥还是私钥 bool save_key_to_pem(EVP_PKEY* pkey, const char* path, bool is_private) { FILE* fp fopen(path, wb); if (!fp) return false; bool ok false; if (is_private) { // 私钥用 PKCS#8 格式不加密方便本地调试 ok PEM_write_PrivateKey(fp, pkey, nullptr, nullptr, 0, nullptr, nullptr) 1; } else { ok PEM_write_PUBKEY(fp, pkey) 1; } fclose(fp); return ok; }EVP_PKEY_CTX_set_rsa_keygen_bits的位数直接决定单次能加密的明文上限2048 位密钥配 PKCS#1 v1.5 填充最多加密 245 字节配 OAEP 填充则更少。这是 RSA 的硬约束不是代码 bug后面避坑章节会专门讲。PEM_write_PrivateKey的第三个参数传nullptr表示不加密私钥本地调试方便但真实项目里私钥落盘必须加密常见做法是传一个回调读密码或者干脆不落盘、只放内存。PEM_write_PUBKEY写出来的是BEGIN PUBLIC KEY格式SubjectPublicKeyInfo和PEM_write_RSAPublicKey写出的BEGIN RSA PUBLIC KEY不是一回事跨语言对接时经常在这里对不上。3.3 公钥加密填充模式与输出长度加密段的核心是EVP_PKEY_encrypt分两步调用第一次传空输出拿长度第二次真正加密。这个“两次调用”模式是 OpenSSL 的惯例漏掉第一次会拿到随机长度的缓冲区。#include vector #include string // 用公钥加密返回密文字节失败返回空 vector std::vectorunsigned char rsa_encrypt(EVP_PKEY* pub_key, const std::string plain) { std::vectorunsigned char cipher; EVP_PKEY_CTX* ctx EVP_PKEY_CTX_new(pub_key, nullptr); if (!ctx) return cipher; size_t out_len 0; // 第一次调用只算密文长度 if (EVP_PKEY_encrypt_init(ctx) 0 || EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) 0 || EVP_PKEY_encrypt(ctx, nullptr, out_len, reinterpret_castconst unsigned char*(plain.data()), plain.size()) 0) { EVP_PKEY_CTX_free(ctx); return cipher; } cipher.resize(out_len); // 第二次调用真正加密 if (EVP_PKEY_encrypt(ctx, cipher.data(), out_len, reinterpret_castconst unsigned char*(plain.data()), plain.size()) 0) { cipher.clear(); } else { cipher.resize(out_len); } EVP_PKEY_CTX_free(ctx); return cipher; }EVP_PKEY_CTX_set_rsa_padding这里选的是RSA_PKCS1_OAEP_PADDING比默认的 PKCS#1 v1.5 更安全但对接方如果用的是 v1.5两边填充模式不一致会直接解密失败且错误信息只有一句decryption failed非常难查。参数out_len在第一次调用后被填成密文长度2048 位密钥下固定是 256 字节和明文长度无关——这也是为什么 RSA 不适合加密大段数据业务上通常是“RSA 加密对称密钥对称密钥加密正文”。plain.data()用reinterpret_cast转成unsigned char*是因为 OpenSSL 的 C 接口不认char*这一步在 C 里绕不开。3.4 私钥解密与错误处理解密是加密的镜像但错误处理要更细因为解密失败的原因太多填充不对、密钥不匹配、密文被截断。实现里把EVP_PKEY_decrypt的返回值单独判断方便上层区分。// 用私钥解密失败返回空 vector std::vectorunsigned char rsa_decrypt(EVP_PKEY* pri_key, const std::vectorunsigned char cipher) { std::vectorunsigned char plain; EVP_PKEY_CTX* ctx EVP_PKEY_CTX_new(pri_key, nullptr); if (!ctx) return plain; size_t out_len 0; if (EVP_PKEY_decrypt_init(ctx) 0 || EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) 0 || EVP_PKEY_decrypt(ctx, nullptr, out_len, cipher.data(), cipher.size()) 0) { EVP_PKEY_CTX_free(ctx); return plain; } plain.resize(out_len); if (EVP_PKEY_decrypt(ctx, plain.data(), out_len, cipher.data(), cipher.size()) 0) { plain.clear(); } else { plain.resize(out_len); } EVP_PKEY_CTX_free(ctx); return plain; }注意EVP_PKEY_CTX_set_rsa_padding在解密端必须和加密端完全一致这是最容易漏的一步。另外plain.resize(out_len)之后第二次调用会把out_len改成真实明文长度所以最后要再resize一次否则尾部会带上一段未初始化的脏数据——这个细节在打印字符串时表现为末尾多几个乱码字符很多人以为是编码问题其实是缓冲区没裁干净。EVP_PKEY_CTX_free在每个返回路径上都要调漏掉就是内存泄漏用std::unique_ptr配自定义 deleter 能省掉这类手工清理改造章节会给写法。4. 避坑与排查Windows 上 RSA 跑不通的五个高频现场4.1 现象编译过、链接报一堆未解析符号原因通常是三选一OpenSSL 位数和工程平台不一致32 位库配 64 位工程、链接顺序写反、只链了ssl没链crypto。解决时先看错误里的符号名带EVP_前缀的基本都在crypto里确认target_link_libraries里OpenSSL::Crypto在前。再核对-A参数和 OpenSSL 的lib目录名libcrypto.lib在x64子目录下才是 64 位。最后检查OPENSSL_ROOT_DIR是否指到了安装根目录而不是bin目录。4.2 现象加密中文后解密出来是乱码原因多半是明文长度按字符数算而不是字节数。std::string存 UTF-8 中文时一个汉字占 3 字节plain.size()返回的是字节数这个是对的但如果中间经过std::wstring转换或者用了strlen截断就会丢字节。解决方式是全程用std::string承载 UTF-8加密前打印plain.size()确认字节数解密后按字节还原不要在中途做编码转换。另一个隐蔽原因是解密缓冲区没resize到真实长度尾部脏数据混进字符串。4.3 现象明文稍长就加密失败返回空这是 RSA 的填充上限在起作用不是代码错。2048 位密钥配 OAEP 填充单次最多加密 190 字节配 PKCS#1 v1.5 是 245 字节。超过就返回失败。解决办法是分层加密随机生成一个 AES 密钥用 AES 加密正文再用 RSA 加密这个 AES 密钥这也是 TLS 里的常见做法。如果业务上确实只有短数据那就把密钥长度提到 3072 或 4096但要注意密钥生成时间会明显变长4096 位在普通机器上生成一次可能要几秒。4.4 现象公钥文件能读但PEM_read_PUBKEY返回空原因通常是文件格式不匹配。PEM_read_PUBKEY只认BEGIN PUBLIC KEY如果文件是BEGIN RSA PUBLIC KEYPKCS#1 格式得用PEM_read_RSAPublicKey再转成EVP_PKEY。跨语言对接时尤其常见某些语言的库默认导出 PKCS#1而 OpenSSL 的 EVP 接口默认吃 SubjectPublicKeyInfo。解决时先用文本编辑器看文件头再决定用哪个读函数别硬套。4.5 现象程序跑完data/下文件是空的原因是BIO或FILE*没刷新就关了或者写文件时用了w但路径不存在导致fopen返回空却没检查。解决时在每个fopen后立刻判空写完后fclose之前确认返回值用BIO的话要调BIO_flush。另外 Windows 下路径分隔符用/也能被接受但相对路径是相对于工作目录而不是 exe 所在目录从 IDE 里跑和双击 exe 跑的工作目录可能不同建议用绝对路径或显式chdir。5. 二次开发把 demo 改成能进业务的加解密模块5.1 用 RAII 包住 OpenSSL 资源杜绝手工 freedemo 里每个函数末尾都手工EVP_PKEY_CTX_free一旦中间加个return就漏。改造第一步是用std::unique_ptr配自定义 deleter把EVP_PKEY、EVP_PKEY_CTX、BIO都管起来#include memory struct PkeyDeleter { void operator()(EVP_PKEY* p) const { EVP_PKEY_free(p); } }; struct CtxDeleter { void operator()(EVP_PKEY_CTX* c) const { EVP_PKEY_CTX_free(c); } }; using PkeyPtr std::unique_ptrEVP_PKEY, PkeyDeleter; using CtxPtr std::unique_ptrEVP_PKEY_CTX, CtxDeleter; // 用法PkeyPtr key(generate_rsa_key(2048));这样即使中途抛异常资源也会被释放。改造后rsa_encrypt的签名可以改成接收EVP_PKEY*裸指针不转移所有权内部用CtxPtr管上下文调用方用PkeyPtr管密钥职责清晰。5.2 把填充模式和密钥长度提成配置项业务对接时填充模式经常变硬编码在函数里改一次要重编译。改造思路是抽一个RsaConfig结构体配置项可选值默认说明key_bits2048 / 3072 / 40962048密钥位数影响单次加密上限paddingOAEP / PKCS1OAEP必须与对接方一致hashSHA256 / SHA1SHA256仅 OAEP 生效padding和hash通过EVP_PKEY_CTX_set_rsa_oaep_md设置OAEP 默认用 SHA1和很多现代对接方的 SHA256 不一致时解密会失败且错误信息不提示哈希不匹配。这个配置项建议从配置文件或环境变量读别写死在代码里。5.3 验证改造是否正确的三个动作改完之后别急着接业务先做三件事一是用同一对密钥加密再解密比对字节级相等覆盖空串、单字节、接近上限长度的边界值二是故意用错误的私钥解密确认返回空而不是崩溃三是把密文截断一个字节再解密确认错误处理路径能走到。这三个动作能覆盖大部分填充和长度相关的坑。我自己的习惯是每次动加解密模块都先把这三个用例跑一遍再提交从那以后因为“填充模式不一致”导致的线上问题基本没再出现过。希望这份拆解能帮到你把 Windows 上这套 OpenSSL RSA 的链路真正跑顺。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询