五、非对称加密实战(RSA):RSA算法原理、密钥生成、公钥加密与私钥解密

说到非对称加密,RSA 绝对是绕不开的基石。我最早接触 RSA 是在做一套金融报文系统的时候,那时候对它的印象就是「慢,但真安全」。后来踩过几次坑,才慢慢摸透了它的脾气。今天咱们就把它拆开揉碎了讲清楚。

5.1 RSA 算法原理:数学其实没那么可怕

RSA 的核心思想,说白了就是「单向门」——正向走很容易,反向走几乎不可能。它依赖的是大整数分解难题:两个大质数相乘很简单,但给你一个大合数让你找出是哪两个质数乘出来的,那就难如登天了。

具体来说,RSA 的数学基础是欧拉定理和模反元素。我当年看这些数学公式也是一头雾水,后来发现只要记住三个关键步骤就够了:

  1. 选两个大质数 p 和 q —— 越大越安全,但越慢
  2. 计算 n = p × q —— n 就是公钥和私钥都会用到的模数
  3. 找一个 e 和 d —— e 是公钥指数,d 是私钥指数,它们满足 e × d ≡ 1 (mod φ(n))

这里 φ(n) = (p-1)(q-1),也就是欧拉函数。你想想看,如果不知道 p 和 q,光凭 n 想算出 φ(n),那就得分解 n——这正是 RSA 的安全根基。

核心公式:

  • 加密:c = me mod n
  • 解密:m = cd mod n

其中 m 是明文,c 是密文。只要 e 和 n 公开,任何人都能加密;但只有持有 d 的人才能解密。

5.2 密钥生成:手写一个简易 RSA

我在项目中遇到过一个问题:OpenSSL 生成的 RSA 密钥对太大了,嵌入式设备跑不动。后来我写了一个小工具,专门生成 512 位的测试密钥。下面这个例子就是简化版,帮你理解生成过程。

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

// 判断质数(简化版,实际要用 Miller-Rabin)
int is_prime(int n) {
    if (n < 2) return 0;
    for (int i = 2; i <= sqrt(n); i++) {
        if (n % i == 0) return 0;
    }
    return 1;
}

// 求最大公约数
int gcd(int a, int b) {
    while (b != 0) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

// 求模反元素(扩展欧几里得算法)
int mod_inverse(int e, int phi) {
    int t = 0, newt = 1;
    int r = phi, newr = e;
    while (newr != 0) {
        int quotient = r / newr;
        int temp = t;
        t = newt;
        newt = temp - quotient * newt;
        temp = r;
        r = newr;
        newr = temp - quotient * newr;
    }
    if (r > 1) return -1; // 没有模反元素
    if (t < 0) t += phi;
    return t;
}

int main() {
    int p = 61, q = 53;  // 实际应用中要用大质数
    int n = p * q;
    int phi = (p-1) * (q-1);
    int e = 17;  // 常用公钥指数
    
    // 确保 e 和 phi 互质
    if (gcd(e, phi) != 1) {
        printf("e 和 phi 不互质,请重新选择\n");
        return -1;
    }
    
    int d = mod_inverse(e, phi);
    
    printf("公钥: (n=%d, e=%d)\n", n, e);
    printf("私钥: (n=%d, d=%d)\n", n, d);
    printf("p=%d, q=%d, phi=%d\n", p, q, phi);
    
    // 测试加密解密
    int m = 42;  // 明文
    int c = (int)pow(m, e) % n;  // 加密
    int m2 = (int)pow(c, d) % n; // 解密
    
    printf("明文: %d\n", m);
    printf("密文: %d\n", c);
    printf("解密: %d\n", m2);
    
    return 0;
}

注意:上面的代码用了 pow() 函数,这在处理大数时会溢出。实际项目中必须用大数库(如 GMP、OpenSSL 的 BIGNUM)来处理。我曾经在原型阶段用 int 测试,结果加密 100 以上的数字就出错了——嗯,这个坑我替你们踩过了。

5.3 公钥加密与私钥解密:实战中的正确姿势

实际开发中,没人会自己手写 RSA 的数学运算。我们通常用 OpenSSL 库。下面是我常用的加密解密流程:

#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>

// 生成 RSA 密钥对并保存到文件
void generate_rsa_keypair(const char* pub_file, const char* priv_file) {
    RSA* rsa = RSA_new();
    BIGNUM* bn = BN_new();
    BN_set_word(bn, RSA_F4);  // 65537,常用公钥指数
    
    // 生成 2048 位密钥
    if (!RSA_generate_key_ex(rsa, 2048, bn, NULL)) {
        fprintf(stderr, "密钥生成失败: %s\n", ERR_error_string(ERR_get_error(), NULL));
        return;
    }
    
    // 保存公钥
    FILE* pub_fp = fopen(pub_file, "wb");
    PEM_write_RSAPublicKey(pub_fp, rsa);
    fclose(pub_fp);
    
    // 保存私钥
    FILE* priv_fp = fopen(priv_file, "wb");
    PEM_write_RSAPrivateKey(priv_fp, rsa, NULL, NULL, 0, NULL, NULL);
    fclose(priv_fp);
    
    RSA_free(rsa);
    BN_free(bn);
}

// 公钥加密
int rsa_encrypt(const char* pub_file, const unsigned char* plaintext, 
                int plain_len, unsigned char* ciphertext) {
    FILE* fp = fopen(pub_file, "rb");
    RSA* rsa = PEM_read_RSAPublicKey(fp, NULL, NULL, NULL);
    fclose(fp);
    
    int result = RSA_public_encrypt(plain_len, plaintext, ciphertext, 
                                    rsa, RSA_PKCS1_OAEP_PADDING);
    RSA_free(rsa);
    return result;  // 返回加密后的长度,失败返回 -1
}

// 私钥解密
int rsa_decrypt(const char* priv_file, const unsigned char* ciphertext, 
                int cipher_len, unsigned char* plaintext) {
    FILE* fp = fopen(priv_file, "rb");
    RSA* rsa = PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL);
    fclose(fp);
    
    int result = RSA_private_decrypt(cipher_len, ciphertext, plaintext, 
                                     rsa, RSA_PKCS1_OAEP_PADDING);
    RSA_free(rsa);
    return result;  // 返回解密后的长度,失败返回 -1
}

个人经验:填充模式一定要选对。RSA_PKCS1_OAEP_PADDING 是目前推荐的安全填充方式。我见过有人用 RSA_PKCS1_PADDING,结果被 padding oracle 攻击打穿了——那场面,真是惨不忍睹。

5.4 RSA 的局限与混合加密方案

RSA 有个硬伤:慢。而且它加密的数据长度不能超过密钥长度(还要减去填充开销)。比如 2048 位的密钥,一次最多加密 190 字节左右。

所以实际项目中,我们从来不用 RSA 直接加密大文件。正确的做法是「混合加密」:

  1. 用 AES 等对称算法加密数据
  2. 用 RSA 加密 AES 的密钥
  3. 把加密后的数据和加密后的密钥一起发送

这样做的好处很明显:对称加密快,能处理任意大小的数据;非对称加密安全,解决了密钥分发问题。我在做物联网设备固件升级时就是这么干的——先用 RSA 加密 AES 密钥,再用 AES 加密固件包,既安全又高效。

5.5 避坑指南:我踩过的那些雷

  • 密钥长度别选太短:我曾经为了性能用 512 位密钥,结果被同事用一台普通电脑花了两天就分解了。现在最低要求 2048 位,推荐 4096 位。
  • 随机数生成器要靠谱:RSA 的安全性高度依赖随机数质量。在嵌入式设备上,我见过用 rand() 生成密钥的——那基本等于没加密。
  • 私钥一定要保护好:别硬编码在代码里,别明文传输。用硬件安全模块(HSM)或者至少用密码保护 PEM 文件。
  • 注意侧信道攻击:解密时间、功耗、电磁辐射都可能泄露信息。高安全场景下要用常数时间实现。

5.6 RSA 核心流程一览

下面这张图把 RSA 的完整流程串起来了,从密钥生成到加解密,一目了然。

RSA 非对称加密核心流程 密钥生成 选择大质数 p, q → 计算 n = p×q → 计算 φ(n) = (p-1)(q-1) → 选择 e → 计算 d 公钥: (n, e) | 私钥: (n, d) 公钥公开分发,私钥秘密保存 发送方:公钥加密 c = me mod n 明文 m → 密文 c 接收方:私钥解密 m = cd mod n 密文 c → 明文 m 传输密文 安全基础:大整数分解难题 已知 n 和 e,无法在合理时间内计算出 d(除非能分解 n) 推荐密钥长度:2048 位(当前安全)| 4096 位(长期安全)

这张图把 RSA 的完整生命周期都画出来了。你注意看,公钥加密和私钥解密是两条独立的路径,中间只通过密文连接。这就是非对称加密的精髓——加密和解密用的是不同的钥匙。

一句话总结:RSA 用数学上的单向门实现了「公开加密、秘密解密」。公钥随便给,私钥死守好。实际项目中别裸用 RSA,一定要配合对称加密做混合方案。


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