密码学基础:对称加密、非对称加密、哈希、MAC与数字签名
说实话,密码学这块内容,很多C++开发者都觉得头疼。我刚开始做安全编程那会儿,也踩过不少坑。今天咱们就把这几个核心概念掰开揉碎了讲清楚。
你想想看,一个安全的通信系统,至少得解决三个问题:机密性(别人看不懂)、完整性(内容没被改过)、真实性(确实是对方发的)。这五个技术,就是解决这些问题的工具箱。
核心要点:密码学不是魔法,它只是把数学问题转化成计算难题。你不需要成为数学家,但必须理解每种算法的适用场景和坑。
1. 对称加密:AES
对称加密,说白了就是加密和解密用同一个密钥。就像你家大门,用一把钥匙锁上,也用同一把钥匙打开。
AES(Advanced Encryption Standard)是目前最主流的对称加密算法。我个人习惯用AES-256-GCM模式,为什么?因为它同时提供了加密和认证,能防篡改。
// 使用OpenSSL进行AES-256-GCM加密
#include <openssl/evp.h>
#include <iostream>
#include <vector>
std::vector<unsigned char> aes_encrypt(
const std::vector<unsigned char>& plaintext,
const std::vector<unsigned char>& key,
const std::vector<unsigned char>& iv) {
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
if (!ctx) throw std::runtime_error("Failed to create context");
// 初始化加密上下文
EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr,
key.data(), iv.data());
std::vector<unsigned char> ciphertext(plaintext.size() + 16);
int len = 0, ciphertext_len = 0;
// 加密数据
EVP_EncryptUpdate(ctx, ciphertext.data(), &len,
plaintext.data(), plaintext.size());
ciphertext_len = len;
// 完成加密
EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len);
ciphertext_len += len;
// 获取认证标签(GCM模式特有)
std::vector<unsigned char> tag(16);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag.data());
EVP_CIPHER_CTX_free(ctx);
// 返回密文+标签
ciphertext.resize(ciphertext_len + 16);
std::copy(tag.begin(), tag.end(), ciphertext.begin() + ciphertext_len);
return ciphertext;
}
⚠️ 我曾经踩过的坑:千万别用ECB模式!它会把相同明文块加密成相同密文块,从密文就能看出数据模式。我在一个项目里见过有人用ECB加密图片,结果加密后的图片轮廓依然清晰可见——这跟没加密有啥区别?
2. 非对称加密:RSA
非对称加密就更有意思了。它用一对密钥:公钥加密,私钥解密。公钥可以公开,私钥自己藏好。
RSA的原理基于大整数分解难题。两个大素数相乘很容易,但从乘积反推这两个素数就难了。嗯,这里要注意:RSA不适合加密大量数据,它太慢了。我一般只用它来加密对称密钥,然后用AES加密实际数据——这叫混合加密。
// RSA密钥生成示例
#include <openssl/rsa.h>
#include <openssl/pem.h>
RSA* generate_rsa_keypair(int bits = 2048) {
RSA* rsa = RSA_new();
BIGNUM* e = BN_new();
BN_set_word(e, RSA_F4); // 65537
// 生成RSA密钥对
if (!RSA_generate_key_ex(rsa, bits, e, nullptr)) {
RSA_free(rsa);
BN_free(e);
throw std::runtime_error("RSA key generation failed");
}
BN_free(e);
return rsa;
}
// 使用公钥加密对称密钥
std::vector<unsigned char> rsa_encrypt_key(
RSA* rsa,
const std::vector<unsigned char>& aes_key) {
std::vector<unsigned char> encrypted(RSA_size(rsa));
int result = RSA_public_encrypt(
aes_key.size(), aes_key.data(),
encrypted.data(), rsa, RSA_PKCS1_OAEP_PADDING);
if (result == -1) {
throw std::runtime_error("RSA encryption failed");
}
encrypted.resize(result);
return encrypted;
}
💡 个人经验:RSA密钥长度至少2048位,1024位已经不安全了。另外,填充方式一定要用OAEP,别用PKCS#1 v1.5——后者有已知的 Bleichenbacher 攻击漏洞。
3. 哈希函数:SHA-256
哈希函数是个单向过程。你把任意长度的数据扔进去,它吐出一个固定长度的摘要。而且,从摘要反推原始数据,理论上是不可能的。
SHA-256输出256位(32字节)的哈希值。它有个重要特性:雪崩效应——输入改一个比特,输出就天翻地覆。
// SHA-256哈希计算
#include <openssl/sha.h>
std::vector<unsigned char> sha256_hash(
const std::vector<unsigned char>& data) {
std::vector<unsigned char> hash(SHA256_DIGEST_LENGTH);
SHA256_CTX ctx;
SHA256_Init(&ctx);
SHA256_Update(&ctx, data.data(), data.size());
SHA256_Final(hash.data(), &ctx);
return hash;
}
// 验证文件完整性
bool verify_file_integrity(
const std::string& filename,
const std::vector<unsigned char>& expected_hash) {
// 读取文件内容并计算哈希
std::ifstream file(filename, std::ios::binary);
std::vector<unsigned char> content(
(std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
auto actual_hash = sha256_hash(content);
return actual_hash == expected_hash;
}
⚠️ 注意:哈希不是加密!它不可逆。别想着用哈希来"加密"数据。另外,MD5和SHA-1已经不安全了,别再用。我在一个遗留系统里见过用MD5存密码的,结果彩虹表一查就破。
4. 消息认证码:HMAC
HMAC(Hash-based Message Authentication Code)解决了哈希的一个问题:谁都能算哈希。你发一条消息和它的SHA-256哈希,中间人可以把消息改了,重新算个哈希发过去,你根本发现不了。
HMAC引入了密钥。只有知道密钥的人,才能算出正确的MAC值。这就能验证消息的完整性和来源。
// HMAC-SHA256计算
#include <openssl/hmac.h>
std::vector<unsigned char> hmac_sha256(
const std::vector<unsigned char>& key,
const std::vector<unsigned char>& message) {
std::vector<unsigned char> result(EVP_MAX_MD_SIZE);
unsigned int result_len = 0;
HMAC(EVP_sha256(), key.data(), key.size(),
message.data(), message.size(),
result.data(), &result_len);
result.resize(result_len);
return result;
}
// 验证HMAC
bool verify_hmac(
const std::vector<unsigned char>& key,
const std::vector<unsigned char>& message,
const std::vector<unsigned char>& received_hmac) {
auto computed_hmac = hmac_sha256(key, message);
// 使用常量时间比较,防止时序攻击
if (computed_hmac.size() != received_hmac.size())
return false;
int result = 0;
for (size_t i = 0; i < computed_hmac.size(); ++i) {
result |= computed_hmac[i] ^ received_hmac[i];
}
return result == 0;
}
💡 避坑指南:比较HMAC时,千万别用 memcmp 或 == 运算符。它们一旦发现不匹配就立即返回,攻击者可以根据响应时间推断出正确值。这就是时序攻击。我见过一个支付系统因此被攻破,教训深刻。
5. 数字签名
数字签名是非对称加密的"反向使用":用私钥签名,用公钥验证。它解决了HMAC的一个局限——密钥分发问题。HMAC要求双方共享密钥,但数字签名只需要公开公钥。
实际应用中,我们不会直接对消息签名(太慢),而是先对消息的哈希值签名。
// 使用ECDSA进行数字签名(比RSA签名更高效)
#include <openssl/ec.h>
#include <openssl/evp.h>
std::vector<unsigned char> sign_message(
EVP_PKEY* private_key,
const std::vector<unsigned char>& message) {
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
if (!ctx) throw std::runtime_error("Failed to create context");
// 初始化签名上下文
EVP_DigestSignInit(ctx, nullptr, EVP_sha256(), nullptr, private_key);
// 计算签名
size_t sig_len = 0;
EVP_DigestSign(ctx, nullptr, &sig_len, message.data(), message.size());
std::vector<unsigned char> signature(sig_len);
EVP_DigestSign(ctx, signature.data(), &sig_len,
message.data(), message.size());
EVP_MD_CTX_free(ctx);
signature.resize(sig_len);
return signature;
}
bool verify_signature(
EVP_PKEY* public_key,
const std::vector<unsigned char>& message,
const std::vector<unsigned char>& signature) {
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
if (!ctx) return false;
EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, public_key);
int result = EVP_DigestVerify(ctx, signature.data(), signature.size(),
message.data(), message.size());
EVP_MD_CTX_free(ctx);
return result == 1;
}
知识体系总览
下面这张图,把今天讲的五个技术串起来了。你仔细看看它们之间的关系:
实际应用中的选择建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 大量数据加密 | AES-256-GCM | 速度快,自带认证 |
| 密钥交换 | RSA-2048 或 ECDH | 安全传输对称密钥 |
| 密码存储 | bcrypt / scrypt / Argon2 | 抗暴力破解,别用SHA-256直接存 |
| 消息完整性 | HMAC-SHA256 | 需要共享密钥 |
| 身份认证 | ECDSA 或 Ed25519 | 比RSA签名更高效 |
最后说一句:密码学实现,千万别自己造轮子。用OpenSSL、libsodium这些经过审计的库。我在一个创业项目里见过有人自己实现RSA,结果密钥生成有bug,所有"加密"的数据用公钥就能解密——那还叫加密吗?