一、缘由
RSA是一种常用的非对称加密算法。所以有时需要在不用编程语言中分别使用RSA的加密、解密。例如用Java做后台服务端,用C#开发桌面的客户端软件时。
由于 .Net、Java 的RSA类库存在很多细节区别,尤其是它们支持的密钥格式不同。导致容易出现“我加密的数据对方不能解密,对方加密的数据我不能解密,但是自身是可以正常加密解密”等情况。
虽然网上已经有很多文章讨论 .Net与Java互通的RSA加解密,但是存在不够全面、需要第三方dll、方案复杂 等问题。
于是我仔细研究了这一课题,得到了一些稳定可靠的代码。现在将研究成果分享给大家。
二、密钥
2.1 RSA密钥文件格式介绍
要保证 .Net与Java 两端均能正常的加解密,其中的重中之重就是确立一种密钥文件格式,使 .Net与Java 两端均能正确的加载密钥。
.Net与Java内置类库对密钥文件格式的支持情况——
.Net
: 支持xml格式的密钥文件。Java
: 没有直接提供对密钥文件的支持,仅提供了 PKCS#8、X.509 等编码的密钥数据的解析类。
2.1.1 技术细节——密钥文件为什么这么复杂
看到 PKCS#8、X.509,大家是否有些头晕了?
其实RSA的密钥文件不止这2种,还有许多种存储格式。可参考 蒋国纲《那些证书相关的玩意儿(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)》。
为什么RSA密钥文件这么复杂,这是因为密钥文件需存储多个数值。具体来说,RSA加解密中有5个重要的数字 p,q,n(Modulus),e(Exponent),d。然后公钥与私钥分别要存储不同的值——
- 公钥:需存储 n、e。
- 私钥:需存储 n、d。而对于常用的X.509等编码的私钥文件中,其不仅存储了 n、e、d、p、q,还存储了 d mod (p-1)、d mod (q-1)、(inverse of q) mod p 等用于简化、校验加密的值。
所以我们会发现私钥文件的字节数,一般比公钥文件大一些。
为了统一密钥文件格式,我们不得不编写密钥解析代码,这需要理解rsa的p、q、n、e、d 具体含义与用法。学习难度较高,需要一定时间仔细研读。
所以我便封装了一些稳定、可靠的函数来处理这些内容。使下次可以直接用这些函数,不用再次费神处理这些复杂的技术细节。
若想支持绝大多数的密钥文件格式,推荐使用 OpenSSL库。它支持 .Net与Java。
可是,该库比较庞大,项目依赖多会导致部署麻烦,不适合小型程序。所以我们还是选择一种格式比较好。
2.2 确立密钥文件格式
我挑选密钥文件格式有2个条件——
- 文本格式。这样用记事本打开密钥文件,能够方便的复制粘贴,且能作为程序中的字符串常量。使用灵活,方便测试等。
- 易于生成。不必编写、运行代码来生成,而是能够通过多种办法来生成密钥对。既可以命令行生成,又可以通过图形界面工具点击生成。
所以最终选择了 PEM(Privacy Enhanced Mail)格式的密钥文件。用记事本打开可看到文本内容,其以"-----BEGIN..."开头,以"-----END..."结尾,内容是BASE64编码。
随后对于具体的公钥、私钥的编码格式,选择了 PKCS#8 与 X.509,具体情况是——
- 公钥:X.509 pem。Java类为 X509EncodedKeySpec 。
- 私钥:PKCS#8 pem。Java类为 PKCS8EncodedKeySpec 。
2.3 生成密钥
首先,可使用代码来生成密钥对,.Net、Java的类库有完善的支持。该办法适合于自己生成、管理密钥的项目。但对于一些小型项目来说,该办法比较复杂,不太实用。
其次,可以使用 OpenSSL 等命令行工具来生成密钥。需要花点时间来学习命令行,并且需要安装相应工具,稍微有点麻烦。
其实还有第三种方法,就是用在线工具来生成密钥。因为我们用的是PEM格式的密钥,该格式简单,很多在线工具都支持。
例如 http://web.chacuo.net/netrsakeypair
用法——
- 选择“生成密钥位数”。直接使用默认的“2048位”就行,因为2048位是目前主流的密钥位数,且.Net、Java均支持该长度。
- 选择“密钥格式”。直接使用默认的“PKCS#8”就行,因为我们也是采用这种格式。
- 填写“证书密码”。一般不用填写。
- 点击“生成密钥对(RSA)”。随后下面的两个文本框分别会出现公钥与私钥,便可复制粘贴进行保存了。
2.3.1 本文范例用的密钥
公钥(public1.pem)
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAywl5THDMsLUbzYX66YGp
Mr9AaiX6NNHp4gOQMa0BDM125ZftY/YL7ZJT9TgnVegK/vVSJn2PoGTw+x0OMx86
nCXOxX7h7xRt6oVRq3ekN36kBjGm56MFbYpAaLg0LLfPQcZME1g6T8CGCGpSZR90
bwqBh56uRFKa5ptJwLCloCc9fvW4uP6M/CcaRcpRcF0f4ofV/Urvq2l4Id+XxQyr
WX1JgR9mo6dvUaaX9osjZW615t6PlyoewkUUfv5rNTh7wjIZzKLl+pD8YCheZ7aJ
PlJWaIuwSENgVEYEbXcOyCbr2HqWA7EKA5+QxSaVy5z7q5BDpEz8ky3QxRfj+EDJ
VQIDAQAB
-----END PUBLIC KEY-----
私钥(private1.pem)
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDLCXlMcMywtRvN
hfrpgakyv0BqJfo00eniA5AxrQEMzXbll+1j9gvtklP1OCdV6Ar+9VImfY+gZPD7
HQ4zHzqcJc7FfuHvFG3qhVGrd6Q3fqQGMabnowVtikBouDQst89BxkwTWDpPwIYI
alJlH3RvCoGHnq5EUprmm0nAsKWgJz1+9bi4/oz8JxpFylFwXR/ih9X9Su+raXgh
35fFDKtZfUmBH2ajp29Rppf2iyNlbrXm3o+XKh7CRRR+/ms1OHvCMhnMouX6kPxg
KF5ntok+UlZoi7BIQ2BURgRtdw7IJuvYepYDsQoDn5DFJpXLnPurkEOkTPyTLdDF
F+P4QMlVAgMBAAECggEAIbtJM7Hpz9HG9LY1oWWxPoUXpor4rp3RRYNiCV68tevM
vQgooFrYUHfnCu5xWoxah1EqfMqPeg5LGu0Q1t1xV0/Qsm8KCjZSrIvJrbsKxU18
4qqNGB61YCV/3eX8hRFklYDkUrJtvaI2ol9HoRVAutH8AxQRz7gJlBZogmLWoWyX
r5CwPat/6n7mw//LtSblP9A10I8X+1G+9LFF48TKIZWvxkCkiLWiFwqQgbmfVdw8
vtCyMHLb62C3o6qTEjOYGD3xlE5kGPO7AovUihC8e/E5CaR840p+5j12qy62VbG6
7d0KFHIwAF4njhQA1wEWn+C+27lzE1Ps9eb3xlQdYQKBgQDuHCd0UewvL9YF6TYA
y2IuYtwDBlF2TZpJ5+y396ncHhdL90vAeIoDcBlK8zwBuH1M7Ewv3NlcNB1zlT95
itltPqdDkdl4TXboDTWrIhDD5RqiowrLTRSlO1hdZOw9ya88lxLYsUvMrNZzR3zW
T355YzqIC9JQYRu/O7+nysPiGwKBgQDaSrhz13c+PrUeExE34y3cdlN5aZkn3Rw/
MRpQWpV0+9NuTdBizENZ5uW3kCTI5+vk3OmgmCa2Lq48LZjKPa7BffIPK406V1Vs
xSZyzeTRRtaG7+Is1uTyASAimQ/0EIX3HjtZmHSPGeKyvYhKy0M+W1j1zPN1iP6w
Dy1nUMI5TwKBgQDQ5EQ8yQ4yi33w65rj8Ynt9e7cfHOFHSmpgt1qu8z5/jAkBg0g
Ct/Riku2NFPFkqviiz9/kfni6RmZaCsqnwSG0bt+DPtDjnottEEMJLOemGTYn779
gl8FYl3weXTD9CdXOZZgIpLEOjFdKy86+LyVE9equOxGdhsYlvtZ4godVwKBgQCa
ndpQkwlvGVOIXdEQWOWfBmDR2q4UwlTDnbAZwk+icMytkIhNsojyIM4NWxfzBfLc
RG1mxt6EpEPddB6JAW/Ktb7CaAK8lCd5x5sYLiYo5ZgGM9tsDzpS/+EXIHtgUGPT
SaKYL5g/1AHywLTM5XRXsrQsRmMbmVFsuxNZ3qXzmQKBgQDX9MkY7vDz5n27XtIQ
S65K5Wsmoqx5T+xhxQ9pRSbHm9t7cAO0We5sMLsAIjt1vKNBSeYLgxtqdEUcylb5
bZNVj5+qQFzcBh9yl7HtcAe3IkBvkrTAkonHN7gNqXKFUGlFkEFTBJm8IiSeUB9E
J99XfDatcok6GddO++ZMowAAJQ==
-----END PRIVATE KEY-----
2.4 Java加载密钥
2.4.1 PEM解包
对于解析密钥文件,第一个重要步骤就是进行PEM解包。这是因为PEM文件是以“-----BEGIN”开头、“-----END”结尾的,而实际的密钥数据是以BASE64编码的形式给放在中间的。
由于Java没有直接提供对密钥文件的支持,仅提供了 PKCS#8、X.509 等编码的密钥数据的解析类。于是需要我们自己来做PEM解包。
我观察了网上的PEM解包的源码,发现它们一般是用字符串数组存储“-----BEGIN”的各种模式,然后根据该数组查找字符串来来定位数据的。但该办法并不稳定,容易遇到问题——
- BEGIN后面的文本内容不规范。例如有写成“-----BEGIN PUBLIC KEY”开头的,有写成“-----BEGIN RSA PUBLIC KEY”开头的,还有其他各种五花八门的模式。
- BEGIN(或END)前后的减号(
-
)长度不定。不同工具生成的PEM文件中,减号(-
)长度是不同的。 - 有时中间会有多余的空格等空白字符。
于是我写了个状态机算法来解析PEM数据。这样便能处理各种意外,提高稳定性。
另外,该算法还增加自动判断是公钥还是私钥的功能。由于Java函数不允许返回多个值,所以用了一个Map来传递多余的返回值。
/** 用途文本. 如“BEGIN PUBLIC KEY”中的“PUBLIC KEY”. */
public final static String PURPOSE_TEXT = "PURPOSE_TEXT";
/** 用途代码. R私钥, U公钥. */
public final static String PURPOSE_CODE = "PURPOSE_CODE";
/** PEM解包.
*
* <p>从PEM密钥数据中解包得到纯密钥数据. 即去掉BEGIN/END行,并作BASE64解码. 若没有BEGIN/END, 则直接做BASE64解码.</p>
*
* @param data 源数据.
* @param otherresult 其他返回值. 支持 PURPOSE_TEXT, PURPOSE_CODE。
* @return 返回解包后的纯密钥数据.
*/
public static byte[] PemUnpack(String data, Map<String, String> otherresult) {
byte[] rt = null;
final String SIGN_BEGIN = "-BEGIN";
final String SIGN_END = "-END";
int datelen = data.length();
String purposetext = "";
String purposecode = "";
if (null!=otherresult) {
purposetext = otherresult.get(PURPOSE_TEXT);
purposecode = otherresult.get(PURPOSE_CODE);
if (null==purposetext) purposetext= "";
if (null==purposecode) purposecode= "";
}
// find begin.
int bodyPos = 0; // 主体内容开始的地方.
int beginPos = data.indexOf(SIGN_BEGIN);
if (beginPos>=0) {
// 向后查找换行符后的首个字节.
boolean isFound = false;
boolean hadNewline = false; // 已遇到过换行符号.
boolean hyphenHad = false; // 已遇到过“-”符号.
boolean hyphenDone = false; // 已成功获取了右侧“-”的范围.
int p = beginPos + SIGN_BEGIN.length();
int hyphenStart = p; // 右侧“-”的开始位置.
int hyphenEnd = hyphenStart; // 右侧“-”的结束位置. 即最后一个“-”字符的位置+1.
while(p<datelen) {
char ch = data.charAt(p);
// 查找右侧“-”的范围.
if (!hyphenDone) {
if (ch=='-') {
if (!hyphenHad) {
hyphenHad = true;
hyphenStart = p;
hyphenEnd = hyphenStart;
}
} else {
if (hyphenHad) { // 无需“&& !hyphenDone”,因为外层判断了.
hyphenDone = true;
hyphenEnd = p;
}
}
}
// 向后查找换行符后的首个字节.
if (ch=='
' || ch=='
') {
hadNewline = true;
} else {
if (hadNewline) {
// 找到了.
bodyPos = p;
isFound = true;
break;
}
}
// next.
++p;
}
// purposetext
if (hyphenDone && null!=otherresult) {
purposetext = data.substring(beginPos + SIGN_BEGIN.length(), hyphenStart).trim();
String purposetextUp = purposetext.toUpperCase();
if (purposetextUp.indexOf("PRIVATE")>=0) {
purposecode = "R";
} else if (purposetextUp.indexOf("PUBLIC")>=0) {
purposecode = "U";
}
otherresult.put(PURPOSE_TEXT, purposetext);
otherresult.put(PURPOSE_CODE, purposecode);
}
// bodyPos.
if (isFound) {
//OK.
} else if (hyphenDone) {
// 以右侧右侧“-”的结束位置作为主体开始.
bodyPos = hyphenEnd;
} else {
// 找不到结束位置,只能退出.
return rt;
}
}
// find end.
int bodyEnd = datelen; // 主体内容的结束位置. 即最后一个字符的位置+1.
int endPos = data.indexOf(SIGN_END, bodyPos);
if (endPos>=0) {
// 向前查找换行符前的首个字节.
boolean isFound = false;
boolean hadNewline = false;
int p = endPos-1;
while(p >= bodyPos) {
char ch = data.charAt(p);
if (ch=='
' || ch=='
') {
hadNewline = true;
} else {
if (hadNewline) {
// 找到了.
bodyEnd = p+1;
break;
}
}
// next.
--p;
}
if (!isFound) {
// 忽略.
}
}
// get body.
if (bodyPos>=bodyEnd) {
return rt;
}
String body = data.substring(bodyPos, bodyEnd).trim();
// Decode BASE64.
rt = Base64.decode(body.getBytes());
return rt;
}
2.4.2 加载公钥
PemUnpack解出纯密钥数据后,便可分别加载公钥与私钥了。
由于Java提供了X509EncodedKeySpec,加载公钥是比较简单的。
下面代码中的strDataKey为PEM文本内容,最后的 key 就是公钥对象。
Map<String, String> map = new HashMap<String, String>();
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
key = kf.generatePublic(spec);
2.4.3 加载私钥
由于Java提供了PKCS8EncodedKeySpec,加载私钥是比较简单的。
下面代码中的strDataKey为PEM文本内容,最后的 key就是私钥对象。
Map<String, String> map = new HashMap<String, String>();
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
key = kf.generatePrivate(spec);
2.4.4 判断密钥位数
密钥位数是一个很重要的数值,很多地方都要用到。可是Java没有简单的提供该属性,而是需要一些步骤来得到,且公钥、私钥得使用不同的类。
- 调用 KeyFactory.getKeySpec 方法,传递EncodedKeySpec(公钥为X509EncodedKeySpec,私钥为PKCS8EncodedKeySpec),获取 KeySpec(公钥为RSAPublicKeySpec,私钥为RSAPrivateKeySpec)。
- 随后调用 KeySpec对象的 getModulus 方法获取 Modulus(即n)。
- 获取 Modulus(即n)的位数,它就是密钥位数。
范例代码如下——
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
int keysize;
// 公钥.
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
key = kf.generatePublic(spec);
RSAPublicKeySpec keySpec = (RSAPublicKeySpec)kf.getKeySpec(key, RSAPublicKeySpec.class);
keysize = keySpec.getModulus().bitLength();
// 私钥.
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
key = kf.generatePrivate(spec);
RSAPrivateKeySpec keySpec = (RSAPrivateKeySpec)kf.getKeySpec(key, RSAPrivateKeySpec.class);
keysize = keySpec.getModulus().bitLength();
2.4.4 小结
刚才讲解了加载密钥过程中的各个关键步骤,现在来将它们组合起来吧。演示一下完整的密钥加载过程。
参数说明——
fileKey
: 密钥文件.
String strDataKey = new String(ZlRsaUtil.fileLoadBytes(fileKey));
Map<String, String> map = new HashMap<String, String>();
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
String purposecode = map.get(ZlRsaUtil.PURPOSE_CODE);
//out.println(bytesKey);
// key.
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
int keysize;
if ("R".equals(purposecode)) {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
key = kf.generatePrivate(spec);
RSAPrivateKeySpec keySpec = (RSAPrivateKeySpec)kf.getKeySpec(key, RSAPrivateKeySpec.class);
keysize = keySpec.getModulus().bitLength();
} else {
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
key = kf.generatePublic(spec);
RSAPublicKeySpec keySpec = (RSAPublicKeySpec)kf.getKeySpec(key, RSAPublicKeySpec.class);
keysize = keySpec.getModulus().bitLength();
}
System.out.println(String.format("keysize: %d", keysize));
System.out.println(String.format("key.getAlgorithm: %s", key.getAlgorithm()));
System.out.println(String.format("key.getFormat: %s", key.getFormat()));
其中的 ZlRsaUtil.fileLoadBytes 是一个加载文件的函数。严格来说,是加载文件的二进制数据。因为PEM文件是纯ASCII的,故可以简单的通过new String
的方式转为字符串。
/**
* RSA .
*/
public final static String RSA = "RSA";
/** 加载文件中的所有字节.
*
* @param filename 文件名.
* @return 返回文件内容的字节数组.
* @throws IOException IO异常.
*/
public static byte[] fileLoadBytes(String filename) throws IOException {
byte[] rt = null;
File file = new File(filename);
long fileSize = file.length();
if (fileSize > Integer.MAX_VALUE) {
throw new IOException(filename + " file too big...");
}
FileInputStream fi = new FileInputStream(filename);
try {
rt = new byte[(int) fileSize];
int offset = 0;
int numRead = 0;
while (offset < rt.length
&& (numRead = fi.read(rt, offset, rt.length - offset)) >= 0) {
offset += numRead;
}
// 确保所有数据均被读取
if (offset != rt.length) {
throw new IOException("Could not completely read file " + file.getName());
}
}finally{
try {
fi.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return rt;
}
2.5 .Net加载密钥
2.5.1 PEM解包
.Net里仅提供对Xml密钥文件的支持,所以我们得自己编写PEM的解包代码。
同样是因为网上范例代码考虑的不周全,于是我写了个状态机算法来解析PEM数据。能处理各种意外,提高了稳定性。
/// <summary>
/// PEM解包.
/// </summary>
/// <para>从PEM密钥数据中解包得到纯密钥数据. 即去掉BEGIN/END行,并作BASE64解码. 若没有BEGIN/END, 则直接做BASE64解码.</para>
/// <param name="data">源数据.</param>
/// <param name="purposetext">用途文本. 如返回“BEGIN PUBLIC KEY”中的“PUBLIC KEY”.</param>
/// <param name="purposecode">用途代码. R私钥, U公钥. 若无法识别,便保持原值.</param>
/// <returns>返回解包后的纯密钥数据.</returns>
/// <exception cref="System.ArgumentNullException">data is empty, or data body is empty.</exception>
/// <exception cref="System.FormatException">data body is not BASE64.</exception>
public static byte[] PemUnpack(String data, ref string purposetext, ref char purposecode) {
byte[] rt = null;
const string SIGN_BEGIN = "-BEGIN";
const string SIGN_END = "-END";
if (String.IsNullOrEmpty(data)) throw new ArgumentNullException("data", "data is empty!");
int datelen = data.Length;
// find begin.
int bodyPos = 0; // 主体内容开始的地方.
int beginPos = data.IndexOf(SIGN_BEGIN, StringComparison.OrdinalIgnoreCase);
if (beginPos >= 0) {
// 向后查找换行符后的首个字节.
bool isFound = false;
bool hadNewline = false; // 已遇到过换行符号.
bool hyphenHad = false; // 已遇到过“-”符号.
bool hyphenDone = false; // 已成功获取了右侧“-”的范围.
int p = beginPos + SIGN_BEGIN.Length;
int hyphenStart = p; // 右侧“-”的开始位置.
int hyphenEnd = hyphenStart; // 右侧“-”的结束位置. 即最后一个“-”字符的位置+1.
while (p < datelen) {
char ch = data[p];
// 查找右侧“-”的范围.
if (!hyphenDone) {
if (ch == '-') {
if (!hyphenHad) {
hyphenHad = true;
hyphenStart = p;
hyphenEnd = hyphenStart;
}
} else {
if (hyphenHad) { // 无需“&& !hyphenDone”,因为外层判断了.
hyphenDone = true;
hyphenEnd = p;
}
}
}
// 向后查找换行符后的首个字节.
if (ch == '
' || ch == '
') {
hadNewline = true;
} else {
if (hadNewline) {
// 找到了.
bodyPos = p;
isFound = true;
break;
}
}
// next.
++p;
}
// purposetext
if (hyphenDone) {
int start = beginPos + SIGN_BEGIN.Length;
purposetext = data.Substring(start, hyphenStart - start).Trim();
string purposetextUp = purposetext.ToUpperInvariant();
if (purposetextUp.IndexOf("PRIVATE") >= 0) {
purposecode = 'R';
} else if (purposetextUp.IndexOf("PUBLIC") >= 0) {
purposecode = 'U';
}
}
// bodyPos.
if (isFound) {
//OK.
} else if (hyphenDone) {
// 以右侧右侧“-”的结束位置作为主体开始.
bodyPos = hyphenEnd;
} else {
// 找不到结束位置,只能退出.
return rt;
}
}
// find end.
int bodyEnd = datelen; // 主体内容的结束位置. 即最后一个字符的位置+1.
int endPos = data.IndexOf(SIGN_END, bodyPos);
if (endPos >= 0) {
// 向前查找换行符前的首个字节.
bool isFound = false;
bool hadNewline = false;
int p = endPos - 1;
while (p >= bodyPos) {
char ch = data[p];
if (ch == '
' || ch == '
') {
hadNewline = true;
} else {
if (hadNewline) {
// 找到了.
bodyEnd = p + 1;
break;
}
}
// next.
--p;
}
if (!isFound) {
// 忽略.
}
}
// get body.
if (bodyPos >= bodyEnd) {
return rt;
}
string body = data.Substring(bodyPos, bodyEnd - bodyPos).Trim();
// Decode BASE64.
if (String.IsNullOrEmpty(body)) throw new ArgumentNullException("data", "data body is empty!");
rt = Convert.FromBase64String(body);
return rt;
}
2.5.2 加载公钥
由于.Net平台没有提供 X.509 的解码类,故需要自己编写。
我参考网上代码,写了一个公钥的解码函数。
/// <summary>
/// 根据PEM纯密钥数据,获取公钥的RSA加解密对象.
/// </summary>
/// <param name="pubcdata">公钥数据</param>
/// <returns>返回公钥的RSA加解密对象.</returns>
public static RSACryptoServiceProvider PemDecodePublicKey(byte[] pubcdata) {
byte[] SeqOID = { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 };
MemoryStream ms = new MemoryStream(pubcdata);
BinaryReader reader = new BinaryReader(ms);
if (reader.ReadByte() == 0x30)
ReadASNLength(reader); //skip the size
else
return null;
int identifierSize = 0; //total length of Object Identifier section
if (reader.ReadByte() == 0x30)
identifierSize = ReadASNLength(reader);
else
return null;
if (reader.ReadByte() == 0x06) { //is the next element an object identifier?
int oidLength = ReadASNLength(reader);
byte[] oidBytes = new byte[oidLength];
reader.Read(oidBytes, 0, oidBytes.Length);
if (!SequenceEqualByte(oidBytes, SeqOID)) //is the object identifier rsaEncryption PKCS#1?
return null;
int remainingBytes = identifierSize - 2 - oidBytes.Length;
reader.ReadBytes(remainingBytes);
}
if (reader.ReadByte() == 0x03) { //is the next element a bit string?
ReadASNLength(reader); //skip the size
reader.ReadByte(); //skip unused bits indicator
if (reader.ReadByte() == 0x30) {
ReadASNLength(reader); //skip the size
if (reader.ReadByte() == 0x02) { //is it an integer?
int modulusSize = ReadASNLength(reader);
byte[] modulus = new byte[modulusSize];
reader.Read(modulus, 0, modulus.Length);
if (modulus[0] == 0x00) {//strip off the first byte if it's 0
byte[] tempModulus = new byte[modulus.Length - 1];
Array.Copy(modulus, 1, tempModulus, 0, modulus.Length - 1);
modulus = tempModulus;
}
if (reader.ReadByte() == 0x02) { //is it an integer?
int exponentSize = ReadASNLength(reader);
byte[] exponent = new byte[exponentSize];
reader.Read(exponent, 0, exponent.Length);
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
RSAParameters RSAKeyInfo = new RSAParameters();
RSAKeyInfo.Modulus = modulus;
RSAKeyInfo.Exponent = exponent;
RSA.ImportParameters(RSAKeyInfo);
return RSA;
}
}
}
}
return null;
}
/// <summary>
/// Read ASN Length.
/// </summary>
/// <param name="reader">reader</param>
/// <returns>Return ASN Length.</returns>
private static int ReadASNLength(BinaryReader reader) {
//Note: this method only reads lengths up to 4 bytes long as
//this is satisfactory for the majority of situations.
int length = reader.ReadByte();
if ((length & 0x00000080) == 0x00000080) { //is the length greater than 1 byte
int count = length & 0x0000000f;
byte[] lengthBytes = new byte[4];
reader.Read(lengthBytes, 4 - count, count);
Array.Reverse(lengthBytes); //
length = BitConverter.ToInt32(lengthBytes, 0);
}
return length;
}
/// <summary>
/// 字节数组内容是否相等.
/// </summary>
/// <param name="a">数组a</param>
/// <param name="b">数组b</param>
/// <returns>返回是否相等.</returns>
private static bool SequenceEqualByte(byte[] a, byte[] b) {
var len1 = a.Length;
var len2 = b.Length;
if (len1 != len2) {
return false;
}
for (var i = 0; i < len1; i++) {
if (a[i] != b[i])
return false;
}
return true;
}
2.5.3 加载私钥
.Net平台也没有提供 PKCS#8 的解码类,也需要自己编写。
我最初测试了很多网上的私钥解码代码,均不能正常工作。直到后来查了 OpenSSL 的源码,才找到了解决办法。发现这是因为PKCS#8的私钥数据,其实还嵌套了一层X.509编码,故得按顺序分别进行解码。
/// <summary>
/// 解码 PKCS#8 编码的私钥,获取私钥的RSA加解密对象.
/// </summary>
/// <param name="privkey">私钥数据。</param>
/// <returns>返回私钥的RSA加解密对象. 失败时返回null.</returns>
public static RSACryptoServiceProvider PemDecodePkcs8PrivateKey(byte[] pkcs8) {
// encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
// this byte[] includes the sequence byte and terminal encoded null
byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
byte[] seq = new byte[15];
// --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------
MemoryStream mem = new MemoryStream(pkcs8);
int lenstream = (int)mem.Length;
BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading
byte bt = 0;
ushort twobytes = 0;
try {
twobytes = binr.ReadUInt16();
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
binr.ReadByte(); //advance 1 byte
else if (twobytes == 0x8230)
binr.ReadInt16(); //advance 2 bytes
else
return null;
bt = binr.ReadByte();
if (bt != 0x02)
return null;
twobytes = binr.ReadUInt16();
if (twobytes != 0x0001)
return null;
seq = binr.ReadBytes(15); //read the Sequence OID
if (!SequenceEqualByte(seq, SeqOID)) //make sure Sequence for OID is correct
return null;
bt = binr.ReadByte();
if (bt != 0x04) //expect an Octet string
return null;
bt = binr.ReadByte(); //read next byte, or next 2 bytes is 0x81 or 0x82; otherwise bt is the byte count
if (bt == 0x81)
binr.ReadByte();
else
if (bt == 0x82)
binr.ReadUInt16();
//------ at this stage, the remaining sequence should be the RSA private key
byte[] rsaprivkey = binr.ReadBytes((int)(lenstream - mem.Position));
RSACryptoServiceProvider rsacsp = PemDecodeX509PrivateKey(rsaprivkey);
return rsacsp;
} finally { binr.Close(); }
}
/// <summary>
/// 解码 X.509 编码的私钥,获取私钥的RSA加解密对象.
/// </summary>
/// <param name="privkey">私钥数据。</param>
/// <returns>返回私钥的RSA加解密对象. 失败时返回null.</returns>
public static RSACryptoServiceProvider PemDecodeX509PrivateKey(byte[] privkey)
{
byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;
// --------- Set up stream to decode the asn.1 encoded RSA private key ------
MemoryStream mem = new MemoryStream(privkey);
BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading
byte bt = 0;
ushort twobytes = 0;
int elems = 0;
try
{
twobytes = binr.ReadUInt16();
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
binr.ReadByte(); //advance 1 byte
else if (twobytes == 0x8230)
binr.ReadInt16(); //advance 2 bytes
else
return null;
twobytes = binr.ReadUInt16();
if (twobytes != 0x0102) //version number
return null;
bt = binr.ReadByte();
if (bt != 0x00)
return null;
//------ all private key components are Integer sequences ----
elems = GetIntegerSize(binr);
MODULUS = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
E = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
D = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
P = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
Q = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
DP = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
DQ = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
IQ = binr.ReadBytes(elems);
// ------- create RSACryptoServiceProvider instance and initialize with public key -----
CspParameters CspParameters = new CspParameters();
CspParameters.Flags = CspProviderFlags.UseMachineKeyStore;
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(1024, CspParameters);
RSAParameters RSAparams = new RSAParameters();
RSAparams.Modulus = MODULUS;
RSAparams.Exponent = E;
RSAparams.D = D;
RSAparams.P = P;
RSAparams.Q = Q;
RSAparams.DP = DP;
RSAparams.DQ = DQ;
RSAparams.InverseQ = IQ;
RSA.ImportParameters(RSAparams);
return RSA;
}
finally
{
binr.Close();
}
}
/// <summary>
/// 取得整数大小.
/// </summary>
/// <param name="binr">BinaryReader</param>
/// <returns>返回整数大小.</returns>
private static int GetIntegerSize(BinaryReader binr)
{
byte bt = 0;
byte lowbyte = 0x00;
byte highbyte = 0x00;
int count = 0;
bt = binr.ReadByte();
if (bt != 0x02) //expect integer
return 0;
bt = binr.ReadByte();
if (bt == 0x81)
count = binr.ReadByte(); // data size in next byte
else
if (bt == 0x82)
{
highbyte = binr.ReadByte(); // data size in next 2 bytes
lowbyte = binr.ReadByte();
byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
count = BitConverter.ToInt32(modint, 0);
}
else
{
count = bt; // we already have the data size
}
while (binr.ReadByte() == 0x00)
{ //remove high order zeros in data
count -= 1;
}
binr.BaseStream.Seek(-1, SeekOrigin.Current); //last ReadByte wasn't a removed zero, so back up a byte
return count;
}
2.5.4 判断密钥位数
在 .Net中,访问 RSACryptoServiceProvider.KeySize 便可得到密钥位数,非常简单。
int keysize = rsa.KeySize;
2.5.4 小结
刚才讲解了加载密钥过程中的各个关键步骤,现在来将它们组合起来吧。演示一下完整的密钥加载过程。
参数说明——
fileKey
: 密钥文件.
string strDataKey = File.ReadAllText(fileKey);
string purposetext = null;
char purposecode = '