Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

RSA加/解密

首先设

为欧拉函数(Euler’sche Phi-function)。

RSA加密算法的初始化流程如下:

  1. 选择2个很大的质数$p, q$.
  2. 计算$n:=pq$。这里的n是公开的,$p, q$则是保密的。
  3. 计算$\varphi(n)$。(因为我们知道$n=pq$,且$p, q$均为质数,我们可以利用公式$\varphi(n)=(p-1)(q-1)$进行快速计算。)
  4. 选择$e \in \{1,2,…,\varphi(n)-1\}$,使得$gcd(\varphi(n),e)=1$.
    我们的公钥为$(e,n)$。
  5. 计算密钥$d$,满足$ed \equiv 1$ mod $\varphi(n)$.

    1. 用扩展欧几里得算法找到 $x$ 和 $y$,使得:
    1. 上式中,$x$ 模 $\varphi(n)$ 的值即为 $d$:

      如果 $d < 0$,需要将其调整到正数范围:

公钥:$n, e$

私钥:$d$

假设明文内容为$m$,密文为$c$,加密

解密

如此一来,任何人都可以将一段信息加密后发送给我们,而只有我们能够解密这段信息。

注意:在RSA加解密中我们默认明文$m$是一个小于$n$的数。

而我们这样计算出来的密钥d之所以可以成功解密信息是基于以下理论:

欧拉定理(Euler Theorem):设 $n$ 是一个正整数,$a$ 是一个与 $n$ 互素的整数(即 $(a, n) = 1$),$\varphi$ 是欧拉函数。那么

完整加解密代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes		# 将string转换成大整数
from math import gcd

def modinv(e, phi):
# 扩展欧几里得算法计算模逆
def extended_gcd(a, b):
if b == 0:
return (1, 0)
else:
x1, y1 = extended_gcd(b, a % b)
x, y = y1, x1 - (a // b) * y1
return (x, y)
x, _ = extended_gcd(e, phi)
return x % phi


p = getPrime(512) # 512位的素数
q = getPrime(512) # 512位的素数

n = p * q
phi = (p - 1) * (q - 1)

e = 65537 # 大部分默认使用这个e的值
assert gcd(e, phi) == 1

d = modinv(e, phi)

# 加密
def encrypt(message, e, n):
m = bytes_to_long(message.encode())
c = pow(m, e, n)
return c

# 解密
def decrypt(cipher_int, d, n):
m = pow(cipher_int, d, n)
message = long_to_bytes(m).decode()
return message

msg = "Hello World!"
cipher = encrypt(msg, e, n)
decrypted = decrypt(cipher, d, n)

RSA-CRT

PEM 格式(Privacy Enhanced Mail)

RSA 的密钥(无论是私钥还是公钥)在保存或传输时,通常会被先序列化成二进制格式(如 DER),然后再用 Base64 编码,再加上头尾标记(如 ——-BEGIN PUBLIC KEY——-),形成一种叫 PEM 的文本格式。

我们有2种比较好用的方法来生成/解析PEM内容:

Python

使用 cryptography 库:

生成

生成私钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from Crypto.Util.number import getPrime

e = 65537
p = getPrime(512) # 512位的素数
q = getPrime(512) # 512位的素数
n = p * q
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)

# 计算 CRT 参数
# 这些参数必须传(库要求的)
dmp1 = d % (p - 1)
dmq1 = d % (q - 1)
iqmp = pow(q, -1, p)

# 构造私钥对象
private_numbers = rsa.RSAPrivateNumbers(
p=p,
q=q,
d=d,
dmp1=dmp1,
dmq1=dmq1,
iqmp=iqmp,
public_numbers=rsa.RSAPublicNumbers(e=e, n=n)
)

private_key = private_numbers.private_key()

# 导出为 PEM 格式(PKCS#8)
pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)

# 打印 PEM 私钥
print(pem.decode())

然后可以从里面提取公钥:

1
2
3
4
5
6
public_key = private_key.public_key()
pem_pub = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
print(pem_pub.decode())

解析

解析私钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from cryptography.hazmat.primitives import serialization

pem_data = b"""-----BEGIN PRIVATE KEY-----
......
-----END PRIVATE KEY-----"""

# 解析 PEM 私钥对象
private_key = serialization.load_pem_private_key(
pem_data,
password=None # 如果加密了,需要提供密码(bytes)
)

# 提取私钥参数(适用于 RSA 密钥)
numbers = private_key.private_numbers()

# 打印出 RSA 私钥参数
print(f"Modulus (n): {numbers.public_numbers.n}")
print(f"Public Exponent (e): {numbers.public_numbers.e}")
print(f"Private Exponent (d): {numbers.d}")
print(f"Prime 1 (p): {numbers.p}")
print(f"Prime 2 (q): {numbers.q}")
print(f"d mod (p-1): {numbers.dmp1}")
print(f"d mod (q-1): {numbers.dmq1}")
print(f"q^-1 mod p: {numbers.iqmp}") # 用于CRT的

解析公钥:

1
2
3
4
5
6
7
8
9
10
11
12
from cryptography.hazmat.primitives import serialization

pem_data = b"""-----BEGIN PUBLIC KEY-----
......
-----END PUBLIC KEY-----"""

key = serialization.load_pem_public_key(pem_data)

# 提取公钥的详细信息
numbers = key.public_numbers()
print(f"Modulus (n): {numbers.n}")
print(f"Exponent (e): {numbers.e}")

攻击

质因数分解n

Fermat’s Factorization

这种方法适用于p和q的大小差不多的情况。

算法原理
我们先来看一个引理:
引理 1 如果 $n$是一个奇正整数,那么将$n$分解为两个正整数的乘积,与将其表示为两个平方数之差之间存在一一对应关系。

证明: 设n是一个奇正整数,且$ n = ab$,其中$a$和$b$是两个正的奇整数。那么$n$可以表示为两个平方数之差:

其中,$r = \frac{a - b}{2}$,$s = \frac{a + b}{2}$。
反过来,如果$n$是两个平方数之差,即$n = s^2 - r^2$,那么我们可以将其因式分解为:

也就算说:对于任意的奇正整数$n$,只要我们可以找到2个数$s,r$,使得他们的平方差等于$n$,那么我们就可以得到$n$的两个因数。$\square$


因此我们可以考虑以下算法:

  1. 首先计算 $t := \lceil \sqrt{n} \rceil$
  2. 依次测试 $t^2-n, (t+1)^2-n,… $ 是否为平方数
    1. 如果是(即$r^2 = s^2-n$ ),则得到$n = (s-r)(s+r)$。
    2. 如果不是,则这个方法行不通。

注意,这个算法一定会终止,因为测试到$s = n+1$时:

会得到$n = n\cdot 1$(没有什么意义),但是在实际情况里我们不一定会让它跑完,因为太慢了。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import math

def fermat_factor(n, max_iter=1000000):
a = math.isqrt(n)
if a * a < n:
a += 1
for _ in range(max_iter):
b2 = a*a - n
b = math.isqrt(b2)
if b*b == b2:
return a - b, a + b
a += 1
return None

n =
result = fermat_factor(n)
if result:
print("p =", result[0], "q =", result[1])
else:
print("未成功")

Pollard’s p-1

适用于$n$有一个smooth factor $p$(即所有满足$q | (p-1)$的素数$q$都很小)的情况。

算法原理

首先来回顾以下费马小定理:

费马小定理(Fermat’s Little Theorem):设 $p$ 是一个素数,$a$ 是一个整数,且 $a$ 与 $p$ 互素(即 $(a, p) = 1$)。那么有:


我们现在假设$p$是$n$的一个素因数。如果我们能找到一个整数$k$使得$(p-1)|k!$,即存在一个$l$使得$(p-1)l=k!$,那么就有:

这意味着 $p|(2^{k!}-1)$,也就是说$p$是$2^{k!}-1$和$n$的一个共同的因数。

所以算法如下:

  1. 对于$k=1,2,3,…$我们计算

  2. 计算$M = \text{gcd}(r^k-1,n)$:

    1. 如果等于1,则继续;
    2. 如果不等于1,那么$p$一定是$M$的一个因数。这时只需要对$M$进行质因数分解。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import math

def pollard_p1(n, B=1000000):
a = 2
for j in range(2, B):
a = pow(a, j, n)
g = math.gcd(a-1, n)
if 1 < g < n:
return g, n // g
return None

n =
result = pollard_p1(n)
if result:
print("p =", result[0], "q =", result[1])
else:
print("未成功")

The Elliptic Curve Factorization Method

详细可见:

https://doc.sagemath.org/html/en/reference/interfaces/sage/interfaces/ecm.html

我们可以直接使用sageecm.factor()函数。

例子:

1
2
3
4
from sage.all import *
n =
factors = ecm.factor(n)
phi = prod([f - 1 for f in factors])

低指数攻击

低加密指数攻击

适用于加密所选的e非常小的情况,比如说 e=3,5 之类的。

这个时候很有可能 $m^e < 10^5 \cdot n$,所以可以直接遍历。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from gmpy2 import iroot
from Crypto.Util.number import long_to_bytes

e =
n =
c =

# 可以根据条件以及测试结果调整max的大小
max = 10000
for i in range(1, max):
modified_c = c + i * n
m_root, exact = iroot(modified_c, e)
if exact:
print(f"找到解: i = {i}")
print("明文:", long_to_bytes(m_root))
break
else:
print("尝试范围内未找到可解的 m")

低解密指数攻击

这种攻击适用于d特别小的情况。(当给的e非常大的时候都可以试一下这种方法。)

Wiener’s Attack

这个是基于连分数(Continued Fraction)的一种算法。适用于 $d < \frac{n^{1/4}}{3}$ 的情况。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import math
from Crypto.Util.number import long_to_bytes

def continued_fraction(numerator, denominator):
"""生成分数 numerator/denominator 的连分数表示"""
cf = []
while denominator:
a = numerator // denominator
cf.append(a)
numerator, denominator = denominator, numerator - a * denominator
return cf


def convergents_from_cf(cf):
"""根据连分数序列 cf 生成收敛分数 (k, d)"""
n0, d0 = cf[0], 1
yield (n0, 1)
if len(cf) == 1:
return
n1 = cf[1] * cf[0] + 1
d1 = cf[1]
yield (n1, d1)
for i in range(2, len(cf)):
ni = cf[i] * n1 + n0
di = cf[i] * d1 + d0
yield (ni, di)
n0, d0, n1, d1 = n1, d1, ni, di


def is_perfect_square(x):
"""判断 x 是否为完全平方数"""
if x < 0:
return False
s = math.isqrt(x)
return s * s == x


def wiener_attack(e, n):
"""
使用 Wiener 攻击尝试恢复 RSA 私钥 d。
参数:
e: 公钥指数
n: 模数
返回:
若成功,返回私钥指数 d;否则返回 None。
"""
cf = continued_fraction(e, n)
for k, d in convergents_from_cf(cf):
if k == 0:
continue
# 检查 (e*d - 1) 是否被 k 整除,以得到 phi
if (e * d - 1) % k != 0:
continue
phi = (e * d - 1) // k
# 计算方程 x^2 - (n - phi + 1)x + n = 0 的判别式
s = n - phi + 1
discr = s * s - 4 * n
if discr >= 0 and is_perfect_square(discr):
t = math.isqrt(discr)
# 计算 p, q
p = (s + t) // 2
q = (s - t) // 2
if p * q == n:
return d
return None


n =
e =
c =

d = wiener_attack(e, n)
if d:
print(f"得到d: {d}")
m = pow(c, d, n)
message = long_to_bytes(m).decode()
print(f"明文内容为:{message}")
else:
print("Wiener 攻击未能恢复 d")

共模攻击

适用情况:同一条明文消息被用相同的模数但不同的指数加密多次

共模攻击成立必须满足以下几个条件:

  1. 使用 RSA 加密的两个密文:
  2. 两个密文 $c_1$ 和 $c_1$ 是使用相同的模数 n,但使用了不同的指数 e₁ 和 e₂加密得到的。

  3. $e_1$ 和 $e_2$ 需要满足互质:$\text{gcd}(e₁, e₂) = 1$。

攻击方法:使用扩展欧几里得算法(Extended Euclidean Algorithm)

当满足上述条件时,我们可以用扩展欧几里得算法找到整数 x 和 y,使得:

然后便可计算出明文:

因为:

注意:

  • 如果 x 或 y 为负数,则需要使用逆元(modular inverse)处理负指数:
    • $ c^{-a} \mod n = (c^{-1})^a \mod n $

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from Crypto.Util.number import inverse, long_to_bytes
from math import gcd

n =
e1 =
e2 =
c1 =
c2 =

# 使用扩展欧几里得算法求出 x 和 y
def extended_gcd(a, b):
if b == 0:
return (1, 0)
else:
x1, y1 = extended_gcd(b, a % b)
x = y1
y = x1 - (a // b) * y1
return (x, y)

x, y = extended_gcd(e1, e2)

# 处理负指数(计算逆元)
if x < 0:
c1 = inverse(c1, n)
x = -x
if y < 0:
c2 = inverse(c2, n)
y = -y

m = (pow(c1, x, n) * pow(c2, y, n)) % n

# 解密后的明文
print(long_to_bytes(m))