RSA加密算法在Java与C#中的跨语言密钥转换问题

前言

  前段时间遇到个项目问题,涉及RSA加密,研究了几天,终于解决了,情况是这样的,项目使用的是.net framework的框架,需要对网络传输的个人数据进行加密,首先能从服务器直接获取一个加密用的公钥,使用这个公钥对本地个人数据加密后,再进行传输。
  出现的问题是完成加密后的数据在服务器端始终无法解析,折腾了好一段时间才知道是由于服务器端的Java和客户端的C#两者使用的RSA算法的密钥格式不一致,而.net framework没有提供对于相应格式的密钥的转换支持,C#对RSA相关密钥格式的支持在.net core之后才完善。
  问题找到了,解决方案两条思路,第一找开源库,研究了一下RSA,看了一圈github上的开源库,感觉开源库功能过于全面而复杂,引入项目增加的代码量太大,又看一下解析规则,感觉并不是太复杂,于是尝试第二条路自己解析,从头记录一下解决方案:

1 算法简介

  RSA算法是一种广泛使用的非对称加密算法,是目前最优秀的公钥方案之一。RSA算法基于一个数学理论支持,即获取两个大素数相乘的结果很简单,而对它们的乘积进行因式分解却极其困难。

对称加密:同一个密钥可以同时用作加密和解密,称为对称加密。
非对称加密:非对称加密算法需要两个密钥分别进行加密和解密,即公钥和私钥。

1.1 算法步骤

  1. p、q(两个大素数): 取两个足够大的素数p、q
  2. N(合数): 令N = p * q
  3. L: L = (p-1) * (q-1)
  4. E(公有幂): 使得E与L互质,且1 < E < L
  5. D(私有幂): 使得(D*E)% L = 1,且1 < D < L

素数:除了1和它自身外,不能被其他自然数整除的大于1的自然数。
合数:除了素数以外的大于1的自然数。
互质:公约数只有1的两个整数,叫做互质整数。

其中p和q是两个很大的素数,N是他们的乘积,用于之后求模,它是个合数,即合数模,E是公钥中的幂指数,D是私钥中的幂指数。

得到的(E,N)即为公钥,(D,N)即为私钥。

公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密;如果用私钥对数据进行加密,那么只有用对应的公钥才能解密。

公钥加密——私钥解密
加密过程:密文=(明文^E)mod N
解密过程:明文=(密文^D)mod N

也可以反过来,私钥加密——公钥解密
加密过程:密文=(明文^D)mod N
解密过程:明文=(密文^E)mod N

由于公钥和私钥都是数的组合,我们一般把合数模N的二进制位数长度称作密钥的长度。一般密钥会要求一定的长度,RSA从提出到现在已近二十年,经历了各种攻击的考验,目前被破解的最长RSA密钥是768个二进制位,因此可以认为,768位的密钥不用担心受到除了国家安全管理(NSA)外的其他事物的危害,1024位的密钥几乎已经是安全的。

加、解密的过程是个模指数运算过程。

1.2 算法示例

p = 3q = 11
计算N:N = p * q = 3 * 11 = 33
计算L:L = (p - 1) * (q - 1) = 20
E = 3,可满足E与L互质,且1 < E < L
D = 7,可满足(D * E) % L = 1,即(D * 3) % 20 = 1,且1 < D < L

得到(3, 33)为公钥,(7, 33)为私钥

该例中的N为33,即二进制的10 0001,即例子中密钥的长度为5

例子里的数取得很小以便于理解。

给定要加密的明文“7”,演示加密和解密的过程:

进行“公钥加密、私钥解密”
加密过程:密文 = (明文^E)mod N = (7 ^ 3) % 33 = 343 % 33 = 13,得到加密后的密文为“13”
解密过程:明文 = (密文^D)mod N = (13 ^ 7) % 33 = 62748517 % 33 = 7,解回明文“7”

反过来,进行“私钥加密,公钥解密”
加密过程:密文 = (明文^D)mod N = (7 ^ 7) % 33 = 823543 % 33 = 28,得到加密后的密文为“28”
解密过程:明文 = (密文^E)mod N = (28 ^ 3) % 33 = 21952 % 33 = 7,解回明文“7”

例子中的明文为数字,加密算法可以用于各种数据信息(数字、字母、符号、图片、音频、视频),现实中的所有数据在计算机中都是使用二进制的“0”、“1”组合的形式表示的,再通过解码可以转换成能够理解的数据信息格式。

2 密钥格式

  RSA算法的密钥具有多种不同的描述格式,例如PEM格式、ASN格式、XML格式、DER格式等,使用不同的密钥格式通过对应的规则都可以用来表示密钥对象,解析出各个字段的内容。

2.1 PEM格式

PEM格式又具有多种填充方式从“PKCS#1”一直到“PKCS#15”,常用的是“PKCS#1”和“PKCS#8”,它们的文件头也可能存在区别。

公钥

-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALDTR8eu33xA4ru/JPbEsIapTTF1SzEi
IjAbntCXdEK6xdwsuomv7kL7rVQXef2rzhvzBSyZxuRbf33u6fi4jW8CAwEAAQ==
-----END PUBLIC KEY-----

私钥

-----BEGIN PRIVATE KEY-----
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAsNNHx67ffEDiu78k
9sSwhqlNMXVLMSIiMBue0Jd0QrrF3Cy6ia/uQvutVBd5/avOG/MFLJnG5Ft/fe7p
+LiNbwIDAQABAkEAmKtS5k1OF/HN0VwPhh/8acfzJiinaxyVeAPg8yhQ8OryQxG2
CnqTgG4V2PAMvxX42W+ZqA0zTFXx4EtWmq8FQQIhANjpAY7W6TAidjy2qlmfuSl4
DoY75bKJRsg2GVVYDDyTAiEA0LD88irK80hKj2JeAgEP0NXyYV8QZSuEM5Qk0G3U
0TUCIFpNhwyEhEg50KeuFHWDfX66MLHJtfMCG6m2fA1/vnhpAiEAowF7sdRHDdvr
kS+uajZWGjLizbepYLyq2HbggoUnc/kCICj08MHdsE2excF0rtNi457J57ZhnTsj
9uDBvPY+9JTT
-----END PRIVATE KEY-----

2.2 ASN格式

ASN格式是一串纯Base64字符串,可以看成去掉了PEM格式的头尾描述后的形式。

公钥

MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALDTR8eu33xA4ru/JPbEsIapTTF1SzEi
IjAbntCXdEK6xdwsuomv7kL7rVQXef2rzhvzBSyZxuRbf33u6fi4jW8CAwEAAQ==

私钥

MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAsNNHx67ffEDiu78k
9sSwhqlNMXVLMSIiMBue0Jd0QrrF3Cy6ia/uQvutVBd5/avOG/MFLJnG5Ft/fe7p
+LiNbwIDAQABAkEAmKtS5k1OF/HN0VwPhh/8acfzJiinaxyVeAPg8yhQ8OryQxG2
CnqTgG4V2PAMvxX42W+ZqA0zTFXx4EtWmq8FQQIhANjpAY7W6TAidjy2qlmfuSl4
DoY75bKJRsg2GVVYDDyTAiEA0LD88irK80hKj2JeAgEP0NXyYV8QZSuEM5Qk0G3U
0TUCIFpNhwyEhEg50KeuFHWDfX66MLHJtfMCG6m2fA1/vnhpAiEAowF7sdRHDdvr
kS+uajZWGjLizbepYLyq2HbggoUnc/kCICj08MHdsE2excF0rtNi457J57ZhnTsj
9uDBvPY+9JTT

2.3 XML格式

XML格式密钥是C#语言下的默认格式,使用XML描述语言将密钥对象的各个字段表示出来。

公钥

<RSAKeyValue>
    <Modulus>
        riLSQFVDC229P5F+Mkicbkpg5OC8+SeL6hvkJXIGYiN/e4YnprCxuIp5sH9AwWup4WJmObPKd1jOVGm07UwgVU7CDtTaVe1Uuk78yJBwgRuSteQjHYMmH6nG5YHvvONuvkmLnyIKGygJBL+4+Qmd3GaCHRtIrdfShlH3UbPINlM=
    </Modulus>
    <Exponent>
        AQAB
    </Exponent>
</RSAKeyValue>

私钥

<RSAKeyValue>
    <Modulus>
		riLSQFVDC229P5F+Mkicbkpg5OC8+SeL6hvkJXIGYiN/e4YnprCxuIp5sH9AwWup4WJmObPKd1jOVGm07UwgVU7CDtTaVe1Uuk78yJBwgRuSteQjHYMmH6nG5YHvvONuvkmLnyIKGygJBL+4+Qmd3GaCHRtIrdfShlH3UbPINlM=
    </Modulus>
    <Exponent>
        AQAB
    </Exponent>
    <P>
        6yCboYtKzIezMOFzGzzW8dp7SBT8f7jTRzH1ZIKQYKF0Mq/39k80SeUvY578O031+bg6i3cbNvvAhL8XjqTtmQ==
    </P>
    <Q>
        vZgnL5LHnNE5uUW5NBwYvZbIz6hWNzc6kyDGimI8WBBFJOI06IdYGL2VMeGVs4lt5a1tM7T3c6gzBKgDQpL+yw==
    </Q>
    <DP>
        p5tV9YDyr/unq5d6Uxc6bar9qHN1TqJ00VJ2h9BelNNinmM70fPB5U8fSddiG/BGAF3oNdSQrNAm+zmw1DkTOQ==
    </DP>
    <DQ>
        fxS1b1XbJmm3X1A0y5DppGqlP0t+PpRuVp/pdGhUOlLthcN540KU8kBg+IZUaXr8hq6wO7BZDNT5HW3ggYc18Q==
    </DQ>
    <InverseQ>
        q29etXnlszOH0FlQWDL9yLfJ+EruH4VURY1mZGz/+/qvPewUwyEf+EqJkZHVXEijnSa1CiFELK2YE9PhkUp2Xw==
    </InverseQ>
    <D>
        bZUoLqf5KwYCJDDQ85/SIW3ZD++FvF1wpQCsUAwzjCq+nONNrI5hKLqr3bAW9iFkpJshrYpBDV3rah+jZfmUFk/UZeur2+kA2r5r1or34+HiIhT4sehU1lxww4DvTzf1/1ivG4LCvUPoFtT67Zdh8pNEC27N6bFDL8fbSU7GcmE=
    </D>
</RSAKeyValue>

2.4 DER格式

DER格式是一种十六进制的密钥格式

公钥

30818902818100C19CFA5EA25F99C482499E3A557C7D0C3ABB375B19900CF4956E39F5B1EACA46A37CEE30DDBEA72979B6DABA11D4B9BD51CB1D79ED667607E65CF53491EE6BE35155072D5EFB96BA0E0FC0B9C1DFEDFA30886F645218CC680E55A7D5568AD59283E9BB3DC82970F6B3F6DD83FB308E2C610F362C71977D428614ED5FB59EFDB10203010001

私钥

3082025D020100028181009951BFB322876658587C207F2AFC2F2638DFD4EEEE925669C4A9487A3774891AEBE638940318B1AE270784FCFBE768C8C989E33B3953D820326DFAC7862AA133F96EE216C1B3F5651D45194CA02E9926E8FF133B2C03BB22BFE8C60B13D4757F263D4A792B188A8183FAB53B193C8AF8AB8EAB7020547C20D5BC90E1B369DBA902030100010281800FD9829ECB28022D89E0331FD25AC5A906E224CA1A81A84B40D85B34BF3CDDDB999D7025E4F80D8E3A5CADA3D58AC3AB56225A0A4A4FDF9CDC79C01E16419BEE71997546C68E6408E5D9C044858DCB6393F285D3EBAAD0C75298F61B33B752EB8E1ABA4D66E5380FF52DE07929A96367673CDAE5945A0C13F3503FABEDB1758D024100C52F9745C13B77968D52F29ACDEE00F2DF07AF8025048348B054B7EFF460097CAB824212447F674B55CF74E489DD399E702D3D655C74484248E05CA2E9DCCEE7024100C70CA9EA361ED73C42627254F33A3FA81AD0AADD64D45A1E536E64C7E31737B7ED3FCC20E03A082673C6E7A7270640F6132AA295FF406D6133090E7D89397EEF024100883505936395065872AAB77683854216824536CF97C2744543B8618E5909F5C3AE5D3DF28C6A4D19D6DE84EA50E905A211EECE18343306AEF2D43869388E1445024100B061FE6776D1D974A276CE4D8CC2FF099DC96EBF84CBCF97B3E2CD177B9A655B6CB6EDD1EC20407CA2778D6B475F794D152AE0ABFE663F06B4CCBFB46A5732AD02404981E8728E85719A319C9A8C4C7D3B162BFA728AD5FD054C0A45A9A625385167F0822D2398680FD75BF29A3C20A4D72D2115ABD06F27B3819214AAC77284518A

3 密钥解析(ASN.1)

  不管RSA算法的密钥格式形式如何,其最终目的是表示密钥的内容,RSA算法的密钥最终需要被解析成一个类似结构体的对象,其解析规则遵循ASN.1标准。

  ASN.1抽象语法标记(Abstract Syntax Notation One),描述了一种对数据进行表示、编码、传输和解码的数据格式。提供了一整套正规的格式用于描述对象的结构。数字1被加在ASN的后边,是为了保持ASN的开放性,可以让以后功能更加强大的ASN被命名为ASN.2等,但至今也没有出现。

  ASN.1中关联了多个标准化编码规则,基本编码规则(BER)、规范编码规则(CER)、识别名编码规则(DER)、压缩编码规则(PER)和 XML编码规则(XER)。其中BER、CER、DER、PER都是二进制编码规则,BER(BasicEncoding Rules)是ASN.1中最早定义的编码规则,其他编码规则是在BER的基础上添加新的规则而构成的。

3.1 BER编码规则

  传输语法的格式为TLV三元组<Tag, Length, Value>,其中Value字段又能继续包含新的TLV三元组,即可以产生嵌套<T, L, <T, L, V>>

  密钥的各种格式最终都会被转换成二进制,基于八位组的大字节序(Big-Endian)编码方式,高八位在左,低八位在右来解析。例如DER格式的十六进制密钥“30819f300d0609…………”的转换如下:

十六进制密钥 二进制高八位 二进制低八位
30 0011 0000
81 1000 0001
9F 1001 1111
30 0011 0000
0D 0000 1101
06 0000 0110
09 0000 1001

跨语言加密解密的时候需要考虑各个语言的字节序编码方式(Big-Endian与Little-Endian),C/C++语言的字节序跟编译平台的CPU相关,主流的Intel-x86架构采用小字节序,Java语言采用大字节序,用于网络传输的网络字节序是大字节序,多端互操作的情况下,需要考虑大小字节序的转换问题,额外处理。

3.1.1 Tag字段

包含一个或若干个八位组(即字节),以十六进制“30”为例,二进制表示为“0011 0000”,各个位的含义如下:

第7位 第6位 第5位 第4位 第3位 第2位 第1位 第0位
0 0 1 1 0 0 0 0
标签类型 标签类型 编码方式 值类型 值类型 值类型 值类型 值类型

第7、6位指明标签分类:

标签分类
00 universal 通用标签
01 application 应用标签
10 context-specific 上下文专用标签
11 private 私有标签
3.1.1.1 通用标签

当Tag分类为”universal 通用标签“时,第5位指明标签编码方式:

编码方式
0 primitive 原始类型
1 constructed 构造类型

当Tag分类为”universal 通用标签“时,第4~0位:指明标签值的类型:

值(二进制) 值(十进制) 值类型
00000 0 保留
00001 1 Boolean 布尔类型
00010 2 Integer 整型
00011 3 Bit String 位串
00100 4 Octet String 字节串(八位位组串)
00101 5 NULL 空值
00110 6 Object Identifier 对象标识符
00111 7 Object Description
01000 8 External,Instance of
01001 9 Real 实数
01010 10 Enumerated 枚举类型
01011 11 Embedded PDV
01100 12 UTF8 String
01101 13 Relative-oid
01110 14 保留
01111 15 保留
10000 16 Sequence 序列,Sequence of 单类型序列
10001 17 Set集合,Set of 单类型集合
10010 18 Numeric String
10011 19 Printable String
10100 20 Teletex String,T61 String
10101 21 Videotex String
10110 22 IA5 String
10111 23 UTC Time
11000 24 Generalized Time
11001 25 Graphic String
11010 26 Visible String,ISO646 String
11011 27 General String
11100 28 Universal String
11101 29 Character String
11110 30 BMP String
11111 31 保留
3.1.1.2 非通用标签

当Tag分类不为”universal 通用标签“,而是另外三种时,在后续的多个八位组中编码,第一个八位组后五位固定全部为1,其余的八位组最高位为1表示后续还有,为0表示Tag结束。

第一个八位组 第二个八位组 第二个八位组 …… 第n个八位组
01 11 1111 1xxx xxxx 1xxx xxxx 1… … 0xxx xxxx
高位01表示“application 应用标签”
后五位固定全为1
高位为1,表示还在编码 高位为1,表示还在编码 高位为1,表示还在编码 高位为0,表示结束

3.1.2 Length字段

Length字段的组织方式有两大类:定长方式和不定长方式,第一个八位组不为0x80表示定长方式,为0x80表示不定长方式:

3.1.2.1 定长方式

定长方式中,按长度是否超过一个八位,又分为短形式、长形式,最高位为”0“表示短形式,最高位为”1“表示长形式:

短形式

各个位的含义如下:

最高位 后7位
0 xxx xxxx
0表示短形式 表示TLV三元组中的Value字段占用的字节数(范围0~127)
长形式

各个位的含义如下:

第一个八位组 ……
1xxx xxxx ……
最高位”1“表示长形式
后7位表示需要继续编码的长度(字节数)
按照第一个八位提供的长度值,继续编码,其数值表示TLV三元组中的Value字段占用的字节数(范围0~256^126-1)
3.1.2.2 不定长方式

Length所在八位组固定编码为0x80,但在Value编码结束后以两个0x00结尾。

第一个八位组 …… 第n-1个八位组 第n个八位组
1000 0000 …… 0000 0000 0000 0000
0x80表示不定长方式 不为0x00,表示还在编码,其数值表示TLV三元组中的Value字段占用的字节数 遇到第一个0x00 遇到两个连续的0x00表示结束

3.1.3 Value字段

该字段内可能包含基础数据,也可能包含嵌套的TLV三元组,需要具体解析。

3.2 公钥解析示例

按照上述规则,解析一个密钥实例,以公钥为例(DER格式)解析方式如下:

30819f300d06092a864886f70d010101050003818d0030818902818100890f1a96bb296740990674217c96afe8bbbc63ba69123a55c87f03afe36f106522c2935a650a6bfd929a575941396d888424e4ee702e33f5ea2275d4d9e8c80c6c503a07c1f471f501e89abd4c6fd169b4c32460e1fd35ff2bbdb3febaa4c28a5b549b20017caea2652761b2a7edb22cb765921e18f1fe9315a8ade66625d11d0203010001

按照ASN.1规范的BER编码规则对每个字节进行解析如下:

字节序号 密钥原文 对应的二进制 TLV组
(“····”缩进)
语义
0 30 0011 0000 T Tag值 Universal类型 结构体——类型SEQUENCE
1 81 1000 0001 L Length值 定长 长类型——长度1个字节
2 9F 1001 1111 ——实际长度159个字节(0x9F转十进制)
V{ Value值{
3 30 0011 0000 ····T Tag值 Universal类型 结构体 类型SEQUENCE
4 0D 0000 1101 ····L Length值 定长 短类型——实际长度13个字节
····V{ Value值{
5 06 0000 0110 ········T Tag值 Universal类型 原数据——类型OBJECT IDENTIFIER
6 09 0000 1001 ········L Length值 定长 短类型——实际长度9个字节
········V{ Value值{
7~11 2A 86 48 86 F7 ············
12~15 0D 01 01 01 ············
········} }
16 05 0000 0101 ········T Tag值 Universal类型 原数据——类型NULL
17 00 0000 0000 ········T Tag值 Universal类型 原数据——类型BER保留
····} }
18 03 0000 0011 ····T Tag值 Universal类型 原数据——类型BIT STRING
19 81 1000 0001 ····L Length值 定长 长类型——长度1个字节
20 8D 1000 1101 ····· ——实际长度141个字节
····V{ Value值{
21 00 ········ unused bit
22 30 ········T Tag值 Universal类型 结构体 类型SEQUENCE
23 81 1000 0001 ········L Length值 定长 长类型——长度1个字节
24 89 1000 1001 ········ ——实际长度137个字节
········V{ Value值{
25 02 0000 0010 ············T Tag值 Universal类型 原数据——类型INTEGER
26 81 1000 0001 ············L Length值 定长 长类型——长度1个字节
27 81 1000 0001 ············ ——实际长度129个字节
············V{ Value值{
28 00 ················ 首字节的MSB=1时,补0
29~156 89 0F 1A 96 BB ················
29 67 40 99 06 ················
74 21 7C 96 AF ················
E8 BB BC 63 BA ················
69 12 3A 55 C8 ················
7F 03 AF E3 6F ················
10 65 22 C2 93 ················
5A 65 0A 6B FD ················
92 9A 57 59 41 ················
39 6D 88 84 24 ················ 128字节的公钥值modulus
E4 EE 70 2E 33 ················
F5 EA 22 75 D4 ················
D9 E8 C8 0C 6C ················
50 3A 07 C1 F4 ················
71 F5 01 E8 9A ················
BD 4C 6F D1 69 ················
B4 C3 24 60 E1 ················
FD 35 FF 2B BD ················
B3 FE BA A4 C2 ················
8A 5B 54 9B 20 ················
01 7C AE A2 65 ················
27 61 B2 A7 ED ················
B2 2C B7 65 92 ················
1E 18 F1 FE 93 ················
15 A8 AD E6 66 ················
25 D1 1D ················
············} }
157 02 0000 0010 ············T Tag值 Universal类型 原数据——类型INTEGER
158 03 0000 0011 ············L Length值 定长 短类型——实际长度3个字节
············V{ Value值{
159 01 ················
160 00 ················ 3字节的公有幂值exponent
161 01 ················
············} }
········} }
····} }
} }

解析完的公钥对象的结构形式如下:

SEQUENCE{ // 159个字节
    SEQUENCE{ // 13个字节
        OBJECT IDENTIFIER{ // 9个字节
        }
    }
    BIT STRING{ // 141个字节
        SEQUENCE{ // 137个字节
            INTEGER{ // 129个字节
                补0 // 1个字节
                modulus // 128个字节,模数,即N
            }
            INTEGER{
                exponent // 3个字节,公有幂值,即E
            }
        }
    }
}

至此我们已经从DER密钥格式的十六进制串中解析出了模数modulus和公有幂exponent,即公钥内容。

解出了公钥的内容,要向其它几种密钥格式转换,只需要按照对应的格式规则进行处理即可,很多语言都有现呈的类可以进行构造(C#中提供RSACryptoServiceProvider类)。例子中是公钥的解析,在RSA算法中,私钥对象的结构要比公钥多几个字段,但解析方式是一样的,同样遵循ASN.1规范。

4 源代码

最后贴上客户端的C#源码,从DER格式的公钥中解出模数和公有幂:


/**
 * 从DER格式的公钥字符串解析出modulus和exponent
 */
internal static KeyValuePair<byte[], byte[]> DecodeHexPublicKey(string hexPublicKey)
{
    try
    {
        byte[] keyBytes = HexStringToByte(hexPublicKey); // 字符串转成十六进制字节数组
        if (keyBytes[0] != 0x30) // 第一个字节的文件头必须解析成SEQUENCE结构
        {
            throw new Exception("head of file cannot decode to SEQUENCE");
        }

        // 解析主结构体(SEQUENCE结构体)
        int mainSequenceLength = 0;
        int currentIndex = 1; // 从第二个字节开始解析
        mainSequenceLength = DecodeTLV_Length(keyBytes, ref currentIndex); // 获取主SEQUENCE结构体的长度

        // 解析inner(SEQUENCE结构体)
        int innerSequenceLength = 15; // inner的固定长度15
        currentIndex += innerSequenceLength; // 跳过即可

        // 解析bitString(Bit String类型)
        currentIndex++; // 下标跳过Bit String数据的Tag字段,移向Length字段
        int bitStringLength = DecodeTLV_Length(keyBytes, ref currentIndex); // 获取Bit String数据的长度
        currentIndex++; // Bit String的Value字段带一个unused bit,跳过

        // 解析参数结构体(SEQUENCE结构体)
        currentIndex++; // 下标跳过参数结构体的Tag字段,移向Length字段
        int paramSequenceLength = DecodeTLV_Length(keyBytes, ref currentIndex); // 获取参数SEQUENCE结构体的长度

        // 解析modulus模数N
        byte[] modulus = DecodeInteger(keyBytes, ref currentIndex);

        // 解析exponent公开幂E
        byte[] exponent = DecodeInteger(keyBytes, ref currentIndex);

        // 公钥(E,N)
        KeyValuePair<byte[], byte[]> result = new KeyValuePair<byte[], byte[]>(modulus, exponent);

        return result;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        return new KeyValuePair<byte[], byte[]>();
    }
}
/**
 * 字符串以十六进制的规则转为字节数组
 */
private static byte[] HexStringToByte(string hexString)
{
    hexString = hexString.Replace(" ", "");
    if ((hexString.Length % 2) != 0)
    {
        hexString += " ";
    }

    byte[] returnBytes = new byte[hexString.Length / 2];
    for (int i = 0; i < returnBytes.Length; i++)
    {
        returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
    }

    return returnBytes;
}
/**
 * 解码ASN.1规则中的INTEGER类型,返回解码后的数据,处理完成后currentIndex的位置会移到该类型数据末尾的下一个位置
 * @param data 密钥字节数组(十六进制)
 * @param currentIndex 表示当前需要解码的Integer的Tag字段的位置
 */
private static byte[] DecodeInteger(byte[] dataInteger, ref int currentIndex)
{
    if (dataInteger[currentIndex] != 0x02)
    {
        throw new Exception("head of file cannot decode to INTEGER");
    }
    currentIndex += 1;
    var len = DecodeTLV_Length(dataInteger, ref currentIndex);
    int integerStart = currentIndex;
    int integerEnd = currentIndex + len - 1;

    // 跳过补零位,>0x7f时补0
    while (dataInteger[currentIndex] == 0)
    {
        currentIndex++;
    }
    if (dataInteger[currentIndex] > 0x7f)
    {
        integerStart = currentIndex;
    }

    byte[] result = dataInteger.Where((item, index) => index >= integerStart & index <= integerEnd).ToArray();

    currentIndex = integerEnd + 1; // 处理完成后currentIndex移到末尾的下一个位置
    return result;
}
/**
 * 解码TLV元组中的Length字节,返回实际长度,返回0表示错误,处理完成后currentIndex的位置会移到Length字段末尾的下一个位置
 * @param data 密钥字节数组(十六进制)
 * @param index 表示当前需要解码的TLV元组中的Length字段的位置
 */
private static int DecodeTLV_Length(byte[] data, ref int currentIndex)
{
    try
    {
        int length = data[currentIndex]; // 获取当前字节,十进制表示
        if (length == 0) // 解码不能为:0000 0000,抛出异常
        {
            throw new Exception("parameter length in TLV cannot be 0x00");
        }
        else if(length < 0x80) // 解码为:0xxx xxxx,定长式、短形式:最高位为0,后7位表示长度的实际值
        {
            ++currentIndex; // 处理完成后currentIndex移到末尾的下一个位置
            return length;
        }
        else if(length == 0x80) // 解码为:1000 0000,不定长式:以两个连续的0x00结尾
        {
            int result = 0;
            int count = 0; // 连续遇到0x00的次数
            while (count != 2)
            {
                ++currentIndex; // 获取下一个字节
                int temp = data[currentIndex];
                if (temp == 0x00) // 遇到0x00
                {
                    ++count;
                }
                else
                {
                    count = 0;
                }
                ++result;
            }

            ++currentIndex; // 处理完成后currentIndex移到末尾的下一个位置
            return result;
        }
        else // 解码为:1xxx xxxx,定长式、长形式:最高位为1,后7位表示长度的实际值会占用接下的多少个字节数
        {
            length &= 0x7F; // 当前值和“0111 1111”进行按位与,提取出length后7位的1,后7位的十进制表示长度的实际值占用的字节数
            int result = 0;
            for (int i = length - 1; i >= 0; i--)
            {
                ++currentIndex; // 获取下一个字节
                int temp = data[currentIndex] << (i * 8); // 计算当前字节所在的位置表示的值,每多一个字节需要左移八位
                result += temp;
            }

            ++currentIndex; // 处理完成后currentIndex移到末尾的下一个位置
            return result;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        ++currentIndex; // 处理完成后currentIndex移到末尾的下一个位置
        return 0;
    }
}

特别注意:如果场景涉及大字节序和小字节序(Big-Endian与Little-Endian)的转换,需要额外处理。

总结

  RSA算法本身的原理并不复杂,其中的密钥解析依赖于密钥的格式规范,各种密钥格式的互转都可以先转换成原始的密钥内容结构体对象,再按照不同的密钥格式规范进行组装,期间可能会涉及到base64字符串的转换、不同进制的转换、大小字节序的转换等问题,本文提供了对十六进制DER格式公钥解析的具体解决方案。

posted @ 2021-06-16 01:21  seedoubleu  阅读(1613)  评论(0)    收藏  举报