65.9K
CodeProject 正在变化。 阅读更多。
Home

带 C# 私钥加密的 RSA 库

starIconstarIconstarIconstarIconstarIcon

5.00/5 (21投票s)

2012 年 7 月 15 日

CPOL

9分钟阅读

viewsIcon

173690

downloadIcon

15845

具有完整 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 位都过于复杂,而且会非常慢。

密钥生成

  1. 选择两个长度相似的大随机素数 P 和 Q。
  2. 计算 N = P x Q。N 是公钥和私钥的模数。
  3. PSI = (P-1)(Q-1),PSI 也称为欧拉的 totient 函数。
  4. 选择一个整数 E,使得 1 < E < PSI,并确保 E 和 PSI 是互质的。E 是公钥指数。
  5. 计算 D = E-1 ( mod PSI ),通常使用扩展欧几里得算法。D 是私钥指数。

加密

  1. 将要加密的数据字节转换为一个大整数,称为 PlainText
  2. CipherText = PlainText<sup>E</sup> ( mod N )
  3. 将整数 CipherText 转换为字节数组,这是加密操作的结果。

解密

  1. 将加密后的数据字节转换为一个大整数,称为 CipherText
  2. PlainText = CipherText<sup>D </sup>( mod N )
  3. 将整数 PlainText 转换为字节数组,这是解密操作的结果。

其他注意事项

  1. 很明显,指数非常大,因此,大整数很容易溢出普通的 32 位 'int' 和 64 位 'long'。为了解决这个问题,我们需要使用一个可以处理任意大数的 BigInteger 库。在这种情况下,我使用了 .NET Framework 提供的 BigInteger 库 (System.Numerics.BigInteger)。
  2. 从字节数组到整数的转换以及反之亦然是按照特定格式进行的,我们必须遵循大端格式(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          |
                         +--+----------+----------------------------+  
图 1:OAEP 填充方案

关键词

  • 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 编码

  1. 计算 pHash = HASH(Parameter<code>_Array)。
  2. 创建一个由 '0' 组成的长度为 PS_Len 的数组 PS
  3. 创建 DB = pHash || PS || 0x01 || M,|| 表示追加操作。
  4. 生成一个长度为 hLen 的随机八位字节字符串 Seed
  5. 使用 MGF(稍后解释)函数,将 Seed 作为输入随机性,输出长度为 (k - hLen - 1),来扩展 Seed 以获得 dbMask
  6. maskedDB = DB XOR dbMask
  7. seedMask = MGF(maskedDB, hLen)
  8. maskedSeed = Seed XOR seedMask
  9. EncodedMessage = 0x00 || maskedSeed || maskedDB

就是这样。编码后的消息直接使用 RSA 加密。

OAEP 解码

解码过程正好与编码过程相反。因为 maskedSeed 的长度是已知的,我们可以轻松地将 maskedSeedmaskedDB 分离。

seedMask 是通过 MGFmaskedDB 生成的,将生成的 seedMaskmaskedSeed 进行异或操作以获得 Seed

Seed 使用 MGF 进行扩展以获得 dbMask

dbMaskmaskedDB 进行异或操作以获得 DB。

由于 pHash 的长度已知,并且 M 在一串 '0' 和一个 1 之后开始,因此我们可以轻松地从 DB 中获取 pHashM

如果 pHash 与解密期间参数数组的 Hash 匹配,则 M 作为结果返回。否则,过程失败。

重要的是,解码算法不允许用户了解错误的具体性质,否则可能会导致一些攻击。

OAEP 方案的全部目的是确保即使解密时出现 1 位错误也会使整个数据包无效。这是通过哈希值来确保的,因为即使是单个位的更改也会导致输出位的完全变化。

MGF 函数

输入

  1. Z = 初始伪随机种子
  2. L = 所需的输出长度

算法

    for(int loop = 0 ; loop <= Loops ; loop++  )
    {
	X = Z || (loop in octets array of size 4, BigEndian)
	MgfOut += HASH (X);
    }   

返回 MgfOut 的前 L 个八位字节

屏幕截图

截图 1:主窗口

展示了 RSAx 库的一个测试应用程序。可以使用公钥和私钥进行任何加密和解密组合。我已经测试了长度高达 8192 位的密钥。请确保对不同的(加密和解密)操作使用不同的密钥(公钥和私钥),否则解密将无法正常工作。使用 OAEP 时,请确保在单个加密和解密操作中使用相同的哈希算法。更改模数大小后,请务必执行 文件->'生成密钥对',以使程序正常工作。

截图 2:测试和性能窗口

我实现了一个基本的测试平台来验证库的功能。它只是加密和解密随机数据,并用预期结果验证结果。它使用上一个选项卡中的设置。

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:第一个公开版本
© . All rights reserved.