五、非对称加密实战(RSA):RSA算法原理、密钥生成、公钥加密与私钥解密
说到非对称加密,RSA 绝对是绕不开的基石。我最早接触 RSA 是在做一套金融报文系统的时候,那时候对它的印象就是「慢,但真安全」。后来踩过几次坑,才慢慢摸透了它的脾气。今天咱们就把它拆开揉碎了讲清楚。
5.1 RSA 算法原理:数学其实没那么可怕
RSA 的核心思想,说白了就是「单向门」——正向走很容易,反向走几乎不可能。它依赖的是大整数分解难题:两个大质数相乘很简单,但给你一个大合数让你找出是哪两个质数乘出来的,那就难如登天了。
具体来说,RSA 的数学基础是欧拉定理和模反元素。我当年看这些数学公式也是一头雾水,后来发现只要记住三个关键步骤就够了:
- 选两个大质数 p 和 q —— 越大越安全,但越慢
- 计算 n = p × q —— n 就是公钥和私钥都会用到的模数
- 找一个 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 直接加密大文件。正确的做法是「混合加密」:
- 用 AES 等对称算法加密数据
- 用 RSA 加密 AES 的密钥
- 把加密后的数据和加密后的密钥一起发送
这样做的好处很明显:对称加密快,能处理任意大小的数据;非对称加密安全,解决了密钥分发问题。我在做物联网设备固件升级时就是这么干的——先用 RSA 加密 AES 密钥,再用 AES 加密固件包,既安全又高效。
5.5 避坑指南:我踩过的那些雷
- 密钥长度别选太短:我曾经为了性能用 512 位密钥,结果被同事用一台普通电脑花了两天就分解了。现在最低要求 2048 位,推荐 4096 位。
- 随机数生成器要靠谱:RSA 的安全性高度依赖随机数质量。在嵌入式设备上,我见过用 rand() 生成密钥的——那基本等于没加密。
- 私钥一定要保护好:别硬编码在代码里,别明文传输。用硬件安全模块(HSM)或者至少用密码保护 PEM 文件。
- 注意侧信道攻击:解密时间、功耗、电磁辐射都可能泄露信息。高安全场景下要用常数时间实现。
5.6 RSA 核心流程一览
下面这张图把 RSA 的完整流程串起来了,从密钥生成到加解密,一目了然。
这张图把 RSA 的完整生命周期都画出来了。你注意看,公钥加密和私钥解密是两条独立的路径,中间只通过密文连接。这就是非对称加密的精髓——加密和解密用的是不同的钥匙。
一句话总结:RSA 用数学上的单向门实现了「公开加密、秘密解密」。公钥随便给,私钥死守好。实际项目中别裸用 RSA,一定要配合对称加密做混合方案。
公众号:蓝海资料掘金营,微信deep3321