作者: zyl910

一、缘由

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个条件——

  1. 文本格式。这样用记事本打开密钥文件,能够方便的复制粘贴,且能作为程序中的字符串常量。使用灵活,方便测试等。
  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

用法——

  1. 选择“生成密钥位数”。直接使用默认的“2048位”就行,因为2048位是目前主流的密钥位数,且.Net、Java均支持该长度。
  2. 选择“密钥格式”。直接使用默认的“PKCS#8”就行,因为我们也是采用这种格式。
  3. 填写“证书密码”。一般不用填写。
  4. 点击“生成密钥对(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”的各种模式,然后根据该数组查找字符串来来定位数据的。但该办法并不稳定,容易遇到问题——

  1. BEGIN后面的文本内容不规范。例如有写成“-----BEGIN PUBLIC KEY”开头的,有写成“-----BEGIN RSA PUBLIC KEY”开头的,还有其他各种五花八门的模式。
  2. BEGIN(或END)前后的减号(-)长度不定。不同工具生成的PEM文件中,减号(-)长度是不同的。
  3. 有时中间会有多余的空格等空白字符。

于是我写了个状态机算法来解析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=='\n' || ch=='\r') {
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=='\n' || ch=='\r') {
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没有简单的提供该属性,而是需要一些步骤来得到,且公钥、私钥得使用不同的类。

  1. 调用 KeyFactory.getKeySpec 方法,传递EncodedKeySpec(公钥为X509EncodedKeySpec,私钥为PKCS8EncodedKeySpec),获取 KeySpec(公钥为RSAPublicKeySpec,私钥为RSAPrivateKeySpec)。
  2. 随后调用 KeySpec对象的 getModulus 方法获取 Modulus(即n)。
  3. 获取 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 == '\n' || ch == '\r') {
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 == '\n' || ch == '\r') {
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 = '\0';
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, ref purposetext, ref purposecode);
//export.WriteLine(bytesKey);
// key.
RSACryptoServiceProvider rsa;
if ('R' == purposecode) {
rsa = ZlRsaUtil.PemDecodePkcs8PrivateKey(bytesKey); // try
if (null == rsa) {
rsa = ZlRsaUtil.PemDecodeX509PrivateKey(bytesKey);
}
} else { // 公钥或无法判断时, 均当成公钥处理.
rsa = ZlRsaUtil.PemDecodePublicKey(bytesKey);
}
if (null == rsa) {
export.WriteLine("Key decode fail!");
return;
}
export.WriteLine(string.Format("KeyExchangeAlgorithm: {0}", rsa.KeyExchangeAlgorithm));
export.WriteLine(string.Format("KeySize: {0}", rsa.KeySize));

三、加解密

3.1 确立加密模式与填充方式

虽然都是RSA算法,但是若加密模式与填充方式不同的话,会导致加密结果不匹配。所以需要确定好 .Net与Java 均支持的方式。

加密模式一般有 ECB/CBC/CFB/OFB 这四种。对于RSA来说,ECB最简单但安全性比较薄弱,而CBC等模式就很复杂且还需考虑IV(initialization vector,初始化向量)的管理。所以一般情况下可以用 ECB 模式,.Net与Java均支持它,且ECB是.Net的默认模式。

由于加密算法都是按块来处理的,故理论上只有当明文长度正好是块长度的倍数时才能进行加解密。但那样太麻烦了,故有了填充方式的概念,即在明文后面填充一些数据,使其长度正好是块的倍数。填充方式还有2个作用,一是能标记原始数据长度使解码时自动去掉末尾的填充数据,二是能提高安全性。

.Net的RSA算法默认是使用PKCS#1填充方式的,故Java中可选择 PKCS1Padding 填充方式。

现在算法已经确定了,Java中可定义这些常数。

	/**
* RSA .
*/
public final static String RSA = "RSA"; /**
* 具体的 RSA 算法.
*/
public final static String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";

3.2 分段加密

对于.Net、Java自带的RSA库来说,填充方式只是解决了“明文长度小于块尺寸”的问题。而当明文长度大于块尺寸时,便会抛出异常,常见的异常信息有——

// .Net
不正确的长度 // Java
javax.crypto.IllegalBlockSizeException: Data must not be longer than 117 bytes
javax.crypto.IllegalBlockSizeException: Data must not be longer than 245 bytes

此时便需要对数据进行分段加密。

3.2.1 块尺寸的计算

密文的块尺寸是很容易计算的,即“密钥位数/8”。即把二进制长度转为字节长度。

而明文的块尺寸的计算就稍微麻烦了一点,与填充方式有关。因目前使用了PKCS#1填充方式,该方式需占用11个字节。于是块尺寸为“密钥位数/8 - 11”。

例如密钥长度为2048位时——

  • 密文的块尺寸 = 密钥位数/8 = 2048/8 = 256
  • 明文的块尺寸 = 密钥位数/8 - 11 = 2048/8 - 11 = 256 - 11 = 245

即——

  • 加密时:明文的块为245字节,加密后输出的密文块为256字节。
  • 解密时:密文的块为256字节,解密后输出的明文块为245字节。

3.3 Java加解密

3.3.1 加密

	/** RSA加密. 当数据较长时, 能自动分段加密.
*
* @param cipher 加解密服务提供者. 需是已初始化的, 即已经调了init的.
* @param keysize 密钥长度. 例如2048位的RSA,传2048 .
* @param data 欲加密的数据.
* @return 返回加密后的数据.
* @throws BadPaddingException On Cipher.doFinal
* @throws IllegalBlockSizeException On Cipher.doFinal
*/
public static byte[] encrypt(Cipher cipher, int keysize, byte[] data) throws IllegalBlockSizeException, BadPaddingException {
byte[] cipherBytes = null;
int blockSize = keysize/8 - 11; // RSA加密时支持的最大字节数:证书位数/8 -11(比如:2048位的证书,支持的最大加密字节数:2048/8 - 11 = 245).
if (data.length <= blockSize) {
// 整个加密.
cipherBytes = cipher.doFinal(data);
} else {
// 分段加密.
int inputLen = data.length;
ByteArrayOutputStream ostm = new ByteArrayOutputStream();
try {
for(int offSet = 0; inputLen - offSet > 0; ) {
int len = inputLen - offSet;
if (len>blockSize) len=blockSize;
byte[] cache = cipher.doFinal(data, offSet, len);
ostm.write(cache, 0, cache.length);
// next.
offSet += len;
}
cipherBytes = ostm.toByteArray();
}finally {
try {
ostm.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return cipherBytes;
}

3.3.2 解密

	/** RSA解密. 当数据较长时, 能自动分段解密.
*
* @param cipher 加解密服务提供者. 需是已初始化的, 即已经调了init的.
* @param keysize 密钥长度. 例如2048位的RSA,传2048 .
* @param data 欲解密的数据.
* @return 返回解密后的数据.
* @throws BadPaddingException On Cipher.doFinal
* @throws IllegalBlockSizeException On Cipher.doFinal
*/
public static byte[] decrypt(Cipher cipher, int keysize, byte[] data) throws IllegalBlockSizeException, BadPaddingException {
byte[] cipherBytes = null;
int blockSize = keysize/8;
if (data.length <= blockSize) {
// 整个加密.
cipherBytes = cipher.doFinal(data);
} else {
// 分段加密.
int inputLen = data.length;
ByteArrayOutputStream ostm = new ByteArrayOutputStream();
try {
for(int offSet = 0; inputLen - offSet > 0; ) {
int len = inputLen - offSet;
if (len>blockSize) len=blockSize;
byte[] cache = cipher.doFinal(data, offSet, len);
ostm.write(cache, 0, cache.length);
// next.
offSet += len;
}
cipherBytes = ostm.toByteArray();
}finally {
try {
ostm.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return cipherBytes;
}

3.4 .Net加解密

3.3.1 加密

		/// <summary>
/// RSA加密. 当数据较长时, 能自动分段加密.
/// </summary>
/// <param name="rsa">加解密服务提供者. 需是已初始化的.</param>
/// <param name="data">欲加密的数据.</param>
/// <returns>返回加密后的数据.</returns>
/// <exception cref="System.Security.Cryptography.CryptographicException">On RSACryptoServiceProvider.Encrypt .</exception>
public static byte[] Encrypt(RSACryptoServiceProvider rsa, byte[] data) {
byte[] cipherBytes = null;
int keysize = rsa.KeySize;
int blockSize = keysize / 8 - 11; // RSA加密时支持的最大字节数:证书位数/8 -11(比如:2048位的证书,支持的最大加密字节数:2048/8 - 11 = 245).
if (data.Length <= blockSize) {
// 整个加密.
cipherBytes = rsa.Encrypt(data, false);
} else {
// 分段加密.
int inputLen = data.Length;
using (MemoryStream ostm = new MemoryStream()) {
for (int offSet = 0; inputLen - offSet > 0; ) {
int len = inputLen - offSet;
if (len > blockSize) len = blockSize;
byte[] tmp = new byte[len];
Array.Copy(data, offSet, tmp, 0, len);
byte[] cache = rsa.Encrypt(tmp, false);
ostm.Write(cache, 0, cache.Length);
// next.
offSet += len;
}
ostm.Position = 0;
cipherBytes = ostm.ToArray();
}
}
return cipherBytes;
}

3.3.2 解密

		/// <summary>
/// RSA解密. 当数据较长时, 能自动分段解密.
/// </summary>
/// <param name="rsa">加解密服务提供者. 需是已初始化的.</param>
/// <param name="data">欲解密的数据.</param>
/// <returns>返回解密后的数据.</returns>
/// <exception cref="System.Security.Cryptography.CryptographicException">On RSACryptoServiceProvider.Encrypt .</exception>
public static byte[] Decrypt(RSACryptoServiceProvider rsa, byte[] data) {
byte[] cipherBytes = null;
int keysize = rsa.KeySize;
int blockSize = keysize / 8;
if (data.Length <= blockSize) {
// 整个解密.
cipherBytes = rsa.Decrypt(data, false);
} else {
// 分段解密.
int inputLen = data.Length;
using (MemoryStream ostm = new MemoryStream()) {
for (int offSet = 0; inputLen - offSet > 0; ) {
int len = inputLen - offSet;
if (len > blockSize) len = blockSize;
byte[] tmp = new byte[len];
Array.Copy(data, offSet, tmp, 0, len);
byte[] cache = rsa.Decrypt(tmp, false);
ostm.Write(cache, 0, cache.Length);
// next.
offSet += len;
}
ostm.Position = 0;
cipherBytes = ostm.ToArray();
}
}
return cipherBytes;
}

四、测试验证

4.1 编程测试

为了验证.Net、Java的加解密代码是否吻合,最好是写一个测试程序进行验证。然后便可分别测试——

  • Java 端加密生成密文文件,随后 Java 端读取密文文件做解密。
  • .Net 端加密生成密文文件,随后 .Net 端读取密文文件做解密。
  • Java 端加密生成密文文件,随后 .Net 端读取密文文件做解密。
  • .Net 端加密生成密文文件,随后 Java 端读取密文文件做解密。

这4种测试都通过后,便表示加解密没问题。可稳定的运行在.Net、Java通讯的场景下。

4.1.1 命令行设计

为了方便多次重复测试,于是将该程序设计为命令行程序。这样便能灵活的做各种测试。

该程序命名为 rsapemdemo。用法为 rsapemdemo [options] srcfile

命令的范例——

# 使用公钥进行加密
rsapemdemo -e -l publickey.pem -o dstfile srcfile # 使用私钥进行解密
rsapemdemo -d -l privatekey.pem -o dstfile srcfile

参数说明——

-e:RSA加密,并进行BASE64编码。因加密后得到的二进制数据不易查看、复制,故再做了一次BASE64编码。
-d:BASE64解码,并进行RSA解密。
-l [keyfile]:加载密钥文件。
-o [outfile]:指定输出文件。
srcfile:源文件名。

实际测试时所使用的命令行——

rsapemdemo -e -l "E:\rsapemdemo\data\public1.pem" -o "E:\rsapemdemo\data\src1_pub.log" "E:\rsapemdemo\data\src1.txt"
rsapemdemo -e -l "E:\rsapemdemo\data\private1.pem" -o "E:\rsapemdemo\data\src1_pri.log" "E:\rsapemdemo\data\src1.txt" rsapemdemo -d -l "E:\rsapemdemo\data\public1.pem" -o "E:\rsapemdemo\data\src1_pri_d.log" "E:\rsapemdemo\data\src1_pri.log"
rsapemdemo -d -l "E:\rsapemdemo\data\private1.pem" -o "E:\rsapemdemo\data\src1_pub_d.log" "E:\rsapemdemo\data\src1_pub.log"

4.1.2 Java的测试办法

在Eclipse中打开项目。

双击打开含有main函数的文件(RsaPemDemo.java),然后在源码区域右击鼠标,在弹出菜单中选择“Debug As -> Debug Configurations”。

“Debug Configurations”对话框打开后,切换到“Arguments”页,在“Program arguments”文本框中输入命令行参数(不用输入程序名,只需输入后面的参数)。

随后便可点击“Debug”按钮进行调试了。

4.1.3 .Net的测试办法

在VS中打开项目。

点击菜单栏的“项目->属性”。

属性对话框打开后,切换到“调试”页,在“命令行参数”文本框中输入命令行参数(不用输入程序名,只需输入后面的参数)。

随后便可按F5调试了。

测试后发现——

  • .NET 的RSA,仅支持公钥加密、私钥解密。若用私钥加密,则仍是返回公钥加密结果。若用公钥解密,会出现 System.Security.Cryptography.CryptographicException: 不正确的项。 异常.

4.2 在线测试

除了自己编码测试外,还可以使用RSA在线工具进行对比测试。检测我们测试程序所生成的密文,是否能被在线工具解密,或者让在线工具生成密文由我们程序进行解密。

例如可利用这个网站进行测试——

# 在线RSA公钥加密解密、RSA public key encryption and decryption
http://tool.chacuo.net/cryptrsapubkey # 在线RSA私钥加密解密、RSA private key encryption and decryption
http://tool.chacuo.net/cryptrsaprikey

附录、测试程序的主体源码

附录.1 Java版

package rsapemdemo;

import java.io.IOException;
import java.io.PrintStream;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException; /** Java/.NET RSA demo, use pem key file (Java/.NET的RSA加解密演示项目,使用pem格式的密钥文件).
*
* @author zyl910
* @since 2017-10-27
*
*/
public class RsaPemDemo {
/** 帮助文本. */
private static final String helpText = "Usage: rsapemdemo [options] srcfile\n\nFor example:\n\n # encode by public key\n rsapemdemo -e -l publickey.pem -o dstfile srcfile\n\n # decode by private key\n rsapemdemo -d -l privatekey.pem -o dstfile srcfile\n\nThe options:\n\n -e RSA encryption and BASE64 encode.\n -d BASE64 decode and RSA decryption.\n -l [keyfile] Load key file.\n -o [outfile] out file.\n"; /** 是否为空.
*
* @param str 字符串.
* @return 如果字符串为null或空串,则返回true,否则返回false.
*/
private static boolean isEmpty(String str) {
return null==str || str.length()<=0;
} /** 运行.
*
* @param export 文本打印流.
* @param args 参数.
* @return 程序退出码.
*/
public void run(PrintStream export, String[] args) {
boolean showhelp = true;
// args
String state = null; // 状态.
boolean isEncode = false;
boolean isDecode = false;
String fileKey = null;
String fileOut = null;
String fileSrc = null;
int keysize = 0; // RSA密钥位数. 0表示自动获取.
for(String s: args) {
if ("-e".equalsIgnoreCase(s)) {
isEncode = true;
} else if ("-d".equalsIgnoreCase(s)) {
isDecode = true;
} else if ("-l".equalsIgnoreCase(s)) {
state = "l";
} else if ("-o".equalsIgnoreCase(s)) {
state = "o";
} else {
if ("l".equalsIgnoreCase(state)) {
fileKey = s;
state = null;
} else if ("o".equalsIgnoreCase(state)) {
fileOut = s;
state = null;
} else {
fileSrc = s;
}
}
}
try{
if (isEmpty(fileKey)) {
export.println("No key file! Command need add `-l [keyfile]`.");
} else if (isEmpty(fileOut)) {
export.println("No out file! Command need add `-o [outfile]`.");
} else if (isEmpty(fileSrc)) {
export.println("No src file! Command need add `[srcfile]`.");
} else if (isEncode!=false && isDecode!=false) {
export.println("No set Encode/Encode! Command need add `-e`/`-d`.");
} else if (isEncode) {
showhelp = false;
doEncode(export, keysize, fileKey, fileOut, fileSrc, null);
} else if (isDecode) {
showhelp = false;
doDecode(export, keysize, fileKey, fileOut, fileSrc, null);
}
} catch (Exception e) {
e.printStackTrace(export);
}
// do.
if (showhelp) {
export.println(helpText);
}
} /** 进行加密.
*
* @param export 文本打印流.
* @param keysize 密钥位数. 为0表示自动获取.
* @param fileKey 密钥文件.
* @param fileOut 输出文件.
* @param fileSrc 源文件.
* @param exargs 扩展参数.
* @throws IOException
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
private void doEncode(PrintStream export, int keysize, String fileKey, String fileOut,
String fileSrc, Map<String, ?> exargs) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] bytesSrc = ZlRsaUtil.fileLoadBytes(fileSrc);
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(ZlRsaUtil.RSA);
Key key= null;
//boolean isPrivate = false;
if ("R".equals(purposecode)) {
//isPrivate = true;
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();
}
export.println(String.format("keysize: %d", keysize));
export.println(String.format("key.getAlgorithm: %s", key.getAlgorithm()));
export.println(String.format("key.getFormat: %s", key.getFormat()));
// encrypt.
Cipher cipher = Cipher.getInstance(ZlRsaUtil.RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] cipherBytes = ZlRsaUtil.encrypt(cipher, keysize, bytesSrc);
byte[] cipherBase64 = Base64.encode(cipherBytes);
ZlRsaUtil.fileSaveBytes(fileOut, cipherBase64, 0, cipherBase64.length);
export.println(String.format("%s save done.", fileOut));
} /** 进行解密.
*
* @param export 文本打印流.
* @param keysize 密钥位数. 为0表示自动获取.
* @param fileKey 密钥文件.
* @param fileOut 输出文件.
* @param fileSrc 源文件.
* @param exargs 扩展参数.
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
private void doDecode(PrintStream export, int keysize, String fileKey, String fileOut,
String fileSrc, Object exargs) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] bytesB64Src = ZlRsaUtil.fileLoadBytes(fileSrc);
byte[] bytesSrc = Base64.decode(bytesB64Src);
if (null==bytesSrc || bytesSrc.length<=0) {
export.println(String.format("Error: %s is not BASE64!", fileSrc));
return;
}
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(ZlRsaUtil.RSA);
Key key= null;
//boolean isPrivate = false;
if ("R".equals(purposecode)) {
//isPrivate = true;
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();
}
export.println(String.format("key.getAlgorithm: %s", key.getAlgorithm()));
export.println(String.format("key.getFormat: %s", key.getFormat()));
// decrypt.
Cipher cipher = Cipher.getInstance(ZlRsaUtil.RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] cipherBytes = ZlRsaUtil.decrypt(cipher, keysize, bytesSrc);
ZlRsaUtil.fileSaveBytes(fileOut, cipherBytes, 0, cipherBytes.length);
export.println(String.format("%s save done.", fileOut));
} public static void main(String[] args) {
RsaPemDemo demo = new RsaPemDemo();
demo.run(System.out, args);
}
}

附录.2 .Net版

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Collections;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography; namespace RsaPemDemo {
/// <summary>
/// Java/.NET RSA demo, use pem key file (Java/.NET的RSA加解密演示项目,使用pem格式的密钥文件).
/// </summary>
class Program {
/// <summary>
/// 帮助文本.
/// </summary>
private const string helpText = "Usage: RsaPemDemo [options] srcfile\n\nFor example:\n\n # encode by public key\n rsapemdemo -e -l publickey.pem -o dstfile srcfile\n\n # decode by private key\n rsapemdemo -d -l privatekey.pem -o dstfile srcfile\n\nThe options:\n\n -e RSA encryption and BASE64 encode.\n -d BASE64 decode and RSA decryption.\n -l [keyfile] Load key file.\n -o [outfile] out file.\n"; /// <summary>
/// 运行.
/// </summary>
/// <param name="export">文本打印流.</param>
/// <param name="args">参数.</param>
public void run(TextWriter export, string[] args) {
bool showhelp = true;
// args
string state = null; // 状态.
bool isEncode = false;
bool isDecode = false;
string fileKey = null;
string fileOut = null;
string fileSrc = null;
int keysize = 0; // RSA密钥位数. 0表示自动获取.
foreach(string s in args) {
if ("-e".Equals(s, StringComparison.OrdinalIgnoreCase)) {
isEncode = true;
} else if ("-d".Equals(s, StringComparison.OrdinalIgnoreCase)) {
isDecode = true;
} else if ("-l".Equals(s, StringComparison.OrdinalIgnoreCase)) {
state = "l";
} else if ("-o".Equals(s, StringComparison.OrdinalIgnoreCase)) {
state = "o";
} else {
if ("l".Equals(state, StringComparison.OrdinalIgnoreCase)) {
fileKey = s;
state = null;
} else if ("o".Equals(state, StringComparison.OrdinalIgnoreCase)) {
fileOut = s;
state = null;
} else {
fileSrc = s;
}
}
}
try{
if (string.IsNullOrEmpty(fileKey)) {
export.WriteLine("No key file! Command need add `-l [keyfile]`.");
} else if (string.IsNullOrEmpty(fileOut)) {
export.WriteLine("No out file! Command need add `-o [outfile]`.");
} else if (string.IsNullOrEmpty(fileSrc)) {
export.WriteLine("No src file! Command need add `[srcfile]`.");
} else if (isEncode!=false && isDecode!=false) {
export.WriteLine("No set Encode/Encode! Command need add `-e`/`-d`.");
} else if (isEncode) {
showhelp = false;
doEncode(export, keysize, fileKey, fileOut, fileSrc, null);
} else if (isDecode) {
showhelp = false;
doDecode(export, keysize, fileKey, fileOut, fileSrc, null);
}
} catch (Exception ex) {
export.WriteLine(ex.ToString());
}
// do.
if (showhelp) {
export.WriteLine(helpText);
}
} /// <summary>
/// 进行加密.
/// </summary>
/// <param name="export">文本打印流.</param>
/// <param name="keysize">密钥位数. 为0表示自动获取.</param>
/// <param name="fileKey">密钥文件.</param>
/// <param name="fileOut">输出文件.</param>
/// <param name="fileSrc">源文件.</param>
/// <param name="exargs">扩展参数.</param>
private void doEncode(TextWriter export, int keysize, string fileKey, string fileOut,
string fileSrc, IDictionary exargs) {
byte[] bytesSrc = File.ReadAllBytes(fileSrc);
string strDataKey = File.ReadAllText(fileKey);
string purposetext = null;
char purposecode = '\0';
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, ref purposetext, ref purposecode);
//export.WriteLine(bytesKey);
// key.
RSACryptoServiceProvider rsa;
if ('R' == purposecode) {
rsa = ZlRsaUtil.PemDecodePkcs8PrivateKey(bytesKey); // try
if (null == rsa) {
rsa = ZlRsaUtil.PemDecodeX509PrivateKey(bytesKey);
}
} else { // 公钥或无法判断时, 均当成公钥处理.
rsa = ZlRsaUtil.PemDecodePublicKey(bytesKey);
}
if (null == rsa) {
export.WriteLine("Key decode fail!");
return;
}
export.WriteLine(string.Format("KeyExchangeAlgorithm: {0}", rsa.KeyExchangeAlgorithm));
export.WriteLine(string.Format("KeySize: {0}", rsa.KeySize));
// encrypt.
byte[] cipherBytes = ZlRsaUtil.Encrypt(rsa, bytesSrc);
string cipherBase64 = Convert.ToBase64String(cipherBytes);
File.WriteAllText(fileOut, cipherBase64);
export.WriteLine(string.Format("{0} save done.", fileOut));
} /// <summary>
/// 进行解密.
/// </summary>
/// <param name="export">文本打印流.</param>
/// <param name="keysize">密钥位数. 为0表示自动获取.</param>
/// <param name="fileKey">密钥文件.</param>
/// <param name="fileOut">输出文件.</param>
/// <param name="fileSrc">源文件.</param>
/// <param name="exargs">扩展参数.</param>
private void doDecode(TextWriter export, int keysize, string fileKey, string fileOut,
string fileSrc, IDictionary exargs) {
String bytesSrcB64Src = File.ReadAllText(fileSrc);
byte[] bytesSrc = Convert.FromBase64String(bytesSrcB64Src);
string strDataKey = File.ReadAllText(fileKey);
string purposetext = null;
char purposecode = '\0';
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, ref purposetext, ref purposecode);
//export.WriteLine(bytesKey);
// key.
RSACryptoServiceProvider rsa;
if ('R' == purposecode) {
rsa = ZlRsaUtil.PemDecodePkcs8PrivateKey(bytesKey); // try
if (null == rsa) {
rsa = ZlRsaUtil.PemDecodeX509PrivateKey(bytesKey);
}
} else { // 公钥或无法判断时, 均当成公钥处理.
rsa = ZlRsaUtil.PemDecodePublicKey(bytesKey);
}
if (null == rsa) {
export.WriteLine("Key decode fail!");
return;
}
export.WriteLine(string.Format("KeyExchangeAlgorithm: {0}", rsa.KeyExchangeAlgorithm));
export.WriteLine(string.Format("KeySize: {0}", rsa.KeySize));
// encryption.
byte[] cipherBytes = ZlRsaUtil.Decrypt(rsa, bytesSrc);
File.WriteAllBytes(fileOut, cipherBytes);
export.WriteLine(string.Format("{0} save done.", fileOut));
} static void Main(string[] args) {
Program demo = new Program();
demo.run(Console.Out, args);
}
}
}

源码地址:

https://github.com/zyl910/rsapemdemo

参考文献

全面解决.Net与Java互通时的RSA加解密问题,使用PEM格式的密钥文件的更多相关文章

  1. RSA加解密工具类RSAUtils.java,实现公钥加密私钥解密和私钥解密公钥解密

    package com.geostar.gfstack.cas.util; import org.apache.commons.codec.binary.Base64; import javax.cr ...

  2. 【转】 Java 进行 RSA 加解密时不得不考虑到的那些事儿

    [转] Java 进行 RSA 加解密时不得不考虑到的那些事儿 1. 加密的系统不要具备解密的功能,否则 RSA 可能不太合适 公钥加密,私钥解密.加密的系统和解密的系统分开部署,加密的系统不应该同时 ...

  3. 与非java语言使用RSA加解密遇到的问题:algid parse error, not a sequence

    遇到的问题 在一个与Ruby语言对接的项目中,决定使用RSA算法来作为数据传输的加密与签名算法.但是,在使用Ruby生成后给我的私钥时,却发生了异常:IOException: algid parse ...

  4. 前后端java+vue 实现rsa 加解密与摘要签名算法

    RSA 加密.解密.签名.验签.摘要,前后端java+vue联调测试通过 直接上代码 // 注意:加密密文与签名都是唯一的,不会变化.// 注意:vue 端密钥都要带pem格式.java 不要带pem ...

  5. Java中的RSA加解密工具类:RSAUtils

    本人手写已测试,大家可以参考使用 package com.mirana.frame.utils.encrypt; import com.mirana.frame.utils.log.LogUtils; ...

  6. Rsa加解密Java、C#、php通用代码 密钥转换工具

    之前发了一篇"TripleDes的加解密Java.C#.php通用代码",后面又有项目用到了Rsa加解密,还是在不同系统之间进行交互,Rsa在不同语言的密钥格式不一样,所以过程中主 ...

  7. java RSA加解密以及用途

    在公司当前版本的中间件通信框架中,为了防止非授权第三方和到期客户端的连接,我们通过AES和RSA两种方式的加解密策略进行认证.对于非对称RSA加解密,因为其性能耗费较大,一般仅用于认证连接,不会用于每 ...

  8. RSA加解密用途简介及java示例

    在公司当前版本的中间件通信框架中,为了防止非授权第三方和到期客户端的连接,我们通过AES和RSA两种方式的加解密策略进行认证.对于非对称RSA加解密,因为其性能耗费较大,一般仅用于认证连接,不会用于每 ...

  9. Java 进行 RSA 加解密时不得不考虑到的那些事儿

    1. 加密的系统不要具备解密的功能,否则 RSA 可能不太合适 公钥加密,私钥解密.加密的系统和解密的系统分开部署,加密的系统不应该同时具备解密的功能,这样即使黑客攻破了加密系统,他拿到的也只是一堆无 ...

随机推荐

  1. 016 pickle

    英文也是泡菜的意思. 学完了,还是感觉这个模块是蛮不错的,对多数据保存到文件中,然后在使用的时候,再读取出来,让程序闲的更加优雅,简洁. 一:介绍 1.为什么使用 在开篇已经介绍了,但是我这里粘贴一下 ...

  2. Jenkins的介绍

    在公司看见有的部门在使用这个工具,好奇一下,今天就开始学习一番. 1.官网 https://jenkins.io/ 2.介绍 Jenkins是一个功能强大的应用程序,允许持续集成和持续交付项目,无论用 ...

  3. Scala-Unit7-Scala并发编程模型AKKA

    一.Akka简介 Akka时spark的底层通信框架,Hadoop的底层通信框架时rpc. 并发的程序编写很难,但是Akka解决了spark的这个问题. Akka构建在JVM平台上,是一种高并发.分布 ...

  4. Java中的不同遍历方式

    已知一个Person类: public class Person implements Comparable<Person>{ String name; String id; public ...

  5. Django项目中使用Redis

    Django项目中使用Redis DjangoRedis 1 redis Redis 是一个 key-value 存储系统,常用于缓存的存储.django-redis 基于 BSD 许可, 是一个使 ...

  6. XamarinAndroid组件教程RecylerView适配器设置动画

    XamarinAndroid组件教程RecylerView适配器设置动画 本小节将讲解动画相关设置,如动画的时长.插值器以及复合动画等. 1.设置动画时长 设置动画持续的时间可以使用Animation ...

  7. jsonp 跨域 jsonp 发音

    JSONP(JSON with Padding)是JSON的一种“使用模式” 可用于解决主流浏览器的跨域数据访问的问题. 由于同源策略, 一般来说位于 server1.example.com 的网页 ...

  8. WordPress UpdraftPlus插件 Google Drive 备份

    本文连接地址: http://blog.tuzhuke.info/?p=168 本文作者:tuzhuke 完成时间:2015-04-10 使用wordpress 搭建自己的博客网站,但是对于租用的服务 ...

  9. 潭州课堂25班:Ph201805201 django 项目 第四十五课 mysql集群和负载均衡(课堂笔记)

    2.使用docker安装Haproxy 一.为什么要使用数据库集群和负载均衡? 1.高可用 2.高并发 3.高性能 二.mysql数据库集群方式 三.使用docker安装PXC 1.拉取PXC镜像 d ...

  10. BZOJ4039 : 集会

    将曼哈顿距离转化为切比雪夫距离,即: $|x_1-x_2|+|y_1-y_2|=\max(|(x_1+y_1)-(x_2+y_2)|,|(x_1-y_1)-(x_2-y_2)|)$ 那么每个点能接受的 ...