RSA库对比与原理解析
接触的隐私求交项目需要将原始RSA库替换成另外一个,于是调研了一番两者使用方式。这两个库为了安全性,加密时都采用了非确定性加密,即对相同的输入,即使是相同的key也会产生不一致的加密结果。而当前项目需要确定性加密,我将在后面的博客介绍自己重新实现的确定性RSA算法。
0 RSA算法介绍
RSA算法是最著名的非对称加密算法。非对称加密的主要思想是,存在一对密钥:通信双方分别拥有公钥和私钥,他们各自可以用来对原文进行加密/验证和解密/签名。常见使用场景如设置对远程服务器的免密登录,在本地生成私钥,而将公钥提供给服务器认证。
RSA难以破解的原理在于,大整数质因子分解的复杂度太高,因此可以选取两个大素数的乘积作为密钥。
RSA基于欧拉函数和欧拉定理。
欧拉定理
\(a^{\phi(n)} \mod n = 1\)
准备密钥
大素数:p,q
乘积 n = p*q, 欧拉函数 \(\phi(n) = (p-1)(q-1)\)
我们找到一个 整数 e,使得 e 与 \(\phi(n)\) 互质,并求解 d 使得 (de) mod \(\phi(n)\) = 1,即 d 是 e 的逆元。
将 n 和 e 作为公钥,和 d 作为私钥。
加密过程:
对方将明文进行加密,传输密文 cipher = message^e mod n
解密过程:
将密文使用私钥进行解密 message = cipher^d mod n
1 cryptography库
使用流程
- Key生成,产生私钥(用于签名、解密,只支持sign/decrypt方法)=> 产生公钥(用于验证、加密)
- 传输公钥 & 加密/签名信息
- 对方:
- 数字签名:私钥签名,传输签名和原文,对方用【公钥+签名+原文】进行验证
- 解密:对方用公钥加密传输密文,自己用私钥解密密文
出于安全性,提供的加密算法为非确定性加密,由padding参数指定,只支持两种。
Valid paddings for encryption are
OAEP
andPKCS1v15
.OAEP
is the recommended choice for any new protocols or applications,PKCS1v15
should only be used to support legacy protocols.
对于数字签名,提供的padding也只支持两种。
Valid paddings for signatures are
PSS
andPKCS1v15
.PSS
is the recommended choice for any new protocols or applications,PKCS1v15
should only be used to support legacy protocols.
参考加密方法
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
默认采用OAEP方式填充,会填充一段随机字符串,使得串长达到key的长度。对于该接口,产生的ciphertext
不唯一。
2 rsa库
使用流程
使用方法与cryptography类似。
Key生成有更多的参数,poolsize指定并行线程数,accurate=False产生更弱的key加速运算。
提供的加解密/签名为函数式调用,公钥和私钥作为参数。
# 加密和解密
# message from Alice to Bob
crypto = rsa.encrypt(message, bob_pub)
message = rsa.decrypt(crypto, bob_priv)
# 签名和验证
signature = rsa.sign(message, privkey, 'SHA-1')
rsa.verify(message, signature, pubkey)
其他接口
1)大文件加密
对于大文件进行RSA加密比较慢,rsa官方提供的解决方案是,对原文进行对称加密,而对密钥再用RSA加密传输。或者使用VARBLOCK,但这个接口不被推荐。
对较大的文件使用RSA最常见的方法是使用AES或DES3等块密码,用随机密钥加密文件,然后用RSA加密随机密钥。您将把加密的文件和加密的密钥一起发送给收件人。
2)核心模块
rsa提供了对于整数进行RSA的接口,即 rsa.core.encrypt_int 和 rsa.core.decrypt_int。
3 字符处理逻辑
RSA的数学原理是基于大整数的运算,针对字符处理方式,本人研究了两个库的输入输出和rsa库源码分析。
首先,参数key_size 指定生成key中整数 \(n\) 的二进制最大位数,cryptography最少为512,而rsa最少可为 128。注意,\(n\) 随机生成,可能存在:key_size!=n.bit_length()
,实际上:n.bit_length()<=key_size
。
对于加密后的文本字节大小,两者都是固定 key_size // 8。
分析rsa库,加密过程对字符串的处理逻辑为
- padding 到 key_size 长度 message + padding => paded
其中padding开头内容固定为 "\x00\x02",中间填充随机非零值并以 "\x00" 结束。 - 字节类型 paded 转化为 int 类型(无符号类型),采用大端序:payload = int.from_bytes(paded, "big", signed=False)
- 调用 encrypt_int,本质上使用Python内置pow函数,即 encrypted = pow(payload, e, n)
- 加密的大整数 encrypted 转换为字节类型并返回: int.to_bytes,指定长度为 key_size字节数,向上取整。
解密过程是上面的逆过程,不再赘述。需要注意的是,解密要验证padding开头填充内容,padding后第一个 "\x00"后即为原文。
查看加密源代码
def encrypt(message: bytes, pub_key: key.PublicKey) -> bytes:
keylength = common.byte_size(pub_key.n)
padded = _pad_for_encryption(message, keylength)
payload = transform.bytes2int(padded)
encrypted = core.encrypt_int(payload, pub_key.e, pub_key.n)
block = transform.int2bytes(encrypted, keylength)
return block
查看解密源代码
def decrypt(crypto: bytes, priv_key: key.PrivateKey) -> bytes:
blocksize = common.byte_size(priv_key.n)
encrypted = transform.bytes2int(crypto)
decrypted = priv_key.blinded_decrypt(encrypted)
cleartext = transform.int2bytes(decrypted, blocksize)
# ...验证并找原文起始位置
return cleartext[sep_idx + 1 :]
查阅 Python 文档,输入命令 help(pow)
,可以看到这是内置的函数,原生支持求幂取模,也不需要自行实现快速幂算法。经测试,Python的求幂和pow函数复杂度都是O(log exp)的,而不是简单展开做乘法。
pow(base, exp, mod=None)
Equivalent to base ** exp with 2 arguments or base ** exp % mod with 3 arguments
Some types, such as ints, are able to use a more efficient algorithm when
invoked using the three argument form.
以上两个库对padding的最小size都是11字节,也就是说,输入的字符串最大字节数为 \(\lceil key\_size /8 \rceil - 11\)。经测试,由于在第三步中需要对 n 取模,我们还要保证 text 转化为 int 的数值不超过 n,因此,某些key会最大仅支持 \(\lceil key\_size /8 \rceil - 12\) 长度的字符串。
5 时间性能对比
cryptography库的加解密过程要远快于rsa,通过阅读源码,可知 rsa 库是完全基于数学原理使用Python编程实现的,没有经过任何编译、优化。cryptography库的代码经过了封装,可读性较差,有待进一步研究分析。
(未完待续...)