本篇文章总结了我对RSA算法的理解和设计,并在后文对优化运行效率的方法做了对比分析。
一、RSA算法简介
密码学是研究如何隐密地传递信息的学科,它被认为是数学和计算机科学的分支,和信息论也密切相关。在很久之前的传统密码学中,使用的都是对称加密算法,即使用的密钥只有一个,发收信双方都使用这个密钥对数据进行加密和解密。而现在使用的多为非对称加密算法。
RSA算法是一种著名的非对称加密算法,所谓非对称加密就是说用来加密的密钥和用来解密的密钥是不同的。用来加密的为公钥,是公共的数据,而负责解密的为私钥,是不公开的。利用这种信息不对称的手段,使得该类算法安全性很高,非对称加密算法的大致流程如图1-1。
图1-1非对称加密
其中的公钥和私钥其实就是一组数字,其二进制位长度可以是1024位或者2048位,长度越长其加密强度越大,因为破解时的计算量会随密钥长度指数级的上升。而作为一种非对称加密算法,RSA是目前最有影响力和最常用的公钥加密算法,它能够抵抗到目前为止已知的绝大多数密码攻击,已被ISO推荐为公钥数据加密标准。RSA算法的总体流程如图1-2所示。
图1-2 RSA算法流程
RSA之所以难以被破解的主要原理为:计算两个大素数相乘是容易的,但将计算的结果逆向分解为两个素数是困难的。所以,素数的产生和计算是RSA算法中的一个重要环节。对于为什么破解RSA需要将分解因子,我在后文中进行阐述。
二、RSA算法的实现
1.产生私钥和公钥
在RSA算法的实现过程中,总体可以分为两大部分,就是产生公钥、私钥和加密解密。我对RSA产生共钥、私钥的流程总结为图2-1。
图2-1 RSA产生密钥流程
由图2-1可以发现,若想求得私钥中的d,在已知e和n的情况下,需要求得n的欧拉函数值φ(n),则就需要求得p和q。上文已经说过,在已知结果的情况下,逆向求得两个大素数p和q是非常困难的,这就是RSA可靠的原因。
后面对图2-1具体每一步的详细实现进行阐述。
(1)找到两个较大的素数和乘积n
找到两个大素数是整个RSA算法中最慢的步骤之一,这里先采用的方法是不断的生成随机数,直到找到符合要求的素数为止。
首先,需要能够判断一个数是否为素数,最简单的方法就是看这个数是否有除了1和它本身以外的其他因数,即试除法。检测素数的代码段如下。
bool IsPrime(long b)
{
for (long i = 2; i < sqrt(b); i++)
if (b%i == 0)return false;
return true;
}
能够检测素数以后,就可以进行较大素数的生成。为了达到“较大“的要求,我只对大于100的数进行筛选,实现的主要代码如下。为防止过慢,这里的GetRandom()只生成小于200的随机数。
while (1){
while (1){
p = GetRandom(); //得到随机数
if (s1 > 100 && IsPrime(s1))break; //条件为大于100的素数
}
while (1){
q = GetRandom(); //得到第二个随机数
if (s2 > 100 && IsPrime(s2))break; //条件为大于100的素数
}
if (p != q){ //两个数不能相同
cout << "p=" << p << ",q=" << q << endl;//得到两个大素数
break;
}}
得到两个素数p和q后,相乘即得到n。有关此处对运行速度的改进在后文中说明。
(2)计算n的欧拉函数
对于一个正整数n,欧拉函数是小于n的正整数中与n互质的数的数目。如果此处的 n = p * q,p 与 q 均为素数,则可由如下推导得到欧拉函数值。
所以,因为p和q都是素数,这里只需要用p-1和q-1相乘就能得到n的欧拉函数值。
(3)得到整数e
为了得到一个与φ(n)互质的整数e,首先需要能够计算两个数的最大公约数,最大公约数为1即为互质。我使用辗转取余法求最大公约数。主要代码如下。
int divisor(int x, int y) {
int temp;
while (y){
temp = x;
x = y;
y = temp%y;
}
return x;
}
能够判断两个数是否互质以后,通过循环随机取数,找到符合条件的e,代码如下。为了加快速度,这里的GetRandom()仍然取的是200以下的随机数。
while (1)
{
e = GetRandom();
if (divisor(e, eular) == 1 && e > 1 && e < eular)
break;
}
(4)得到整数d
为了得到一个整数d,使得e*d 除以φ(n) 的余数为 1,可以从1开始往上寻找一个整数i,当用整数i计算(φ(n)*i + 1) / e的结果为整数时,则这个结果就满足d的要求,可将该值赋给d,这样也能保证找到的d尽量的小,加快程序速度。主要代码段如下。
for (int i = 1;; i++){
d = (float)(eular*i + 1)/e;
if (d - (int)d == 0)break; //当结果为整数,找到符合要求的d
}
至此,就获得了公钥(n,e)和私钥(n,d)。
2.加密和解密
在RSA中,加密和解密的过程使用了费尔马小定理的两种等价的描述。简单来说,对明文m进行加密求得密文c的公式如下。
公式看起来不难,但在具体实现中遇到了一些问题。因为e的值通常是三位数,导致a^e的数值太大,就算是使用C++中long long类型也会造成溢出,所以我根据求余运算的分配律进行等价的迭代计算,分配律公式如下。
本问题中a和b的值是相同的,主要代码如下。这里的text即为保存明文的数组。
cout << "加密后的密文为:";
for (int i = 0; i < text.size(); i++){
long temp = text[i] % n; //保存a对n求余的值
for (int j = 0; j < e - 1; j++){ //迭代求余
text[i] = ( temp * (text[i] % n)) % n;
}
cout << text[i] << " ";
}
这样,就把一次性的幂运算分成了多次较小数的求余计算,避免了出现溢出的情况。对密文c,解密求得明文m的公式如下。
同样利用分配律进行迭代计算,主要代码与上文的加密类似,text为保存密文的数组,代码如下所示。
cout << "解密后的明文为:";
for (int i = 0; i < text.size(); i++){
long temp = text[i] % n; //保存a对n求余的值
for (int j = 0; j < d - 1; j++){ //迭代求余
text[i] = ( temp * (text[i] % n)) % n;
}
cout << text[i] << " ";
}
三、RSA算法的优化
在实际测试中,我发现程序运行的非常慢,尤其是产生素数的部分,这里慢的最主要的原因是素数的检测太慢。本节就对如何加快素数的生成进行研究。
1.限制随机数范围
在生成素数时,我之前使用的方法是随机产生一个数然后再判断是否其达到条件,导致效率很慢。既然在筛选的条件之一是保证其“较大”,还不如直接生成一个较大区间内的随机数,比如我设置的是100到200之间的随机数,这样避免了很多无效的生成。改变后的代码如下。
long int GetRandom(){
srand((int)time(0));
return 100 + rand() % 100;
}
之后,对其筛选条件进行更改,只需判断是否为素数。更改后程序的运行速度显著提升。
2.米勒-拉宾素数测试
在网上查询资料后,我发现一种叫米勒-拉宾的快速测试素数的方法,这是一种基于概率的非确定性素数判定法,但是正确率非常高,我感觉应该可以放到RSA算法当中。该算法的步骤如下。
1.首先确定几个基底a,范围在[2,n-1]。
2.当有一个数n,先判断是不是2,3的倍数,是则不为素数。若不是则为一个奇数,继续。
3.令n-1=(2^r)*d,取不同的a,对r从0到最大,满足ad≡1(mod n),此处分以下情况。
(a)d为奇数,若同余1,可能为质数
(b)d为偶数,若同余-1,可能为质数
(c)循环后不符合上述情况,必为合数
有关该算法的正确性的证明比较复杂,需要用到费马小定理等数论的理论,这里只是拿来应用即可。基地a采用2,7,61就可计算至2^32数量级,完全够用。代码如下。
bool MRprime(int a, int n){
int r = 0, d = n - 1;
if (!(n%a)) return false; //倍数必为合数
while (!(d & 1)) //找到奇数
{
d >>= 1;
r++;
}
long long k = pow(a, d, n);
if (k == 1) return true; //同余1
for (int i = 0; i < r; i++, k = k*k%n)
if (k == n - 1) return true; //同余-1
return false;
}
bool CheckPrime(int n)
{
if (n == 2 || n == 3 || n == 7 || n == 61) return true;
if (!(n & 1) || n % 3 == 0) return false;
if (MRprime(2, n) && MRprime(7, n) && MRprime(61, n)) return true;
return false;
}
使用米勒-拉宾素数测试代替试除法,程序的运行效率会有所提高。
3.列表选择法
因为本文的程序中的大素数仅仅为在一定区间内的三位数,采用列表选择法肯定是最有效的,通过网上查找资料,得到100到400之间的素数如下。
101、103、107、109、113、127、131、137、139、149、151、157、163、167、173、179、181、191、193、197、199、211、223、227、229、233、239、241、251、257、263、269、271、277、281、283、293、307、311、313、317、331、337、347、349、353、359、367、373、379、383、389、397
在生成素数时,在以上列表中随机取一个数即可,直接省去了检测。可以预见这种方法肯定是最快的,而且区间设置再扩大几倍也基本没有影响。
四、测试与分析
1.基本功能测试
对明文:123,42,655,367,423,768,523,167,5,21,111,785,903,33,214,54,467,22进行加密和解密测试,得到结果如图4-1。
图4-1 RSA基本功能测试
由图4-1可见,程序的基本功能已经完成。
2. 不同优化方法的对比分析
本节分别对原始算法、限制随机数范围、限制随机数范围同时使用米勒-拉宾测试、列表选择法四种不同的方法进行十次计时测试,并进行对比分析。
四种测试结果如下表。(单位:秒)
实验次数 |
原始方法 |
限制随机 数范围 |
米勒-拉宾测试 |
列表选择法 |
1 |
18.30 |
16.88 |
5.97 |
2.55 |
2 |
35.91 |
11.61 |
5.56 |
2.43 |
3 |
38.79 |
12.317 |
13.80 |
0.87 |
4 |
36.51 |
12.15 |
5.68 |
1.56 |
5 |
89.62 |
7.15 |
13.41 |
1.98 |
6 |
37.61 |
18.98 |
20.33 |
3.79 |
7 |
25.39 |
9.31 |
7.91 |
2.13 |
8 |
26.74 |
2.812 |
11.98 |
2.51 |
9 |
29.42 |
10.74 |
18.89 |
2.90 |
10 |
27.72 |
8.656 |
3.22 |
2.44 |
平均时间 |
36.60 |
11.06 |
10.68 |
2.32 |
可以看到原始的方法是非常慢的,限制随机数范围可以有效提高速度,而使用米勒-拉宾测试并没有起到太大的作用,可能只能稍微的提高速度,列表选择法就像预测的一样,是最快的方法。
但是,由RSA的机制可以想到,如果限制了随机数的范围,则生成素数的范围也被限制,这样破译密钥的复杂度也降低了,因为分解因子时不再盲目,而是可以有范围的穷举。所以我认为这种降低可靠性的方法肯定是不能正式使用的。而用素数列表直接选择素数的方法在要生成1024或2048(二进制)位数的情况下也是不现实的,需要存储数据的容量太大。而米勒-拉宾测试法虽然在本文的实验中看起来效果不明显,但对于大型数据应该具有较好的优势,因为当数据的量级变大时,用试除法的所花的时间是指数级上升的,而米勒-拉宾测试受到的影响相较起来较小。这里因为设备原因,我无法对大型数据进行测试和对比,考虑到本文针对的是优化方法的内在机制进行分析,应该有较强的合理性。