密码学基础:对称加密、非对称加密、哈希、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) 同一密钥加密/解密 哈希函数 (SHA-256) 单向摘要,不可逆 非对称加密 (RSA) 公钥/私钥对 消息认证码 (HMAC) 哈希 + 密钥 = 完整性+真实性 数字签名 哈希 + 非对称 = 不可否认性 混合使用:AES加密数据 + RSA加密密钥 + 数字签名

实际应用中的选择建议

场景 推荐方案 原因
大量数据加密 AES-256-GCM 速度快,自带认证
密钥交换 RSA-2048 或 ECDH 安全传输对称密钥
密码存储 bcrypt / scrypt / Argon2 抗暴力破解,别用SHA-256直接存
消息完整性 HMAC-SHA256 需要共享密钥
身份认证 ECDSA 或 Ed25519 比RSA签名更高效

最后说一句:密码学实现,千万别自己造轮子。用OpenSSL、libsodium这些经过审计的库。我在一个创业项目里见过有人自己实现RSA,结果密钥生成有bug,所有"加密"的数据用公钥就能解密——那还叫加密吗?

公众号:蓝海资料掘金营,微信deep3321