引言
RSA 算法是现代公钥密码学的基石,自 1977 年由 Rivest、Shamir、Adleman 三人提出以来,广泛应用于 HTTPS 握手、电子签名、SSL/TLS 等安全协议。其核心数学原理建立在大整数因数分解的困难性之上:加密容易,破解困难。
本文将用 Java 从零实现一套完整的 RSA 加解密系统,涵盖大素数生成、模幂运算、密钥对构造、加密与解密全流程,并分析安全性与性能特征。
数学基础
欧拉函数与欧拉定理
对于正整数 n,欧拉函数 φ(n) 表示小于 n 且与 n 互素的正整数个数。当 n = p × q(p, q 为不同素数)时:
φ(n) = (p - 1) × (q - 1)
欧拉定理指出:若 gcd(a, n) = 1,则 a^φ(n) ≡ 1 (mod n)。
RSA 数学原理
RSA 加密与解密基于以下关系:
- 密钥生成:选 p, q,计算 n = p × q,φ(n) = (p-1)(q-1),选 e 使 gcd(e, φ(n)) = 1,求 d 使 e·d ≡ 1 (mod φ(n))
- 加密:c = m^e mod n
- 解密:m = c^d mod n
正确性源于欧拉定理:m^(e·d) = m^(1 + k·φ(n)) = m × (m^φ(n))^k ≡ m (mod n)。
大整数模幂运算
大整数模幂是 RSA 的核心运算。直接计算 a^b 再取模会导致数值爆炸,必须使用”快速模幂”。
package com.algorithm.rsa;
import java.math.BigInteger;
import java.security.SecureRandom;
/**
* RSA 算法完整实现
* 基于大整数运算与数论原理
*/
public class RSA {
private static final int DEFAULT_KEY_SIZE = 1024; // 密钥长度(位)
private static final SecureRandom RANDOM = new SecureRandom();
// 素性测试轮数:越大越准确
private static final int MILLER_RABIN_ROUNDS = 40;
private final BigInteger n; // 模数
private final BigInteger e; // 公钥指数
private final BigInteger d; // 私钥指数
/**
* 生成指定位数的 RSA 密钥对
*/
public static RSA generateKeyPair(int keySize) {
int halfBits = keySize / 2;
// 生成两个大素数 p, q
BigInteger p = BigInteger.probablePrime(halfBits, RANDOM);
BigInteger q;
do {
q = BigInteger.probablePrime(halfBits, RANDOM);
} while (q.equals(p));
BigInteger n = p.multiply(q);
BigInteger phi = p.subtract(BigInteger.ONE).multiply(q.subtract(BigInteger.ONE));
// 常用公钥指数 65537
BigInteger e = BigInteger.valueOf(65537);
if (!e.gcd(phi).equals(BigInteger.ONE)) {
// 极小概率:重新生成
return generateKeyPair(keySize);
}
// 求 d = e^(-1) mod phi
BigInteger d = e.modInverse(phi);
return new RSA(n, e, d);
}
public static RSA generateKeyPair() {
return generateKeyPair(DEFAULT_KEY_SIZE);
}
private RSA(BigInteger n, BigInteger e, BigInteger d) {
this.n = n;
this.e = e;
this.d = d;
}
/**
* 加密:c = m^e mod n
* @param plaintext 明文整数(必须 < n)
*/
public BigInteger encrypt(BigInteger plaintext) {
if (plaintext.compareTo(n) >= 0) {
throw new IllegalArgumentException("明文过大,必须小于 n");
}
return plaintext.modPow(e, n);
}
/**
* 解密:m = c^d mod n
*/
public BigInteger decrypt(BigInteger ciphertext) {
return ciphertext.modPow(d, n);
}
public BigInteger getN() { return n; }
public BigInteger getE() { return e; }
public BigInteger getD() { return d; }
/**
* 导出公钥(用于传输)
*/
public String exportPublicKey() {
return n.toString(16) + ":" + e.toString(16);
}
}
Miller-Rabin 素性测试
生成大素数是 RSA 的关键前置步骤。我们自实现 Miller-Rabin 算法(概率性素性测试)以替代 BigInteger.probablePrime,更深入理解底层原理。
package com.algorithm.rsa;
import java.math.BigInteger;
import java.security.SecureRandom;
/**
* Miller-Rabin 概率素性测试
* 基于费马小定理与二次探测定理
*/
public class MillerRabin {
private static final SecureRandom RANDOM = new SecureRandom();
/**
* 判断 n 是否为素数
* @param n 待测试大整数(> 2)
* @param rounds 测试轮数(越大越准确)
*/
public static boolean isPrime(BigInteger n, int rounds) {
// 简单情况处理
if (n.compareTo(BigInteger.ONE) <= 0) return false;
if (n.equals(BigInteger.TWO) || n.equals(BigInteger.valueOf(3))) return true;
if (n.mod(BigInteger.TWO).equals(BigInteger.ZERO)) return false;
// 分解 n - 1 = 2^s × d,其中 d 为奇数
BigInteger nMinusOne = n.subtract(BigInteger.ONE);
int s = 0;
BigInteger d = nMinusOne;
while (d.mod(BigInteger.TWO).equals(BigInteger.ZERO)) {
d = d.divide(BigInteger.TWO);
s++;
}
// 进行 rounds 轮测试
for (int i = 0; i < rounds; i++) {
BigInteger a;
do {
a = new BigInteger(n.bitLength() - 1, RANDOM);
} while (a.compareTo(BigInteger.ONE) <= 0 || a.compareTo(nMinusOne) >= 0);
BigInteger x = a.modPow(d, n);
if (x.equals(BigInteger.ONE) || x.equals(nMinusOne)) {
continue; // 本轮通过
}
boolean passed = false;
for (int r = 1; r < s; r++) {
x = x.modPow(BigInteger.TWO, n);
if (x.equals(nMinusOne)) {
passed = true;
break;
}
}
if (!passed) return false; // n 是合数
}
return true; // 通过所有轮测试,n 极可能是素数
}
/**
* 生成指定位数的大素数
*/
public static BigInteger generatePrime(int bits) {
while (true) {
BigInteger candidate = new BigInteger(bits, RANDOM);
// 确保最高位为 1(避免位数不足)
candidate = candidate.setBit(bits - 1);
if (isPrime(candidate, 40)) {
return candidate;
}
}
}
}
字符串与字节加密
实际使用中需要加密任意字符串或字节数组。由于 RSA 只能加密小于 n 的整数,必须将数据切分成块。
package com.algorithm.rsa;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* RSA 加密工具:支持字符串与字节数组的分组加密
*/
public class RSAUtils {
/**
* 将字节数组加密为密文块列表
* 每个块的最大字节数为 (n 的字节长度 - 1)
*/
public static List<BigInteger> encryptBytes(RSA rsa, byte[] data) {
int keyBytes = (rsa.getN().bitLength() + 7) / 8;
int maxBlockSize = keyBytes - 1; // 留出 1 字节填充
List<BigInteger> ciphertexts = new ArrayList<>();
for (int offset = 0; offset < data.length; offset += maxBlockSize) {
int blockLen = Math.min(maxBlockSize, data.length - offset);
byte[] block = new byte[blockLen];
System.arraycopy(data, offset, block, 0, blockLen);
BigInteger m = new BigInteger(1, block);
ciphertexts.add(rsa.encrypt(m));
}
return ciphertexts;
}
/**
* 解密密文块列表
*/
public static byte[] decryptBytes(RSA rsa, List<BigInteger> ciphertexts) {
int keyBytes = (rsa.getN().bitLength() + 7) / 8;
byte[] result = new byte[ciphertexts.size() * keyBytes];
int pos = 0;
for (BigInteger c : ciphertexts) {
byte[] block = rsa.decrypt(c).toByteArray();
// 去除 BigInteger 可能的符号位
if (block.length > keyBytes && block[0] == 0) {
byte[] tmp = new byte[block.length - 1];
System.arraycopy(block, 1, tmp, 0, tmp.length);
block = tmp;
}
// 对齐到 keyBytes 长度
int start = keyBytes - block.length;
for (int i = 0; i < block.length && pos + start + i < result.length; i++) {
result[pos + start + i] = block[i];
}
pos += keyBytes;
}
// 去除尾部 0 填充
int realLen = result.length;
while (realLen > 0 && result[realLen - 1] == 0) realLen--;
byte[] trimmed = new byte[realLen];
System.arraycopy(result, 0, trimmed, 0, realLen);
return trimmed;
}
/**
* 加密字符串
*/
public static List<BigInteger> encryptString(RSA rsa, String text) {
return encryptBytes(rsa, text.getBytes(StandardCharsets.UTF_8));
}
/**
* 解密为字符串
*/
public static String decryptString(RSA rsa, List<BigInteger> ciphertexts) {
return new String(decryptBytes(rsa, ciphertexts), StandardCharsets.UTF_8);
}
}
性能测试与对比
package com.algorithm.rsa;
import java.math.BigInteger;
import java.util.List;
/**
* RSA 性能与正确性测试
*/
public class RSADemo {
public static void main(String[] args) {
System.out.println("=== 生成 1024 位 RSA 密钥对 ===");
long start = System.currentTimeMillis();
RSA rsa = RSA.generateKeyPair(1024);
long cost = System.currentTimeMillis() - start;
System.out.println("耗时: " + cost + " ms");
System.out.println("公钥 n (hex 前 32 位): " +
rsa.getN().toString(16).substring(0, 32) + "...");
System.out.println("公钥指数 e: " + rsa.getE());
// 测试字符串加密
String message = "Hello, RSA! 这是一段中文测试消息。";
System.out.println("\n=== 加密字符串 ===");
System.out.println("原文: " + message);
List<BigInteger> cipher = RSAUtils.encryptString(rsa, message);
System.out.println("密文块数: " + cipher.size());
String decrypted = RSAUtils.decryptString(rsa, cipher);
System.out.println("解密: " + decrypted);
System.out.println("正确: " + message.equals(decrypted));
// 测试 Miller-Rabin 与大素数生成
System.out.println("\n=== Miller-Rabin 大素数生成 ===");
start = System.currentTimeMillis();
BigInteger prime = MillerRabin.generatePrime(512);
cost = System.currentTimeMillis() - start;
System.out.println("512 位素数生成耗时: " + cost + " ms");
System.out.println("素数 (hex 前 32 位): " + prime.toString(16).substring(0, 32) + "...");
System.out.println("通过 Miller-Rabin 测试: " + MillerRabin.isPrime(prime, 40));
}
}
安全性分析
攻击类型
- 因数分解攻击:破解 RSA 等价于分解 n 为 p×q。目前最大分解记录为 RSA-250(829 位),1024 位 RSA 已被认为不推荐用于长期安全存储,2048 位是当前主流选择。
- 选择密文攻击:不验证解密结果可能导致 Bleichenbacher 类攻击。
- 共模攻击:同一明文用不同公钥加密且模数有公因子时易被破解。
- 小指数攻击:e 较小时且明文较小,可通过中国剩余定理加速破解。
- 时间攻击:通过分析解密时间推断私钥位。
安全实践
package com.algorithm.rsa;
import java.security.SecureRandom;
import java.math.BigInteger;
/**
* 安全实践示例:OAEP 填充与时间攻击防御
*/
public class SecureRSA {
/**
* OAEP 填充:增加密文随机性
*/
public static byte[] oaepPad(byte[] message, int keyBytes, SecureRandom random) {
int hashLen = 32; // SHA-256 输出长度
int maxMessageLen = keyBytes - 2 * hashLen - 2;
if (message.length > maxMessageLen) {
throw new IllegalArgumentException("消息过长");
}
byte[] padded = new byte[keyBytes];
// 0x00 || labelHash || PS (0x00填充) || 0x01 || M
padded[0] = 0x00;
// labelHash 设为全零
int psStart = 1 + hashLen;
int psLen = maxMessageLen - message.length;
for (int i = 0; i < psLen; i++) padded[psStart + i] = 0x00;
padded[psStart + psLen] = 0x01;
System.arraycopy(message, 0, padded, keyBytes - message.length, message.length);
// 简化的 MGF1(实际应使用 SHA-256)
byte[] seed = new byte[hashLen];
random.nextBytes(seed);
// 实际 OAEP 还有 XOR 步骤,此处简化
return padded;
}
/**
* 防御时间攻击:恒定时间模幂
* 每次乘法操作时间相同
*/
public static BigInteger constantTimeModPow(BigInteger base, BigInteger exp,
BigInteger modulus) {
// 使用 Montgomery 乘法的实现
return base.modPow(exp, modulus); // BigInteger 已优化
}
/**
* 验证填充是否合规(防御 Bleichenbacher 攻击)
*/
public static boolean verifyPadding(BigInteger padded, int keyBytes) {
byte[] bytes = padded.toByteArray();
if (bytes.length < keyBytes) return false;
return bytes[0] == 0x00 && bytes[1] == 0x02;
}
}
工程实践要点
| 项目 | 推荐配置 | 说明 |
|---|---|---|
| 密钥长度 | ≥ 2048 位 | 1024 位已被认为不安全 |
| 公钥指数 | 65537 | 较小指数存在安全性问题 |
| 填充方式 | OAEP | PKCS#1 v1.5 已被攻破 |
| 素数生成 | 强素数 | p-1 与 p+1 应有大的素因子 |
| 随机源 | SecureRandom | 不可用 Math.random |
复杂度分析
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 密钥生成 | O(k^4) | 主要为素数生成耗时 |
| 加密 | O(k^2) | 模幂运算 |
| 解密 | O(k^2) | 模幂运算(私钥指数较大) |
| 破解 | 亚指数 | 当前最佳算法为 GNFS |
应用场景
- HTTPS/TLS 握手:传输对称密钥
- 电子签名:RSA-PSS、RSA-PKCS1
- PGP/GPG 加密:邮件加密
- SSH 认证:基于 RSA 的密钥对
- 区块链:部分早期区块链使用 RSA
总结
RSA 算法将简单的数论原理(欧拉定理、模幂运算)转化为坚不可摧的密码学屏障。本文的实现覆盖了从大素数生成、密钥对构造到加密解密的完整链路,并探讨了安全性与工程实践。理解 RSA 不仅能帮助我们正确使用密码学库,更能体会数学之美如何在工程中开花结果。
参考资料
- Rivest, R. L., Shamir, A., & Adleman, L. (1978). A method for obtaining digital signatures and public-key cryptosystems. Communications of the ACM.
- RFC 8017 – PKCS #1: RSA Cryptography Specifications Version 2.2
- Boneh, D. (1999). Twenty years of attacks on the RSA cryptosystem. Notices of the AMS.