在应用层有两个协议分别叫做 HTTP 和 HTTPS,这两个协议想必大家都不陌生,HTTPS 与 HTTP 最大的区别就是 HTTP 发送的数据在网络中发送的信息是以明文的形式发送的,并且是可以被人截获,别人截获到信息后很容易就得到了信息内容。为了让别人不容易拿到消息或者说让别人基本不可能拿到消息,就有了HTTPS,HTTPS 对数据进行了一层加密,让别人即使拿到消息也没办法解密(或者解密的难度变大)。
可以参考:解析HTTPS
通过上面那篇博客我们可以得知,在 HTTPS 中比较重要的或者说加密的关键就是非对称加密。在下面实现的就是非对称加密中的一种算法------RSA算法。
RSA中想要进行加密和解密的过程,最重要的就是求出 公钥 和 私钥,然后通过下面的公式进行加密和解密:
清楚了整个流程之后,接下来整个流程中最难的部分也是最重要的部分就是如何求得公钥和私钥!
公钥和私钥不只是两个数字,公钥和私钥分别是一对数字组成的,也就是我们刚才写的公式中的公钥是(E, N),私钥是(D, N),换言之就是我们如何取求得 E、D、N ?
密钥产生流程:
1、选取素数p1 = 5, p2 = 11;
2、计算两个素数的乘积得到n = p1 * p2 = 55;
3、计算欧拉 φ(n) = (p1 - 1)(p2 - 1) = 4 * 10 = 40;
4、选一个与 φ(n) 互质的数 e = 3;
5、由(d * 3) % 40 = 1;计算得出模反元素 d = 27;
6、得到公钥(3, 55) 私钥(27, 55)
7、加密:对数字 " 3 " 进行加密:由加密公式得:密文 = (3^3)%40 = 27
8、解密:对密文" 27 "进行解密: 由解密公式得:明文 = (27^27)%40 = 3
欧拉函数:
φ ( x ) = x ∏ i = 1 n ( 1 − 1 p i ) φ(x)=x\prod_{i=1}^{n}(1-\frac{1}{p_{i}}) φ(x)=xi=1∏n(1−pi1)
若有两个互质的数:m 和 n,则有
φ ( m n ) = φ ( m ) ∗ φ ( n ) φ(mn) = φ(m)*φ(n) φ(mn)=φ(m)∗φ(n)
对于一个质数来说:
φ ( x ) = x − 1 φ(x) = x - 1 φ(x)=x−1
综上可得
φ ( m n ) = φ ( m ) ∗ φ ( n ) = ( m − 1 ) ∗ ( n − 1 ) φ(mn) = φ(m)*φ(n) = (m-1)*(n-1) φ(mn)=φ(m)∗φ(n)=(m−1)∗(n−1)
long RSA::produce_orla(long prime1, long prime2)
{
return (prime1 - 1) * (prime2 - 1);
}
欧拉定理:
如果两个数 a 和 n 互质,则 n 的欧拉函数 φ(n) 与 a 可得到下面的等式
a φ ( n ) ≡ 1 ( % n ) a^{φ(n)}≡1(\%n) aφ(n)≡1(%n)
这个是数论中关于同余的一个性质。即 ≡ 两边求余的结果相同,可以转换为
a φ ( n ) % n = 1 a^{φ(n)}\%n = 1 aφ(n)%n=1
模反元素(逆元):逆元之优化
由欧拉公式可知有两个互质的数,可以得到欧拉公式,那么一定可以找到一个数 b 使得:
a b ≡ 1 ( % n ) ab≡1(\%n) ab≡1(%n)
进而求得 b = a φ ( n ) − 1 b = a^{φ(n)-1} b=aφ(n)−1
这个 b 就是模的逆元,则有a * b % n = 1
long RSA::produce_dkey(long ekey, long orla)
{
//(dkey * ekey) % orla == 1
//优化:从 orla/ekey 开始找:(5 * x) % 20 == 1; 那么就从 4 开始找,比20大的开始找
long dkey = orla / ekey;
for (;; dkey++){
if ((dkey * ekey) % orla == 1){
break;
}
}
return dkey;
}
在进行加密 / 解密的时候,我们做的其实是一个模幂运算,如果我们要计算这样一个数:7^1080mod1147
所以在这里使用一个快速幂的算法来时间复杂度问题(参考快速幂算法)
快速幂算法解决的最大的问题 - - - 就是减少了幂运算的次数,让这个幂运算的时间复杂度降下来。其次快速幂算法中还会用到一个同余定理- - -解决内存超限的问题。
下面介绍下快速幂算法和同余定理:
快速幂算法:
首先,快速幂算法是减少幂运算的次数,具体原因见下面推理过程:
在我们的快速幂算法中加入同余定理,就可以解决数据大小超出内存限制的问题。
加密/解密的代码如下:
long RSA::ecrept(long msg, long key, long pkey)
{
long msg_des = 1;
//a:需要加密的信息
long a = msg;
long index = key;
//快速幂
while (index){
if (index & 1){ //如果该二进制位是 1 才会将这一位的幂运算结果乘到结果中
msg_des = (msg_des * a) % pkey; //同余定理解决数据溢出
}
index >>= 1;
a = (a * a) % pkey; //快速幂中把下一个二进制位的幂运算结果算出来
}
return msg_des;
}
知道了整个流程之后写出一个RSA的加密过程也就不难了,我的小素数版RSA
经过对RSA的流程了解,我们知道在RSA密钥产生过程一共会产生 6 个值,分别是:
① 素数1
② 素数2
③ 素数1和素数2 的乘积 n
④ 通过欧拉函数计算的结果φ(n)
⑤ 公钥中的 e
⑥ 私钥中的 d
这六个值中只有公钥(e, n)被公布出去,其余数字都是不会公开的,两个素数会销毁,d 不能泄漏
综上所述,这六个值的归宿:①②被销毁,公布公钥(⑤,③),其他值不公布,尤其是⑥保证不传出。
那么知道了公钥(e,n)之后,如何破解出私钥呢?
由密钥产生的流程我们可以知道私钥的产生是在求 e 相对于 φ(n) 的模反元素产生的
公式为: (e*d) mod φ(n) = 1
由上式我们可以得知,要想求出 d,就得知道 e 和 φ(n);
那么 φ(n) 是通过欧拉函数
那么要想求出 φ(n) ,就得知道两个素数是多少。我们当前只知道 e 和 n,所以说求两个素数是多少就要从 n 下手处理,因为 n = prime1 * prime2 。
综上所述,破解私钥的难度就在于对于 n 的因式分解,如果能将 n 因式分解成功,那么两个素数也就相应的可以得到,最终破解出私钥,拿到私钥后,这个RSA非对称加密也就废了。
我们当前使用的这个小数版的RSA加密,是很容易让别人破解出来的,所以增强加密的强度就要从素数的大小来增强。当前世界上公认的难题之一就是因式分解出两个大素数。
而在C++标准库中的数并不是很大,所以我们借助boost库中的cpp_int库,即大数库来增强实现我们的RSA加密流程。
boost库中有从128位到1024位的大数可以供我们使用。boost大数库参考文档
这里我们使用其中1024位的大数:
typedef number<cpp_int_backend<1024, 1024, signed_magnitude, unchecked, void> > int1024_t;
那么产生素数的代码如下:
这里产生随机数以及判断是否是素数用我们普通的处理也就不够快了,因为数字太大,可能判断出来天都黑了。所以我们使用了一个boost库中的产生随机数的接口以及素性检测的接口:大数随机数产生的接口参考文档大随机数以及素性检查参考Miller-Rabin素性检测法 以及 Miller-Rabin详解
素性检测代码:
这样写出来的RSA是相对比较安全的。完整代码参见RSA大素数加强版
在求模的逆元的时候,我们的算法还是一个O(n)的时间复杂度,因为在求解逆元的时候,我们使用的碾转相除法是在一定范围内一个一个试出来的,在小数的时候还能行得通,现在大数版本根本来不及求出来逆元,所以这里我们也可以优化,使用碾转相除法的扩展算法:
扩展的欧几里得算法求私钥的代码如下:
bm::int1024_t RSA_bigPrime::produce_dkey(bm::int1024_t ekey, bm::int1024_t orla)
{
bm::int1024_t d, y;
exgcd(ekey, orla, d, y);
return (d % orla + orla) % orla;
}
bm::int1024_t RSA_bigPrime::exgcd(bm::int1024_t ekey, bm::int1024_t orla
, bm::int1024_t& x, bm::int1024_t& y)
{
if (orla == 0){
x = 1;
y = 0;
return ekey;
}
bm::int1024_t gcd = exgcd(orla, ekey % orla, x, y);
bm::int1024_t x1 = x, y1 = y;
x = y1;
y = x1 - (ekey / orla) * y1;
return gcd;
}