RSA加/解密 首先设
为欧拉函数(Euler’sche Phi-function)。
RSA加密算法的初始化流程如下:
选择2个很大的质数$p, q$.
计算$n:=pq$。这里的n是公开的,$p, q$则是保密的。
计算$\varphi(n)$。(因为我们知道$n=pq$,且$p, q$均为质数,我们可以利用公式$\varphi(n)=(p-1)(q-1)$进行快速计算。)
选择$e \in \{1,2,…,\varphi(n)-1\}$,使得$gcd(\varphi(n),e)=1$. 我们的公钥为$(e,n)$。
计算密钥$d$,满足$ed \equiv 1$ mod $\varphi(n)$.
用扩展欧几里得算法找到 $x$ 和 $y$,使得:
上式中,$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 from math import gcddef 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 ) q = getPrime(512 ) n = p * q phi = (p - 1 ) * (q - 1 ) e = 65537 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 rsafrom cryptography.hazmat.primitives import serializationfrom Crypto.Util.number import getPrimee = 65537 p = getPrime(512 ) q = getPrime(512 ) n = p * q phi = (p - 1 ) * (q - 1 ) d = pow (e, -1 , phi) 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 = private_key.private_bytes( encoding=serialization.Encoding.PEM, format =serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() ) 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 serializationpem_data = b"""-----BEGIN PRIVATE KEY----- ...... -----END PRIVATE KEY-----""" private_key = serialization.load_pem_private_key( pem_data, password=None ) numbers = private_key.private_numbers() 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} " )
解析公钥:
1 2 3 4 5 6 7 8 9 10 11 12 from cryptography.hazmat.primitives import serializationpem_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$
因此我们可以考虑以下算法:
首先计算 $t := \lceil \sqrt{n} \rceil$
依次测试 $t^2-n, (t+1)^2-n,… $ 是否为平方数
如果是(即$r^2 = s^2-n$ ),则得到$n = (s-r)(s+r)$。
如果不是,则这个方法行不通。
注意,这个算法一定会终止,因为测试到$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 mathdef 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$的一个共同的因数。
所以算法如下:
对于$k=1,2,3,…$我们计算
计算$M = \text{gcd}(r^k-1,n)$:
如果等于1,则继续;
如果不等于1,那么$p$一定是$M$的一个因数。这时只需要对$M$进行质因数分解。
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import mathdef 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
我们可以直接使用sage
的ecm.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 irootfrom Crypto.Util.number import long_to_bytese = n = c = 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 mathfrom Crypto.Util.number import long_to_bytesdef 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 if (e * d - 1 ) % k != 0 : continue phi = (e * d - 1 ) // k s = n - phi + 1 discr = s * s - 4 * n if discr >= 0 and is_perfect_square(discr): t = math.isqrt(discr) 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" )
共模攻击 适用情况:同一条明文消息被用相同的模数但不同的指数加密多次 。
共模攻击成立必须满足以下几个条件:
使用 RSA 加密的两个密文:
两个密文 $c_1$ 和 $c_1$ 是使用相同的模数 n ,但使用了不同的指数 e₁ 和 e₂ 加密得到的。
$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_bytesfrom math import gcdn = e1 = e2 = c1 = c2 = 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))