RSA算法的详细设计(C++)及不同优化策略的比较

本篇文章总结了我对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(二进制)位数的情况下也是不现实的,需要存储数据的容量太大。而米勒-拉宾测试法虽然在本文的实验中看起来效果不明显,但对于大型数据应该具有较好的优势,因为当数据的量级变大时,用试除法的所花的时间是指数级上升的,而米勒-拉宾测试受到的影响相较起来较小。这里因为设备原因,我无法对大型数据进行测试和对比,考虑到本文针对的是优化方法的内在机制进行分析,应该有较强的合理性。

猜你喜欢

转载自blog.csdn.net/qq_37553152/article/details/94652735