geopro/tests/net/test_rsa.cpp

125 lines
4.5 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// net 层 RSA 加密器测试OpenSSL 3.x EVP API
// 用临时生成的 RSA-2048 密钥对自测,不依赖实站公钥:
// 1) 用 EVP_RSA_gen(2048) 生成密钥对;
// 2) 导出公钥 PEM喂给 RsaEncryptor 加密 + base64
// 3) 用同一密钥对的私钥解密密文,断言能还原原文。
// PKCS#1 v1.5 填充与 JSEncrypt 默认一致。
#include <gtest/gtest.h>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include "crypto/RsaEncryptor.hpp"
namespace {
// RAII 包装:保证测试里 OpenSSL 句柄不泄漏。
struct PkeyDeleter {
void operator()(EVP_PKEY* p) const noexcept {
if (p) EVP_PKEY_free(p);
}
};
struct PkeyCtxDeleter {
void operator()(EVP_PKEY_CTX* c) const noexcept {
if (c) EVP_PKEY_CTX_free(c);
}
};
struct BioDeleter {
void operator()(BIO* b) const noexcept {
if (b) BIO_free(b);
}
};
using PkeyPtr = std::unique_ptr<EVP_PKEY, PkeyDeleter>;
using PkeyCtxPtr = std::unique_ptr<EVP_PKEY_CTX, PkeyCtxDeleter>;
using BioPtr = std::unique_ptr<BIO, BioDeleter>;
// base64 解码(处理尾部 padding用于长度断言。
std::vector<unsigned char> base64Decode(const std::string& b64) {
std::vector<unsigned char> out(b64.size()); // 解码后必不超过输入长度
int decoded = EVP_DecodeBlock(out.data(),
reinterpret_cast<const unsigned char*>(b64.data()),
static_cast<int>(b64.size()));
if (decoded < 0) return {};
// EVP_DecodeBlock 按 4 字节对齐解码,会把 '=' 当作 0需按 padding 数扣除尾部字节。
size_t pad = 0;
if (!b64.empty() && b64[b64.size() - 1] == '=') ++pad;
if (b64.size() >= 2 && b64[b64.size() - 2] == '=') ++pad;
out.resize(static_cast<size_t>(decoded) - pad);
return out;
}
// 用私钥kp以 PKCS#1 v1.5 解密 cipher。
std::string decryptWithPrivateKey(EVP_PKEY* kp, const std::vector<unsigned char>& cipher) {
PkeyCtxPtr ctx(EVP_PKEY_CTX_new(kp, nullptr));
EXPECT_NE(ctx, nullptr);
EXPECT_GT(EVP_PKEY_decrypt_init(ctx.get()), 0);
EXPECT_GT(EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_PADDING), 0);
size_t outlen = 0;
EXPECT_GT(EVP_PKEY_decrypt(ctx.get(), nullptr, &outlen, cipher.data(), cipher.size()), 0);
std::vector<unsigned char> plain(outlen);
EXPECT_GT(EVP_PKEY_decrypt(ctx.get(), plain.data(), &outlen, cipher.data(), cipher.size()), 0);
return std::string(reinterpret_cast<const char*>(plain.data()), outlen);
}
std::string exportPublicKeyPem(EVP_PKEY* kp) {
BioPtr bio(BIO_new(BIO_s_mem()));
EXPECT_NE(bio, nullptr);
EXPECT_GT(PEM_write_bio_PUBKEY(bio.get(), kp), 0);
char* data = nullptr;
long len = BIO_get_mem_data(bio.get(), &data);
return std::string(data, static_cast<size_t>(len));
}
} // namespace
TEST(RsaEncryptorTest, EncryptsToBase64DecryptableByPrivateKey) {
// Arrange: 临时生成 RSA-2048 密钥对,导出公钥 PEM。
PkeyPtr kp(EVP_RSA_gen(2048));
ASSERT_NE(kp, nullptr);
const std::string pubPem = exportPublicKeyPem(kp.get());
ASSERT_FALSE(pubPem.empty());
const std::string plaintext = "hello-geopro";
// Act: 加密 + base64。
geopro::net::RsaEncryptor enc(pubPem);
const std::string b64 = enc.encryptBase64(plaintext);
// Assert: base64 解码后是 256 字节密文RSA-2048
const std::vector<unsigned char> cipher = base64Decode(b64);
EXPECT_EQ(cipher.size(), 256u);
// Assert: 私钥能解出原文(即便长度断言放宽,这条是核心正确性保证)。
const std::string recovered = decryptWithPrivateKey(kp.get(), cipher);
EXPECT_EQ(recovered, plaintext);
}
TEST(RsaEncryptorTest, ThrowsOnInvalidPublicKey) {
EXPECT_THROW(geopro::net::RsaEncryptor("not-a-valid-pem"), std::runtime_error);
}
TEST(RsaEncryptorTest, MoveConstructionPreservesEncryption) {
PkeyPtr kp(EVP_RSA_gen(2048));
ASSERT_NE(kp, nullptr);
const std::string pubPem = exportPublicKeyPem(kp.get());
geopro::net::RsaEncryptor original(pubPem);
geopro::net::RsaEncryptor moved(std::move(original));
const std::string plaintext = "move-check";
const std::string b64 = moved.encryptBase64(plaintext);
const std::vector<unsigned char> cipher = base64Decode(b64);
EXPECT_EQ(cipher.size(), 256u);
EXPECT_EQ(decryptWithPrivateKey(kp.get(), cipher), plaintext);
}