作者: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个条件——
- 文本格式。这样用记事本打开密钥文件,能够方便的复制粘贴,且能作为程序中的字符串常量。使用灵活,方便测试等。
- 易于生成。不必编写、运行代码来生成,而是能够通过多种办法来生成密钥对。既可以命令行生成,又可以通过图形界面工具点击生成。
所以最终选择了 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=='\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没有简单的提供该属性,而是需要一些步骤来得到,且公钥、私钥得使用不同的类。
- 调用 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 == '\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
参考文献
- 《RSA (cryptosystem)》: https://en.wikipedia.org/wiki/RSA_(cryptosystem)
- Michel I. Gallant Ph.D.《RSA Public, Private, and PKCS #8 key parser》(OpenSSLKey.cs). http://www.jensign.com/opensslkey/
- 《PKCS#1:RSA加密》. http://man.chinaunix.net/develop/rfc/RFC2313.txt
- 《在线生成生成RSA密钥对》. http://web.chacuo.net/netrsakeypair
- 蒋国纲《那些证书相关的玩意儿(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)》. http://www.cnblogs.com/guogangj/p/4118605.html
- 阮一峰《RSA算法原理(二)》. http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html
- 任家《OPENSSL中RSA私钥文件(PEM格式)解析【一】》. http://blog.sina.com.cn/s/blog_4fcd1ea30100yh4s.html
- 写代码的二妹《PHP,C# 和JAVARSA签名及验签》. http://www.cnblogs.com/frankyou/p/5993756.html
- FrankYou《C# RSA 分段加解密》. http://www.cnblogs.com/frankyou/p/5993756.html
- FrankYou《Java RSA 分段加解密》. http://www.cnblogs.com/frankyou/p/5993685.html
- sahusoft《分组对称加密模式:ECB/CBC/CFB/OFB缺CTR》. http://blog.csdn.net/sahusoft/article/details/6867848
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。