数据加密--详解 RSA加密算法 原理与实现
RSA算法简介
RSA是最流行的非对称加密算法之一。也被称为公钥加密。它是由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)在1977年一起提出的。当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。
RSA是非对称的,也就是用来加密的密钥和用来解密的密钥不是同一个。
和DES一样的是,RSA也是分组加密算法,不同的是分组大小可以根据密钥的大小而改变。如果加密的数据不是分组大小的整数倍,则会根据具体的应用方式增加额外的填充位。
RSA作为一种非对称的加密算法,其中很重要的一特点是当数据在网络中传输时,用来加密数据的密钥并不需要也和数据一起传送。因此,这就减少了密钥泄露的可能性。RSA在不允许加密方解密数据时也很有用,加密的一方使用一个密钥,称为公钥,解密的一方使用另一个密钥,称为私钥,私钥需要保持其私有性。
RSA被认为是非常安全的,不过计算速度要比DES慢很多。同DES一样,其安全性也从未被证明过,但想攻破RSA算法涉及的大数(至少200位的大数)的因子分解是一个极其困难的问题。所以,由于缺乏解决大数的因子分解的有效方法,因此,可以推测出目前没有有效的办法可以破解RSA。
RSA算法基于的原理,基本上来说,加密和解密数据围绕着模幂运算,这是取模计算中的一种。取模计算是整数计算中的一种常见形式。x mod n的结果就是x / n的余数。比如,40 mod 13 = 1,因为40 / 13 = 3,余数为1。模幂运算就是计算ab mod n的过程。
计算公钥和私钥
RSA中的公钥和私钥需要结合在一起工作。公钥用来对数据块加密,之后 ,只有对应的私钥才能用来解密。生成密钥时,需要遵循几个步骤以确保公钥和私钥的这种关系能够正常工作。这些步骤也确保没有实际方法能够从一个密钥推出另一个。
开始前,首先要选择两个大的素数,记为p和q。根据当今求解大数因子的技术水平,这两个数应该至少有200位,这们在实践中才可以认为是安全的。
然后,开始计算n:
n = pq
接下来,选择一个小的奇数e,它将成为公钥的一部分。选择e最需要考虑的重点是它与(p-1)(q-1)不能有相同的因子。换句话说,e与(p-1)(q-1)是互为素数关系的。比如,如果p=11而q=19,那么n=11 X 19=209。这里选择e=17,因为(p-1)(q-1)=10 X 18 =180,而17和180没有相同的因子。通常选择3、17、65、537作为e的值。使用这些值不会对RSA的安全性造成影响,因为解密数据还需要用到私钥。
一旦为e选择了一个值,接下来开始计算相对应的值d,d将成为私钥的一部分。d的值就是计算e的倒数对(p-1)(q-1)的取模结果,公式如下:
d = e-1 mod (p-1)(q-1)
这里d和e是模乘法逆元的关系。
思考一下这个问题:当d为多少时可以满足ed mod (p-1)(q-1) = 1 ?比如在等式 17d mod 180 = 1中,d的一个可能值是53。其他的可能值是233、413、593等。在实践中,可以利用欧几里德算法来计算模乘法逆元。这里就不再展开。
现在有了e和d的值,将(e,n)作为公钥P,将(d,n)作为私钥S并保持其不可见。表示为:
P = (e,n) , S = (d,n)
加密方使用P来加密数据,解密方使用S来解密。为了防止就算有人知道了P也无法推算出S,必须保证p和q的值绝对不能暴露。
P和S结合在一起提供的安全性来自于一个事实,那就是乘法是一种很好的单向函数。
单向函数是加密技术的基础。简单的说,单向函数就是在一个方向上能够很容易算出结果,但反向推导则是不切实际的。比如,在RSA算法中,计算p和q的成绩是一种单向函数,因为尽管计算p和q的成绩很容易,但将n反向因子分解为p和q则是极其耗时的。这里,选择的p和q的值要足够大才可以。
计算P和S的步骤起源于欧拉函数中的一些有趣性质。特别是,这些性质允许对模幂运算做一些有用的操作。
欧拉函数记为φ(n),定义所有小于n的正整数里和n互素的整数的个数。
只有当两个整数的唯一公因子为1时,才说这两个整数是互素的。例如,φ(8)=4,因为一共只用4个比8小的整数是互素的,它们是1,3,5,7。
欧拉方程有两个性质对RSA算法来说是特别重要的。
第一,当n是素数时,φ(n)=n-1。这是由于n的唯一因子是1和n,因此,n与之前的所有n-1个正整数都是互素的。
另一个有趣的性质是对于任意小于n且与n互素的正整数a,都有aφ(n) mod n = 1。例如,14 mod 8 = 1, 34 mod 8 = 1, 54 mod 8 = 1, 74 mod 8 = 1。对上述方程两边都乘以a,得到:
(a)(aφ(n) mod n)=a,或者aφ(n)+1 mod n = a
因此,可以得到15 mod 8 = 1, 35 mod 8 = 3, 55 mod 8 = 5, 75 mod 8 = 7。
调整之后得到的等式非常强大。因为对于某些等式c = me mod n,该等于可以让我们找出一个d的值,使得cd mod n = m。
这就是RSA算法中允许加密数据,之后再解密回原文的恒等式。可以按照如下方式表示:
cd mod n = (me)d mod n = med mod n = mφ(n)+1 mod n = m mod n
欧拉函数和指数间的关系保证了加密的任意数据都能够唯一地解密回来。为了找出d的值,解方程d = e-1 φ(n) +1。不巧的是,对于方程d = e-1φ(n)+1不一定总是有整数解。为了解决这种问题,转而计算
d mod φ(n)的值。换句话说,d = (e-1 φ(n) + 1) mod φ(n),可以简化为:
d = e-1 mod φ(n)
我们可以得到这样的简化形式,因为(φ(n)+1) mod φ(n) = (φ(n)+1) – φ(n) = 1。可以用任意的正整数替代φ(n)来证明等式成立。注意这个方程式同之前计算密钥的过程中得出d的推导式之间的相似之处。这提供了一种通过e和n来计算d的方法。当然了,e和n是公开的,对于攻击者来说是事先可知的,因此就有人问,这难道不是给了攻击者相同的机会来计算出私钥吗?讨论到这里,是时候来探讨一下RSA算法安全性保障的由来了。
RSA算法的安全性保障来自一个重要的事实,那就是欧拉函数是乘法性质的。这意味着如果p和q是互素的,那么有φ(pq)=φ(p)φ(q)。因此,如果有两个素数p和q,且n=p*q,则φ(n)=(p-1)(q-1),而且最重要的是:
d = e-1 mod (p-1)(q-1)
因此,尽管攻击者可能知道了e和n的值,为了计算出d必须知道φ(n),而这又必须同时得到p和q的值才能办到。由于p和q是不可知的,因此攻击者只能计算n的因子,只要给出的p和q的值足够大,这就是一个相当耗费时间的过程。
加密和解密数据分组
要使用RSA算法对数据进行加密和解密,首先要确定分组的大小。为了实现这一步,必须确保该分组可以保存的最大数值要小于n的位数。比如,如果p和q都是200位数字的素数,则n的结果将小于400位。因而,所选择的分组所能保存的最大值应该要以是接近于400。在实践中,通常选择的位数都是比n小的2的整数次幂。比如,如果n是209,要选择的分组大小就是7位,因为27 = 128比209小,但28 = 256又大于209。
要从缓冲区M中加密第(i)组明文Mi ,使用公钥(e,n)来获取M的数值,对其求e次幂,然后再对n取模。这将产生一组密文Ci。对n的取模操作确保了Ci将和明文的分组大小保持一致。因而,要加密明文分组有:
Ci = Mie mod n
之前提到过,欧拉函数是采用幂模运算来加密数据的基础,根据欧拉函数及其推导式,能够将密文解密回原文。
要对缓冲区中C中的第(i)组密文进行解密,使用私钥(d,n)来获取Ci的数值部分,对其求d次幂,然后再对n取模。这将产生一组明文Mi。因此,要解密密文分组有:
Mi = Cid mod n
RSA的接口定义
rsa_encipher
void rsa_encipher(Huge plaintext, Huge *ciphertext, RsaPubKey pubkey);
返回值:无
描述:采用RSA算法来加密由plaintext所指定的明文分组。
pubkey是所指定的公钥(e,n),其类型为结构体RsaPubKey。
ciphertext是返回的同plaintext同样大小的密文分组。由调用者负责管理ciphertext所关联的存储空间。要加密大段的数据,可以按照前面介绍的分组加密模式来调用rsa_encipher。
复杂度:O(1)
rsa_decipher
void rsa_decipher(Huge ciphertext, Huge *plaintext, RsaPriKey prikey)
返回值:无
描述:采用RSA算法来解密由ciphertext所指定的密文分组。
prikey是所指定的私钥(d,n),其类型为结构体RsaPriKey。
plaintext是返回的同ciphertext同样大小的明文分组。由调用者负责管理plaintext所关联的存储空间。要解密大段的数据,可以按照前面介绍的分组加密模式来调用rsa_decipher。
复杂度:O(1)
RSA算法的实现与分析
因为RSA加密算法需要的只不过是计算ab mod n,所以基本的实现是比较简单的。关键的是计算幂模的函数。
但是,要使RSA的安全性得到保障,必须使用很大的整数,这就使得事情变得复杂了。特别是,所有的计算中使用到的整数位数必须是密钥位数的2倍(稍后将在幂模计算中看到)。因此,如果密钥是一个200位的整数,就需要一个抽象数据类型来支持至少400位的整数。
关于大数运算已经有一些可用的函数库。这里不再提供具体的实现。在数据加密头文件的示例中,定义了数据类型Huge,在安全的实现中,可以为Huge类型指定typedef别名以支持所选择的大整数抽象数据类型。其他的需求就只剩下替换整数计算中的运算符为Huge类型所支持的操作。为了达到说明的目的,在这里的实现中Huge类型用typedef定义为unsigned long,这种C语言内置的类型通常只能提供10位十进制数的支持。这意味着稍后的实现示例给出的实现只能支持最多5位整数的密钥。因此,虽然示例中的实现是可用的,但如果不把Huge重定义为大数类型,这个实现就不是安全的。
rsa_encipher
rsa_encipher函数采用RSA算法将明文分组加密。
通过调用函数modexp来计算ab mod n的值,这里a代表明文分组,b和n代表公钥的e和n成员。为了提高执行效率,modexp使用称为二进制平方-乘的算法来计算模幂。
二进制平方-乘算法避免了当a和b都很大时计算ab时会出现的超大中间值。比如,假设当a、b和n都是包含200位数字的超大整数,计算ab mod n,其结果是一个40 000位的整数对200位的整数取模!因为最终得到的结果就是一个200位的整数,那么这里的目的就是要避免出现40 000位的整数的中间值。
用二进制平方-乘算法计算ab mod n,主要是多个开平方运算的结果(如下图)。首先,将b表示为二进制形式,然后从右边的位开始处理。对于b中的每一位,求出a的平方对n取模的结果,并将这个结果赋给a。每当遇到b中为1的位时,就将当前的a值乘上另一个寄存器y(初始值为1),并将结果再赋给y。一旦迭代至b的最高有效位,y的值就是ab mod n的结果。在整个计算过程中,产生的最大值是a2。因此,如果a是一个包含200位数字的整数,就不用处理大于400位的数字了。这相对于前面提到过的包含40 000位数字的整数来说已经是很大的优化了。下图中的阴影部分展示了计算511 mod 53 = 48 828 125 mod 53 = 20的过程。在这个计算过程中,相比511 = 48 828 125,所处理的最大数值只有422 = 1764。
图示:采用二进制平方-乘算法来计算模幂
rsa_encipher的时间复杂度为O(1),因为加密一个明文分组的所有步骤都能在恒定的时间内完成。由于分组的大小是固定的,因此modexp中的循环执行时间也是恒定的。
rsa_decipher
rsa_decipher函数采用RSA算法将密文分组进行解密。
该操作通过调用modexp来解密。modexp计算ab mod n的结果,这里a是密文分组,b和n代表私钥成员d和n。处理过程同rsa_decipher中描述的一样。
rsa_decipher的时间复杂度为O(1),因为解密密文分组的所有步骤都可以在恒定的时间内完成。由于分组大小固定,因此,modexp中的循环执行时间也是恒定的。
示例:数据加密的头文件代码
/*encrypt.h*/ #ifndef ENCRYPT_H #define ENCRYPT_H /*在一个安全实现中,Huge 最少要400位10进制数字*/ typedef unsigned long Huge; /*为RSA公钥定义一个数据结构*/ typedef struct RsaPubKey_ { Huge e; Huge n; }RsaPubkey; /*为RSA私钥定义一个数据结构*/ typedef struct RsaPriKey_ { Huge d; Huge n; }RsaPriKey; /*函数声明*/ void des_encipher(const unsigned char *plaintext, unsigned char *ciphertext, const unsigned char *key); void des_decipher(const unsigned char *ciphertext, unsigned char *plaintext, const unsigned char *key); void rsa_encipher(Huge plaintext, Huge *ciphertext, RsaPubKey pubkey); void rsa_decipher(Huge ciphertext,Huge *plaintext, RsaPriKey prikey); #endif // ENCRYPT_H
示例:RSA算法的实现
/*rsa.c RSA算法实现*/ #include "encrypt.h" /*modexp 二进制平方乘算法函数*/ static Huge modexp(Huge a, Huge b, Huge n) { Huge y; /*使用二进制平方乘法计算 pow(a,b) % n*/ y=1; while(b != 0) { /*对于b中的每个1,累加y*/ if(b & 1) y = (y*a) % n; /*对于b中的每一位,计算a的平方*/ a = (a*a) % n; /*准备b中的下一位*/ b = b>>1; } return y; } /*rsa_encipher RSA算法加密*/ void rsa_encipher(Huge plaintext, Huge *ciphertext, RsaPubKey pubkey) { *ciphertext = modexp(plaintext, pubkey.e, pubkey.n); return; } /*rsa_decipher RSA算法解密*/ void rsa_decipher(Huge ciphertext, Huge *plaintext, RsaPriKey prikey) { *plaintext = modexp(ciphertext, prikey.d, prikdy.n); return; }