RSA的大数运算

余年寄山水
659次浏览
2021年01月19日 21:34
最佳经验
本文由作者推荐

dnf黄金粉末-说尺子

2021年1月19日发(作者:平亚利)
俺曾经查阅了网上找得到的各种用于实现
RSA
的大数运算库,然而最终还是决定自 己
动手写一个。因为凡是效率高速度快的代码(
crypto++

mira cl

freelip

rsaref
等)
,要么使
用的数据结构过于复杂,
要么编码风格杂乱无章,
俺的水平和耐心都实在是有限,
以 至于无
法读懂这些东西。
而俺读得懂的一些代码,
其实现方式却又过于幼稚,
效率极低速度一塌糊
涂。
俺觉得像俺这样的人不在少数,
于是决心写一个清晰易懂,< br>效率也过得去的东西奉献给
大家。






这个函数库刚做好的时候,生成
1024
位的随机密钥耗时大约
5
分钟,俺认为是可以接
受的。但后来找到一个叫
tE!
的老外用
m iracl
库写的
RsaTools
,发现其生成
1024
位的密钥
耗时不超过三秒钟!
于是俺针对俺的代码开始了艰苦的优化工作,
希望能达到甚至超过 这一
水平
?
一周之后
1024
位密钥的平均生成时间已经降至
5
秒左右,但是单单依靠优化代码来
进一步提高速度也非常困难了。于是俺开始借助金山词 霸来查阅能够通过
google
找到的一
切与
RSA
算法相关的论 文,但是网上关于
RSA
算法的论述绝大多数都是用于硬件实现的,
将其算法流程用软 件设计语言来实现极其繁琐
?
而且俺发现这样做下去俺只会离自己的初衷
越来越远:

俺的代码将不再清晰易懂
?
所以俺一度准备放弃
?





准备放弃之后,
心态平静了许多,
再回头去看那些原来不太能够理解的
RSA
算法原理,
却发现其实也不是那么高深莫测,不急不躁地慢慢看,慢慢想,突然就一下子全明白 了。一
番改进之后,
现在这个版本的函数库同样具有非常简单而清晰的结构,
速度也不 算慢,
生成
1024
位的密钥在俺
PIII
900
的笔记本上平均耗时不超过两秒。程序使用
C++
编写,可在
VC6.0
下直接编译通过,希望大家喜欢。如果发现
Bug
或者有好的修改建议,俺将非常感
谢您能够给俺一个
Mail







最后,感谢看雪论坛,感谢雪兄多次热心相助,俺在此学到了很多知识,当然

还要乘机拍拍马屁,感谢俺家甜甜的支持!





afanty@





RSA
原理
:

选取两个不同的大素数
p

q
,并计算
N=p*q
选取小素数
d
,并计算
e
,使
d*e % (p-1)(q-1)=1

对于任意
A



B=A**d % N

A=B**e % N

可 见
d

e
形成了非对称秘钥关系,加密者用公钥
d
加密,解 密者可用私钥
e
解密,第三者
即使拦截了密文
B
、公钥
d< br>和
N
,在不知道
p

q
的前提下,无法推算出
e
,从而无法获得
明文
A
。当
N
取非常大的值时,将其因 式分解成
p

q
是非常困难的,例如当
N

102 4 bit
时,据分析,需动用价值数千万美金的大型计算机系统并耗费一年的时间
?



RSA
密钥的选取和加解密过程都非常简洁,在算法上主要要实现四个问题:


1 ?
如何处理大数运算

2
、如何求解同余方程

XY % M = 1
3 ?
如何快速进行模幂运算

4 ?
如何获取大素数


实际上,
在实现
RSA
算 法的过程中大家会发现后三个问题不是各自独立的,
它们互有关联,
环环相套,相信届时你会意 识到:
RSA
算法是一种“优美”的算法!







RSA
依赖大数运算,目前主流
RSA
算法都建立在
1024
位的大数运算之上。而大多数
的编译器只能支持到
64
位的整数运算,即我们在运算中所使用的整数必须小于等于
64
位,
即:0xffffffffffffffff
,也就是
18446744
,这远远达不 到
RSA
的需要,于是需要
专门建立大数运算库来解决这一问题。






最简单的办法是将大数当作数组进行处理,数组 的各元素也就是大数每一位上的数字,
通常采用最容易理解的十进制数字
0

9
。然后对“数字数组”编写加减乘除函数。但是这
样做效率很低,
因为二进制为1024
位的大数在十进制下也有三百多位,
对于任何一种运算,
都需要在两个有 数百个元素的数组空间上多次重循环,
还需要许多额外的空间存放计算的进
退位标志及中间结果 。
另外,
对于某些特殊的运算而言,
采用二进制会使计算过程大大简化,
而这 种大数表示方法转化成二进制显然非常麻烦,
所以在某些实例中则干脆采用了二进制数
组的方法 来记录大数,当然这样效率就更低了
?





一个有效的改进方法是将大数表示为一个
n
进制数组,
对于目前的
32
位系统而言
n

以取值为
2

32
次方,即

0x1 00000000
,假如将一个二进制为
1024
位的大数转化成
0x100 00000
进制,
就变成了
32
位,
而每一位的取值范围不再是二进 制的
0

1
或十进制的
0

9

而是
0-0xffffffff

我们正好可以用一个
32
位的DWORD
(如:
无符号长整数,
unsigned
long


类型来表示该值。所以
1024
位的大数就变 成一个含有
32
个元素的

DWORD
数组,
而针对

DWORD
数组进行各种运算所 需的循环规模至多
32
次而已。而且
0x100000000

制与二进制,对于计算机来说,几乎是一回事,转换非常容易
?





例如大数
18446744
,等于

0xffffffff
ffffffff
,就相当于十进制的
99
:有两
位,每位都是
0 xffffffff
。而
18446744
等于
0x0000 00000000
,就
相当于十进制的
100
:有三位,第一位是
1
,其它两位都是
0
,如此等等。在实际应用中,
“数字数组”的排列顺序采 用低位在前高位在后的方式,这样,大数
A
就可以方便地用数
学表达式来表示其值:

A=Sum[i=0 to n] (A[i]*r**i)

r=0x100000000

0<=A[i]< r
其中
Sum
表示求和,
A[i]
表示用以记录
A
的数组的第
i
个元素,
**
表示乘方。






任何整数运算最终都能分解成数字与数字之间的运算,在
0x100000000
进 制下其“数
字”最大达到
0xffffffff
,其数字与数字之间的运算,结果也必 然超出了目前
32
位系统的字
长。在
VC++
中,存在一个
__int64
类型可以处理
64
位的整数,所以不用担心这一问题,而
在 其它编译系统中如果不存在
64
位整形,就需要采用更小的进制方式来存储大数,例如
16
位的
WORD
类型可以用来表示
0x10000
进制。
但效率更高的办法还是采用
32
位的

DWORD
类型,只不过将
0x100000000
进制改成
0x 40000000
进制,这样两个数字进行四则运算的最
大结果为

0x3fffffff * 0x3fffffff
,小于
0xffffffffff ffff
,可以用一个双精度浮点类型(
double

52
位有效 数字)来储存这一中间结果,只是不能简单地用高位低位来将中间结果拆分成两
个“数字”
。< br>

设有大数
A

B

C
,其中< br>A>=B


A=Sum[i=0 to p](A[i]*r**i)
B=Sum[i=0 to q](B[i]*r**i)
C=Sum[i=0 to n](C[i]*r**i)
r=0x100000000

0<=A[i],B[ i],C[i]
p>=q

则当
C=A+B
C=A-B

C=A*B
时,我们都可以通过计算出
C[i]
来 获得
C



C=A+B

显然
C[i]
不总是等于
A[i]+B[i]

因为
A[i]+B[i]
可能
>0xffffffff


C[i]
必须
<=0xf fffffff

这时就需要进位,当然在计算
C[i-1]
时也可能产生了 进位,所以计算
C[i]
时还要加上上次的
进位值。如果用一个
64
位变量
result
来记录和,另一个
32
位变量
carry
来记录进位,则有:





carry = 0




FOR i FROM 0 TO p






result=A[i]+B[i]+carry






C[i]=result%0x100000000






carry=result/0x100000000




IF carry=0 n=p




Else: n = p + 1

C=A-B
,同 理
C[i]
不总是等于
A[i]-B[i]
,因为
A[i]-B[i ]
可能
<0
,而
C[i]
必须
>=0
,这时就需< br>要借位,同样在计算
C[i-1]
时也可能产生了借位,所以计算
C[i]时还要减去上次的借位值
:




carry = 0




FOR i FROM 0 TO p






IF A[i]-B[i]-carry>=0








C[i]=A[i]-B[i]-carry








carry = 0






Else








C[i]=0x100000000+A[i]-B[i]-carry








carry = 1




n = p




WHILE C[n]=0 n=n-1

C=A*B
,首先我们需要观察日常做乘法所用的“竖式计算”过程:















A3


A2


A1


A0
*

















B2


B1


B0
-------------------------------
=










A3B0 A2B0 A1B0 A0B0
+





A3B1 A2B1 A1B1 A0B1
+ A3B2 A2B2 A1B2 A0B2
-------------------------------
=


C5


C4


C3


C2


C1


C0

可以归纳出:
C[i]=Sum[j=0 to q](A[i-j]*B[j])
,其中
i-j
必须
>=0

<=p
。当然这一结论 没有考
虑进位,
虽然计算
A[i-j]*B[j]

Sum
的时候都可能发生进位,
但显然这两种原因产生的进位
可以累加成一个进位值
?
最终可用如下算法完成乘法
:




n = p + q - 1




carry = 0




For i FROM 0 TO n






Result = carry






For j FROM 0 TO q








IF 0<=i-j<=p result=result+A[i-j]*B[j]






C[i]=result%0x100000000






carry=result/0x100000000




IF carry!=0






n = n + 1






c [n] = carry


对于
C=A/B
,由于无法将
B

A
“试商”< br>,我们只能转换成
B[q]

A[p]
的试商来得到一个
近似 值,所以无法直接通过计算
C[i]
来获得
C
,只能一步步逼近
C< br>。由于:






B*(A[p]/B[q]-1)*0x100000000**(p-q)
令:
X=0
,重复
A=A-X*B

X=X+(A[p]/B[q] -1)*0x100000000**(p-q)
,直到
A则:
X=A/B
,且此时的
A=A%B

注意对于任意大数
A*0x100000000**k
,都等价于将
A
的数组中的各元素左移
k
位,不必
计算;同样,
A/0x1000 00000**k
则等价于右移。




RSA
算法中,往往要在已知
A

N
的情况下,求

B
,使得

(A*B)%N=1
。即相当于求解
B

M
都是未知数的二元一次不定方程

A*B-N*M=1
的最小整数解。






而针对不定方程
ax-by=c
的最小整数解,
古今中外都进行过详尽的研 究,
西方有著名的
欧几里德算法,即辗转相除法,中国有秦九韶的“大衍求一术”
。事 实上二元一次不定方程
有整数解的充要条件是
c

a

b< br>的最大公约数。即当
c=1
时,
a

b
必须互质。而 在
RSA

法里由于公钥
d
为素数,素数与任何正整数互质,所以可 以通过欧几里德方程来求解私钥
e?





欧几里德算法是一种递归算法,比较容易理解:






例如:
11x-49y=1
,求
x





a


11 x - 49 y = 1



49%11=5 ->





b


11 x -

5 y = 1



11%5 =1 ->





c





x -

5 y = 1





y=0
代入(
c
)得
x=1





x=1
代入(
b
)得
y=2





y=2
代入(
a
)得
x=9





同理可使用递归算法求得任意

ax-by=1

a

b
互质)
的解。
实 际上通过分析归纳将递归
算法转换成非递归算法就变成了大衍求一术
:




x=0,y=1




While a! = 0






i = y






y = X - y * a / b






X = i






i = a






a=b%a






b = i




IF x<0 x=x+b




RETURN x










模幂运算是
RSA
的核心算法,
最直接地决定了
RSA
算法的性能。
针对快速模幂运算
这一课题,
西方现代数学家提出了大量的解决方案,< br>通常都是先将幂模运算转化为乘模运算
?




例如求
D=C**15 % N
,由于:
a*b % n = (a % n)*(b % n) % n
,所以:









C1 =C*C


% N

=C**2

% N








C2 =C1*C

% N

=C**3

% N








C3 =C2*C2 % N

=C**6

% N








C4 =C3*C

% N

=C**7

% N








C5 =C4*C4 % N

=C**14 % N








C6 =C5*C

% N

=C**15 % N





即:
对于
E=15
的幂模运算可分解为
6
个乘 模运算,
归纳分析以上方法可以发现对于任

E
,都可采用以下算法计算D=C**E % N






d = 1




While e >= 0






IF E%2=0








C=C*C % N








e = e / 2






Else








D=D*C % N








e = e - 1




RETURN D





继续分析会发现,要知道
E
何时能整除

2
,并不需要反复进行减一或除二的操作,只
需验证
E
的二进制各位是
0
还是
1
就可以了,
从左至右或从右至 左验证都可以,
从左至右
会更简洁,设
E=Sum[i=0 to n](E[i]*2**i)

0<=E[i]<=1
,则:

dnf黄金粉末-说尺子


dnf黄金粉末-说尺子


dnf黄金粉末-说尺子


dnf黄金粉末-说尺子


dnf黄金粉末-说尺子


dnf黄金粉末-说尺子


dnf黄金粉末-说尺子


dnf黄金粉末-说尺子