带 C# 私钥加密的 RSA 库





5.00/5 (21投票s)
具有完整 OAEP 填充和私钥加密支持的 RSA 加密库
引言
RSA 是最重要的公钥加密算法之一,它支撑着网络的运行。从安全交易、安全邮件到身份验证和证书,其应用无处不在。
RSA 的基本设计非常简单而优雅,并且使用了简单的数学运算,但它却非常强大。如果使用得当,鉴于因数分解问题的数学复杂性,它几乎是不可能被破解的。
.NET Framework 为 RSA 提供了本地支持,这对于大多数用途来说都非常有用。但是,对于某些情况,例如某些签名方案,我们可能需要执行“私钥加密”,而这并未得到本地支持。因此,为了一个项目,我不得不从头开始实现 RSA 加密和解密。并且,由于缺乏适当的填充方案会导致该方案容易受到攻击,我还实现了 OAEP 和 PKCS v1.5 填充方案。该库(RSAx)与 .NET Framework 的 RSA 实现完全兼容。
库的特点
- RSA 加密和解密
- 支持私钥加密
- 支持 OAEP 填充
- 支持 PKCS v1.5 填充
- CRT-RSA 用于快速私钥解密
- 与 .NET 加密库完全兼容
- 使用 .NET BigInteger 库
背景
RSA 作为公钥密码系统,有两个密钥:公钥和私钥。加密使用一个密钥完成,解密使用另一个密钥完成。通常,加密使用公钥,解密使用私钥。RSA 模数(稍后解释)的长度称为密码系统的密钥长度。目前已知的最大素数因子有 768 位。由于 RSA 的安全性取决于因数分解问题,使用 1024 位的模数是最低要求。建议使用至少 2048 位以获得良好的安全性。4096 位几乎是不可破解的,任何超过 4096 位都过于复杂,而且会非常慢。
密钥生成
- 选择两个长度相似的大随机素数 P 和 Q。
- 计算 N = P x Q。N 是公钥和私钥的模数。
- PSI = (P-1)(Q-1),PSI 也称为欧拉的 totient 函数。
- 选择一个整数 E,使得 1 < E < PSI,并确保 E 和 PSI 是互质的。E 是公钥指数。
- 计算 D = E-1 ( mod PSI ),通常使用扩展欧几里得算法。D 是私钥指数。
加密
- 将要加密的数据字节转换为一个大整数,称为
PlainText
。 CipherText = PlainText<sup>E</sup> ( mod N )
- 将整数
CipherText
转换为字节数组,这是加密操作的结果。
解密
- 将加密后的数据字节转换为一个大整数,称为
CipherText
。 PlainText = CipherText<sup>D </sup>( mod N )
- 将整数
PlainText
转换为字节数组,这是解密操作的结果。
其他注意事项
- 很明显,指数非常大,因此,大整数很容易溢出普通的 32 位 '
int
' 和 64 位 'long
'。为了解决这个问题,我们需要使用一个可以处理任意大数的BigInteger
库。在这种情况下,我使用了 .NET Framework 提供的BigInteger
库 (System.Numerics.BigInteger
)。 - 从字节数组到整数的转换以及反之亦然是按照特定格式进行的,我们必须遵循大端格式(Big-Endian format)。我提供了通过
I2OSP()
和OS2IP()
进行转换的实用函数。
填充方案
RSA 加密中最关键的部分是填充方案。填充方案通常用于增加明文的长度,以便可以使用对称算法。但是,即使 RSA 在没有填充方案的情况下也能工作,但它对数据的安全性至关重要,因为如果没有适当的填充,密文就会容易受到许多不同攻击的影响。填充方案还为加密数据带来了随机性。每次加密数据时,密文都不同,但在解密后,明文保持不变。
一般有两种主要的填充方案:PKCS 和 OAEP(Optimal Asymmetric Encryption Padding)。PKCS 非常简单,并且作为一种较旧的标准仍然被广泛使用,但它容易受到一些较新的攻击。OAEP 由 Bellare 和 Rogaway 设计,以防止这些攻击,目前推荐使用。但是,OAEP 实现起来有点复杂。我将尝试详细解释 OAEP 填充方案。
OAEP 填充方案
+----------+---------+-------+
DB = | pHash | PS | M |
+----------+---------+-------+
|
+----------+ V
| Seed |--> MGF ---> XOR
+----------+ |
| |
+--+ V |
|00| XOR <----- MGF <-----|
+--+ | |
| | |
V V V
+--+----------+----------------------------+
EM = |00|maskedSeed| maskedDB |
+--+----------+----------------------------+
关键词
DB
- 要加密的数据块,由 pHash、PS 和 M 组成。pHash
- 参数列表的哈希值,以字节数组的形式表示。它用于确保加密端和解密端的参数相同,但在大多数实现中,它被忽略且是可选的。在这种情况下,使用空字节数组的哈希值代替。PS
- 一串 '0' 后面跟着一个 1。用于填充消息长度不足的最大允许消息长度的情况。M
- 要加密的实际消息。Seed
- 随机字节数组,其长度等于所使用的哈希函数的长度。MGF
- 掩码生成函数,它用于从给定的输入随机输入生成可变长度的哈希值。XOR
- 按位异或操作。maskedSeed
- 掩码化的 Seed,它是填充文本的一部分。在解码时,它与maskedDB
的 MGF 输出一起用于获取 Seed。maskedDB
- 掩码化的数据块。在解码时,它用于将 MGF 函数馈入,该函数用于获取 Seed。它还用于通过 Seed 的 MGF 输出获取 DB。
编码初步
在编码之前,必须固定哈希值和参数数组。对于大多数情况,参数 string
是一个空字节数组。通常,标准定义了三种不同的哈希类型:SHA1、SHA256 和 SHA512。SHA1 是默认的哈希算法。在解码时,参数数组和哈希算法保持不变非常重要,否则解码应该会失败。
考虑
hLen
= 哈希函数输出的长度(字节)k
= 模数的长度(八位字节)PS_Len
= k - MessageLength - 2 * hLen - 2
OAEP 编码
- 计算
pHash
= HASH(Parameter<code>_
Array)。 - 创建一个由 '0' 组成的长度为
PS_Len
的数组PS
。 - 创建
DB
=pHash
||PS
|| 0x01 ||M
,|| 表示追加操作。 - 生成一个长度为
hLen
的随机八位字节字符串Seed
。 - 使用 MGF(稍后解释)函数,将
Seed
作为输入随机性,输出长度为 (k
-hLen
- 1),来扩展Seed
以获得dbMask
。 maskedDB
=DB
XORdbMask
seedMask
= MGF(maskedDB
,hLen
)maskedSeed
=Seed
XORseedMask
。EncodedMessage
= 0x00 ||maskedSeed
||maskedDB
。
就是这样。编码后的消息直接使用 RSA 加密。
OAEP 解码
解码过程正好与编码过程相反。因为 maskedSeed
的长度是已知的,我们可以轻松地将 maskedSeed
和 maskedDB
分离。
seedMask
是通过 MGF 从 maskedDB
生成的,将生成的 seedMask
与 maskedSeed
进行异或操作以获得 Seed
。
Seed
使用 MGF 进行扩展以获得 dbMask
。
将 dbMask
和 maskedDB
进行异或操作以获得 DB。
由于 pHash
的长度已知,并且 M
在一串 '0
' 和一个 1
之后开始,因此我们可以轻松地从 DB
中获取 pHash
和 M
。
如果 pHash
与解密期间参数数组的 Hash 匹配,则 M
作为结果返回。否则,过程失败。
重要的是,解码算法不允许用户了解错误的具体性质,否则可能会导致一些攻击。
OAEP 方案的全部目的是确保即使解密时出现 1 位错误也会使整个数据包无效。这是通过哈希值来确保的,因为即使是单个位的更改也会导致输出位的完全变化。
MGF 函数
输入
Z
= 初始伪随机种子L
= 所需的输出长度
算法
for(int loop = 0 ; loop <= Loops ; loop++ )
{
X = Z || (loop in octets array of size 4, BigEndian)
MgfOut += HASH (X);
}
返回 MgfOut
的前 L
个八位字节
屏幕截图
展示了 RSAx 库的一个测试应用程序。可以使用公钥和私钥进行任何加密和解密组合。我已经测试了长度高达 8192 位的密钥。请确保对不同的(加密和解密)操作使用不同的密钥(公钥和私钥),否则解密将无法正常工作。使用 OAEP 时,请确保在单个加密和解密操作中使用相同的哈希算法。更改模数大小后,请务必执行 文件->'生成密钥对',以使程序正常工作。
我实现了一个基本的测试平台来验证库的功能。它只是加密和解密随机数据,并用预期结果验证结果。它使用上一个选项卡中的设置。
Using the Code
使用该库非常简单。
string PlainText = "Encrypt Me";
string KeyInfo = ".....";
RSAx rsa = new RSAx(KeyInfo, 1024);
byte[] CTX = rsax.Encrypt(Encoding.UTF8.GetBytes(PlainText), true);
string CipherText = Convert.ToBase64String(CTX);
byte[] ETX = Convert.FromBase64String(CipherText);
byte[] PTX = rsax.Decrypt(ETX, true);
string DecryptedString = Encoding.UTF8.GetString(PTX);
// PlainText and DecryptedString should be equal.
RSAx 类可以从 XML 字符串创建,类似于本地 .NET 对应项。它也可以通过使用 RSAxParameters
类来创建,该类允许手动指定公钥和私钥部分。
byte [] Modulus = ......
byte [] E = ......
byte [] D = .......
RSAxParameters rsaxParams = new RSAxParameters(Modulus, E, D, 1024);
RSAx rsax = new RSAx(rsaxParams);
// Use it normally
它还可以从 System.Security.Cryptography.RSAParameters
结构创建。
RSA rs = new RSACryptoServiceProvider(1024);
RSAParameters rsa_params = rs.ExportParameters(true);
RSAxParameters rsax_params = new RSAxParameters(rsa_params, 1024);
RSAx rsax = new RSAx(rsax_params);
// Use rsax normally
私钥加密和公钥解密可以按如下方式进行
string PlainText = "Encrypt Me";
string KeyInfo = ".....";
RSAx rsa = new RSAx(KeyInfo, 1024);
// Private key encryption
byte[] CTX = rsax.Encrypt(Encoding.UTF8.GetBytes(PlainText), true, true);
// Public key decryption
byte[] PTX = rsax.Decrypt(CTX, false, true);
string DecryptedString = Encoding.UTF8.GetString(PTX);
关注点
编写代码非常直接。我发现的一个有趣的事情是,<code>System.Security.Cryptography.
RNGCryptoServiceProvider 类提供了一个 GetNonZeroBytes()
函数,非常方便!我只是在想内部实现;如果生成的字节数组包含所有非零字节,那么随机性会不会有一些偏差?
我意识到的另一件事是,有时 RFC [http://tools.ietf.org/html/rfc3447] 是关于某个协议或算法的唯一信息来源,我们必须反复阅读它,直到完全理解为止。
有时,在 CRT-RSA 的最后一步之后,BigInteger
中的结果会变为负数。这个负结果会对解密后的结果造成混乱。为了解决这个问题,我采用了一个简单的技巧。当结果为负时,只需将模数 N 添加到结果中,即可将结果变回正数。这很简单,但却完全解决了问题。
历史
- 版本 1.0:第一个公开版本