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…………”的转换如下:

十六进制密钥二进制高八位二进制低八位
3000110000
8110000001
9F10011111
3000110000
0D00001101
0600000110
0900001001

跨语言加密解密的时候需要考虑各个语言的字节序编码方式(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位
00110000
标签类型标签类型编码方式值类型值类型值类型值类型值类型

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

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

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

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

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

值(二进制)值(十进制)值类型
000000保留
000011Boolean 布尔类型
000102Integer 整型
000113Bit String 位串
001004Octet String 字节串(八位位组串)
001015NULL 空值
001106Object Identifier 对象标识符
001117Object Description
010008External,Instance of
010019Real 实数
0101010Enumerated 枚举类型
0101111Embedded PDV
0110012UTF8 String
0110113Relative-oid
0111014保留
0111115保留
1000016Sequence 序列,Sequence of 单类型序列
1000117Set集合,Set of 单类型集合
1001018Numeric String
1001119Printable String
1010020Teletex String,T61 String
1010121Videotex String
1011022IA5 String
1011123UTC Time
1100024Generalized Time
1100125Graphic String
1101026Visible String,ISO646 String
1101127General String
1110028Universal String
1110129Character String
1111030BMP String
1111131保留
3.1.1.2 非通用标签

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

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

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

3.1.2.1 定长方式

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

短形式

各个位的含义如下:

最高位后7位
0xxx 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 00000000 0000
0x80表示不定长方式不为0x00,表示还在编码,其数值表示TLV三元组中的Value字段占用的字节数遇到第一个0x00遇到两个连续的0x00表示结束
3.1.3 Value字段

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

3.2 公钥解析示例

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

30819f300d06092a864886f70d010101050003818d0030818902818100890f1a96bb296740990674217c96afe8bbbc63ba69123a55c87f03afe36f106522c2935a650a6bfd929a575941396d888424e4ee702e33f5ea2275d4d9e8c80c6c503a07c1f471f501e89abd4c6fd169b4c32460e1fd35ff2bbdb3febaa4c28a5b549b20017caea2652761b2a7edb22cb765921e18f1fe9315a8ade66625d11d0203010001

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

字节序号密钥原文对应的二进制TLV组
(“····”缩进)
语义
0300011 0000TTag值 Universal类型 结构体——类型SEQUENCE
1811000 0001LLength值 定长 长类型——长度1个字节
29F1001 1111——实际长度159个字节(0x9F转十进制)
V{Value值{
3300011 0000····TTag值 Universal类型 结构体 类型SEQUENCE
40D0000 1101····LLength值 定长 短类型——实际长度13个字节
····V{Value值{
5060000 0110········TTag值 Universal类型 原数据——类型OBJECT IDENTIFIER
6090000 1001········LLength值 定长 短类型——实际长度9个字节
········V{Value值{
7~112A 86 48 86 F7············
12~150D 01 01 01············
········}}
16050000 0101········TTag值 Universal类型 原数据——类型NULL
17000000 0000········TTag值 Universal类型 原数据——类型BER保留
····}}
18030000 0011····TTag值 Universal类型 原数据——类型BIT STRING
19811000 0001····LLength值 定长 长类型——长度1个字节
208D1000 1101·····——实际长度141个字节
····V{Value值{
2100········unused bit
2230········TTag值 Universal类型 结构体 类型SEQUENCE
23811000 0001········LLength值 定长 长类型——长度1个字节
24891000 1001········——实际长度137个字节
········V{Value值{
25020000 0010············TTag值 Universal类型 原数据——类型INTEGER
26811000 0001············LLength值 定长 长类型——长度1个字节
27811000 0001············——实际长度129个字节
············V{Value值{
2800················首字节的MSB=1时,补0
29~15689 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················
············}}
157020000 0010············TTag值 Universal类型 原数据——类型INTEGER
158030000 0011············LLength值 定长 短类型——实际长度3个字节
············V{Value值{
15901················
16000················3字节的公有幂值exponent
16101················
············}}
········}}
····}}
}}

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

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格式公钥解析的具体解决方案。

附加资料:跨越千年的RSA算法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RSA(Rivest-Shamir-Adleman)是一种非对称加密算法,用于加密和解密数据。C#提供了对RSA算法的支持。下面是一个简单的示例代码,展示如何使用RSA算法进行加密和解密: ```csharp using System; using System.Security.Cryptography; using System.Text; public class RSADemo { public static void Main() { // 创建RSA实例 using (RSA rsa = RSA.Create()) { try { // 生成RSA公钥私钥RSAParameters publicKey; RSAParameters privateKey; rsa.KeySize = 2048; // 设置密钥长度 // 生成密钥对 publicKey = rsa.ExportParameters(false); privateKey = rsa.ExportParameters(true); // 要加密的数据 string dataToEncrypt = "Hello, RSA!"; // 加密数据 byte[] encryptedData = EncryptData(Encoding.UTF8.GetBytes(dataToEncrypt), publicKey); // 解密数据 byte[] decryptedData = DecryptData(encryptedData, privateKey); // 显示结果 Console.WriteLine("原始数据: {0}", dataToEncrypt); Console.WriteLine("加密后的数据: {0}", Convert.ToBase64String(encryptedData)); Console.WriteLine("解密后的数据: {0}", Encoding.UTF8.GetString(decryptedData)); } catch (CryptographicException e) { Console.WriteLine(e.Message); } } } // 使用RSA算法加密数据 public static byte[] EncryptData(byte[] dataToEncrypt, RSAParameters publicKey) { using (RSA rsa = RSA.Create()) { try { rsa.ImportParameters(publicKey); return rsa.Encrypt(dataToEncrypt, RSAEncryptionPadding.OaepSHA256); } catch (CryptographicException e) { Console.WriteLine(e.Message); return null; } } } // 使用RSA算法解密数据 public static byte[] DecryptData(byte[] dataToDecrypt, RSAParameters privateKey) { using (RSA rsa = RSA.Create()) { try { rsa.ImportParameters(privateKey); return rsa.Decrypt(dataToDecrypt, RSAEncryptionPadding.OaepSHA256); } catch (CryptographicException e) { Console.WriteLine(e.Message); return null; } } } } ``` 这个示例代码,我们使用`RSA.Create()`创建了一个RSA实例,并设置了密钥长度为2048位。然后,调用`ExportParameters()`方法生成公钥私钥对。接下来,我们使用公钥加密数据,然后再用私钥解密数据。最后,我们将结果打印到控制台上。 需要注意的是,RSA算法对于较大的数据加密耗时较长,因此通常会使用RSA算法加密对称加密算法(如AES)的密钥,从而提高加密效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值