1、RSA的由来
1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人 的名字命名,叫做RSA算法。直到现在,RSA算法一直是最广为使用的"非对称加密算法"。毫不夸张地说,只要有计 算机网络的地方,就有RSA算法。
2、RSA的加/解密方式
- 公钥(e,n),私钥(d,n)
- 加密:密文 = (明文^e) mod n
- 解密:明文 = (密文^d) mod n
3、数学推导
注:这里只是简单推导密钥产生原理,不证明公钥加密与私钥解密,加解密证明点这里
1、欧拉函数:欧拉函数是小于x的正整数中与x互质的数的数目
F(m*n) = F(m)*F(n)
若n为质数,则:
F(n) = n - 1
若m ,n都为质数,则:
F(m*n) = F(m)*F(n) = (m - 1)*(n - 1)
2、欧拉定理:若果两个正整数a,n互质,则n的欧拉函数F(n)满足下面的式子:
a ^(F(n)) mod n = 1
3、模反元素(逆元):根据欧拉定理,如果两个正整数a和n互质,那么一定可以找到整数b,使得
(a*b - 1 )mod n = 0
,又或者说a*b mod n = 1
。这时,b就叫做a的模反元素(逆元)。
(a*b) mod n = 1
由于a,n互质,根据欧拉定理和a的模范元素可推出:
b = a^(F(n) - 1)
4、秘钥产生过程
- 随机获取两个相等的质数
p
和q
(这两个质数越大,就越难破解)- 计算
p
和q
的乘积n
- 计算
n
的欧拉函数F(n) = F(p) * F(q) = (p- 1 )*(q - 1)
- 随机选出公钥
e
,条件是1 < e < F(n) 且 e^(F(n)) mod n = 1(e与F(n)互质)
- 计算
e
相对于F(n)
的模反元素d
,使得(e*d) mod F(n) = 1
- 至此,获取到公钥
(e,n)
,私钥(d,n)
- 加密:
密文 = (明文^e) mod n
- 解密:
明文 = (密文^d) mod n
举个栗子:
- 选择
p = 3, q = 11
n = p*q = 33
F(n) = (p - 1)*(q - 1) = 20
- 选择e = 3, 此时e与F(n) 互质
(e*d) mod F(n) =(3*d) mod 20 = 1
,求出d = 7- 公钥(3,33),私钥(7,33)
- 例如对明文数字“5”加密,根据加密公式,
密文=(5^3) mod (33) = 125 % 33 = 26
- 把上述密文解密,
明文=26^7 % 33 = 8031810176 % 33 = 5
但是,做到这些还远远不够,比如:
- 大素数的产生,前面我们只用到了srand()随机数来产生两个素数,这就会使得产生的素数不足够大,违背了RSA算法的初衷,大大降低了数据在加解密过程中的破解难度
- 计算一个数比如
10^1011 % 47
是非常消耗我们的计算资源的,在整个计算过程中最麻烦的就是我们的10^1011
这个 过程,在我们在之后计算指数后,计算的数字不断增大,非常的占用我们的计算资源,并且我们计算的中间过程数字大的恐怖,我们现有的计算机是没有办法记录这么长的数据的,会导致溢出- 模反元素也成为模的逆元,如果使用暴力搜索,求逆元d的时间复杂度为O(n),这对于大数性能是无法忍受的,所以需要更快速的算法。
1、随机大数的产生以及素性检测
boost库中有专门进行大数运算的库cpp_int,它的大数类型有很多种
typedef number<cpp_int_backend<> > cpp_int;// 固定位的无符号大数,从128到1024位:
typedef number<cpp_int_backend<128, 128, unsigned_magnitude, unchecked, void> >
uint128_t;
typedef number<cpp_int_backend<256, 256, unsigned_magnitude, unchecked, void> >
uint256_t;
typedef number<cpp_int_backend<512, 512, unsigned_magnitude, unchecked, void> >
uint512_t;
typedef number<cpp_int_backend<1024, 1024, unsigned_magnitude, unchecked, void> >
uint1024_t;
它的使用方式可普通的内置类型的使用方式完全相同,当然对于大数的初始化,比如普通类型可能无法表示的数字, 可以用字符串进行初始化
#include
#include
using namespace std;
namespace mp = boost::multiprecision;
namespace rp = boost::random;
void test()
{
char* rsa100 =
"12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
012345678901234567890";
char* rsa50 = "12345678901234567890123456789012345678901234567890";
//用字符串初始化大数
mp::cpp_int a(rsa100);
mp::cpp_int min(rsa50);
cout << sizeof(a) << endl;
cout << a << endl;
cout << a / 2 << endl;
cout << a / 100 << endl;
产生大数随机数 boost库中也提供产生大数随机数的接口,它在
random.hpp
文件中
#include
namespace rp = boost::random;
void test()
{
//mt19937:一种随机数产生器
rp::mt19937 gen(time(nullptr));
cout << "random" << endl;
//指定随机数的范围 0 ~ (1<<786)
rp::uniform_int_distribution<mp::cpp_int> dist(0, mp::cpp_int(1)<<768);
cout << dist(gen) << endl;
}
普通的素数检测方法对于大数的效率太慢,大数的素性检测有专门的算法,比如fermat检测,Miller-Rabin等算法。 boost库中实现了Miller-Rabin方法
#include
namespace rp = boost::random;
Rsa::is_prime_bigInt(const mp::int1024_t digit)
{
rp::mt11213b gen(time(nullptr));
if (miller_rabin_test(digit, 25, gen))
{
if (miller_rabin_test((digit - 1) / 2, 25, gen))
{
return true;
}
}
return false;
}
2、快速幂取模运算
同余定理:
(a +/- b) % c = (a % c +/- b % c) % c
(a * b) % c = ((a % c) * (b % c)) % c
(a^b) % c = (a * a * a *……*a) % c
= ((a % c) * (a % c) * (a % c) *……*(a % c)) % c
= ((a % c)^b) % c
模幂运算:
对于一个数b可以展开它的二进制形式:
b = (b0 * 2^0) + (b0 * 2^0) + ……+(bn * 2^n)
所以a^b就可以拆成:
a^b = (a^(b0 * 2^0) * a^(b1 * 2^1) *……* a^(bn * 2^n))
那么假设出去bi为0的二进制位(a^0 =1),保留bi = 1之后:
a^b = (a^(bi * 2^i) *……* a^(bn * 2^n)) (bi!=0)
= (a^(2^i)*……*a^(2^n))
所以也可以推出:
(a^b) % c = (a^(2^i)*……*a^(2^n)) % c
= ((a^(2^i) % c)*……*(a^(2^n)% c)) % c
我们令Ai = a^(2^i) % c
,则:
A0 = a^(2^0) % c
……
Ai = a^(2^i) % c
An = a^(2^n) % c
= (a^(2(n-1)))^2 % c
= (a^(2(n-1)) * a^(2(n-1))) % c
= (A(n-1) * A(n-1)) % c
所以:
(a^b) % c = (a^(2^i)*……*a^(2^n)) % c
= ((a^(2^i) % c)*……*(a^(2^n)% c)) % c
= (Ai * …… * An) % c
算出第一个比特位不为0的Ai,Ai = (a^(2^i)) % c
就可算出A(i+i) = (Ai * Ai) % c
= ((Ai % c) * (Ai % c)) % c
这样每次都mod c,每次结果就永远不会大于c,解决了数据溢出且不占用大量的资源的问题,而且效率非常的高。
3、扩展欧几里得算法
模反元素也成为模的逆元,如果使用暴力搜索,求逆元d的时间复杂度为O(n),这对于大数性能是无法忍受的,所以需要更快速的算法。
欧几里得定理
gcd(a,b) = gcd(b, a%b)
,(a,b)的最大公约数与(a,a%b)的最大公约数相同。
欧几里得算法----碾转相除法
int gcd(int a, int b) {
if(b == 0)
return a;
return gcd(b, a%b);
}
扩展的欧几里得算法
如果a,b的最大公约数为gcd,则一定也可以找到x, y, 使等式
a*x + b*y = gcd
成立。此方程的x,y的解不唯一,可以有无数组解。
我们观察一下等式:
a*x + b*y = gcd
当b = 0时, 等式变成了a*x = gcd,
此时我们返回a的值,a既是所求的最大公约数gcd,故此时x = 1, y可以是任意值
所以,简单的一组解即为(1, 0)。
现在再让我们回头看模的逆元的等式:
(a*b) % n = 1
(a*b)-1为n的倍数, 即 a*b + k*n = 1
这里放入加密算法中对应的即为:e*d + k*F(n) = 1
其中e为加密密钥,d为解密密钥, e与F(n)互质,他们的最大公约数即为1
所以这里的解密密钥d和k就相当于等式的一组解,此解可以用扩展的欧几里得算法求解。
这里我们假设
d = x, k = y, e = a, F(n) = b
因为a,b互质,当a!=0且b!=0时根据扩展欧几里得满足:
a*x + b*y = gcd(a,b)= 1 ①
设a1 = b,b1 = a % b,有gcd(a,b)=gcd(b,a%b)=gcd(a1,b1)=1
当b1 = a % b = 0时,返回最大公约数a1
a1*x1 + b1*y1 = 1-->求得一组解(x1=1,y1=0)
将a1=b,b1=a%b代入上式:
-->b*x1 +(a%b)*y1 = 1
--> b*x1+(a-(a/b)*b)*y1 = 1
-->a*y1+b*(x1-(a/b)*y1) = 1 ②
①式与②式可得:x = y1,y = x1-(a/b)*y1
即在递归的过程中最终的结果可以通过上次递归的结果推导出来,关系如上所示。算法最终求得一组解(x,y)和最大公约数1。
DataType RSA::ExGcd(DataType ekey, DataType orla, DataType& x, DataType& y)
{
//ekey * x + orla * y = 1
//ekey = e, F(n) = orla, x = dkey(e的模反元素d(私钥)),y=orla的整数倍
if (b == 0)
{
x = 1;
y = 0;
return a;
}
DataType gcd = ExGcd(b, a % b, x, y);
DataType x1 = x;
DataType y1 = y;
x = y1;
y = x1 - (a / b) * y1;
return gcd;
}
但是我们一般会找出最小的那个解对应的x,如何求得最小的解呢?其实也简单,只需要x%F(n)即可。如果x为 一个解,则ax + by = 1,则它的通解为
x + k*b
,使a(x + k * b) % b = 1
,这里ax % b = 1, (k * b) % b = 0
,总体结果还 是为1。所以a关于b的逆元x是一个关于b的同余数,所以一定存在一个最小的正整数,它是a关于b的逆元,而最小的肯定在(0,b)之间。最后一个问题,有时候我们得到的解x是一个负数,所以统一处理,可以直接进行操作:(x % b + b) % b
。算法如下
DataType RSA::GetDKey(DataType ekey, DataType orla)//获取解密秘钥d
{
//(e * d) % f(n) = 1 (f(n) = orla)
DataType x, y;
ExGcd(ekey, orla, x, y);//x就是最终求得的秘钥d
return (x % orla + orla) % orla;//变换,让私钥d是一个比较小的值
}
github地址点这里
1、网络通信协议Https
https是一种透过计算机网络进行安全通信的传输协议,他和http协议的区别在于传输的信息是经过加密的。
https协议采用非对称加密和对称加密算法相结合的方式进行加密信息通信,大概机制如下:
- 浏览器发起链接请求。
- 服务器返回公钥。
- 浏览器产生一个对称加密密钥session key。
- 使用服务器的公钥加密此session key。
- 加密的session key 发送给服务器。
- 服务器通过私钥解密session key, 获取明文密码。
- 浏览器和服务器的通信现在通过session key 进行加密,进行安全通信。
如果需要登录远程服务器,一般需要传输密码,但是密码不能明文传输,需要加密,一般采用非对称加密的方式,也就是我们经常使用的ssh方式登录,大致过程如下:
- 远程Server收到Client端用户的登录请求,Server把自己的公钥发给用户。
- Client使用这个公钥,将密码进行加密。
- Client将加密的密码发送给Server端。
- 远程Server用自己的私钥,解密登录密码,然后验证其合法性。
- 若验证结果,给Client相应的响应