Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 625|回复: 0

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

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-4-22 11:20:18 | 显示全部楼层 |阅读模式

    作者: 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 != b)
    					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

    参考文献

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-18 19:48 , Processed in 0.075525 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表