RSA算法是公钥密码学中的重要部分。关于RSA算法的原理部分,阮一峰讲的很清楚,具体可以阅读他的博客:
http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html
http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html
但是,网上很多都是原理的讲解,很难找到一个具体的实现案例。我找了一个Python的源代码,分析源代码加深理解。
代码下载的地址为:
https://pypi.python.org/pypi/rsa
打开源代码,找到key.py,这个文件中实现了RSA算法的核心部分。
找到其中的函数gen_key,源代码如下:
def gen_keys(nbits, getprime_func, accurate=True, exponent=DEFAULT_EXPONENT):
在这个函数里面实现了生成公钥和私钥的方法。
具体流程如下:
(1)生成质数p和q
函数的参数nbits也就是算法原理里面n的位数,可以是1024位或者2048位。这里以1024位为例。然后调用函数find_p_q生成p和q。
(p, q) = find_p_q (nbits/2, getprime_func, accurate)
这个函数生成了nbits/2=512位的质数(其实p、q不是严格为512位,在本文后半部分有解释),这样可以保证p*q为1024位。其中生成p和q的函数是:
p = getprime_func(pbits)
q = getprime_func(qbits)
在prime.py中可以找到getprime(nbits)函数。这个函数的具体实现是:先随机生成一个数字,然后判断是不是质数。
生成随机数的方法如下:
integer = rsa.randnum.read_random_odd_int(nbits)
那么如何判断p和q是不是质数呢,这里用到了Miller Rabin算法,这个算法的实现在prime.py文件中。
miller_rabin_primality_testing(n, k):
关于Miller Rabin算法可以阅读相关资料。
维基百科:https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test
(2)计算e和d
调用函数calculate_keys_custom_exponent(p, q, exponent=exponent)。根据p和q的值生成e和d。其中e的默认值是65537。
(e, d) = calculate_keys_custom_exponent(p, q, exponent=exponent)
当calculate_keys_custom_exponent函数不再出现异常的时候,算法将停止,并返回p,q,e,d。
现在已经生成了p,q,n,e,d,也就是获得了公钥和私钥。下面介绍如何加密和解密。
(3)加密、解密
从原理可以看出,加密的时候需要对明文计算幂,解密时候也需要求幂。加密解密的实现在pkcs1.py中。encrypt函数实现加密,decrypt函数实现解密。
def encrypt(message, pub_key): def decrypt(crypto, priv_key):
在加密函数encrypt中,对加密算法的直接实现是
encrypted = core.encrypt_int(payload, pub_key.e, pub_key.n)
于是,打开core.py,找到encrypt_int函数,最后一行是对求幂的实现。
return pow(message, ekey, n)
这个加密实现直接使用了pow函数,但是在一般的实现中,需要计算高次幂,直接使用pow会导致计算性能问题。
求完幂还需要进行取模的计算,这一步是在下一行代码中,在int2bytes的时候做了取模的计算,代码如下所示:
encrypted = core.encrypt_int(payload, pub_key.e, pub_key.n) block = transform.int2bytes(encrypted, keylength)
计算大整数的大整数次幂是非常麻烦的,上面的求幂计算和取模计算可以合并成一步,直接使用快速求指数模算法。
在RSA实现中,还需要考虑大文件的加密问题,一般是把大文件分割成小文件,分块加密,代码中bigfile.py中有具体实现,就不赘述了。
(4)总结
由源代码可以看出,算法的实际实现比原理稍微复杂一点。在实现时候注意需要考虑以下问题:
(1)大数的存储和运算问题,int型能保存31位二进制数或者9位的十进制数,但是当1024位的时候需要重新定义大数的数据结构。在java里面有一个java.math.BigInteger封装了一个实现,而且其中也实现了大数的四则运算和模反等操作,使得基于BigInteger的RSA实现异常方便。可以通过阅读Java中的BigInteger的源代码理解其实现。BigInteger源代码阅读可以参考这篇博客:
http://www.hollischuang.com/archives/176
(2)质数的生成问题。为了保证质数相乘不能超过1024位,采取的方法是先生成两个512位的质数p和q,这样就可以保证不会溢出。其实在具体实现的时候,并不需要两个质数严格的是512位 ,可以做适当的移位,但是需要保证两个数相乘为1024位。在代码find_p_q函数中,有如下代码
shift = nbits // 16 pbits = nbits + shift qbits = nbits - shift
可以看出,pbits和qbits可以允许进行适当移位,这里shift表示移位512/16=32位,也就是pbits为544位,而qbits为480位。因为默认p>q,所以p的位数多于q的位数。这样的话pbits+qbits仍然为1024位。
(3)质数的分布问题。如果大质数比较稀疏,那么RSA算法就可以通过穷举质数的方法来攻破。但是512位大质数的分布没有想象的那么稀疏,所以能够保证安全性。
在学习理论的时候,如果有不明白的地方,可以找一份源代码阅读,帮助理解。
正如Linus Torvalds所说,Read The Fucking Source Code