RSA算法实践整理

背景

以前有一篇文章介绍过系统升级操作的实现流程:通过上传zip压缩包、并通过RMI方式调用另一个Java程序执行upgrade.sh脚本完成的。其中有一个系统版本信息校验的逻辑,版本信息是一段xml信息经过RSA算法加密,直接打包到zip文件中。系统升级操作,首先对zip文件中的版本描述信息进行解密。

存在一个诡异的问题,只有我本机的360压缩工具生成的zip文件,直接读取密文并解密才不会出错,而winRAR或者7Zip工具生成的压缩文件都报解密异常。这个问题困扰了鄙人一年,这是个大坑啊,如果不解决,万一我的电脑挂了,这个功能就无法正常运转了。昨天没事儿,想着把这个坑抹平,就下决心要找找原因。最终被同事找到根源了,记录一下IO中的坑。

直接读取ZIP文件

升级操作直接读取zip文件流中的加密密文,Java直接读取Zip文件的流程如下:

public static void readFromZip(String zipFileName) throws IOException{
		ZipFile zf = null;
		InputStream in = null;
		ZipInputStream zin = null;
		
		try{
			zf = new ZipFile(zipFileName);
			in = new BufferedInputStream(new FileInputStream(zipFileName));
			zin = new ZipInputStream(in);
			
			ZipEntry ze = null;
			while ((ze = zin.getNextEntry()) != null) {
			  String zipName = ze.getName();
			  if(zipName.contains("descriptor")){//找到密文文件并读取
					InputStream inputStream = zf.getInputStream(ze);
					byte[] data = new byte[inputStream.available()];
					int len = 0;
					while ((len = inputStream.read(data)) > 0) {
					  System.out.println("length:"+len); 		
					}
					System.out.println("data is :"+Arrays.toString(data));				}
			}
		} finally {
			try {
				zin.closeEntry();
				in.close();
				zf.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}
	}
    

直接使用ZipInputStream类,逐个遍历压缩包中的每个文件,找到加密文件后读取该文件的内容到字节数组中。这里处理时有一个问题,Java对不同压缩工具生成的压缩文件处理方式有差异。

不同压缩工具的对应Java实现的差异

1 )WinRAR压缩文件,在使用Java IO工具读取时,zf.getInputStream()流的实例对象为:

这里写图片描述

这个类的read(data)操作,分了两次才读完数据。执行打印结果为:

length:765
length:3

2)360压缩文件,在使用Java IO工具读取时,zf.getInputStream()流的实例对象为:

这里写图片描述

这个类的read(data)操作,一次读完数据。执行打印结果为:

length:768

3)使用BufferedInputStream,定义缓存取后,两种流都能一次读完加密数据。

	public static byte[] bufferedReadFromInputStream(InputStream inputstream) throws IOException{
		BufferedInputStream bufferedInputStream = new BufferedInputStream(inputstream);
		byte[] data = new byte[inputstream.available()];
		int len = 0;
		while ((len = bufferedInputStream.read(data)) > 0) {
			
		}
		
		return data;
	}

这是因为原始的InputStream的read()操作,是每完成一次io读取,就往内部缓冲区执行写入一次数据;而缓冲区定义后,只有缓冲区写满或者读不到数据时才写入内存,这就能保证每次写入内存时的数据长度是有保障的。

RSA解密流程

压缩文件中的加密文件是通过RSA算法生成的,解密代码如下:

private byte[] decryptDescriptor(InputStream inputstream) {
	ByteArrayOutputStream bos = new ByteArrayOutputStream();
	X509EncodedKeySpec keySpec;
	try {
		keySpec = new X509EncodedKeySpec(
				new BASE64Decoder().decodeBuffer(Encription.KEY_PUBLIC));
		PublicKey key = KeyFactory.getInstance("RSA").generatePublic(keySpec);
		
		Cipher cipher = Cipher.getInstance("RSA");
		cipher.init(Cipher.DECRYPT_MODE, key);
		byte[] data = new byte[cipher.getOutputSize(inputstream.available())];
		int len = 0;
		//直接循环读取密文输入流,doFinal解密写入字节输出流中
		while ((len = bufferedInputStream.read(data)) > 0) {
			bos.write(cipher.doFinal(data, 0, len));
		}
		
		return bos.toByteArray();
	} catch (IOException | InvalidKeySpecException | NoSuchAlgorithmException | NoSuchPaddingException
			| InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
		logger.error("解密升级描述文件异常.",e);
	}finally{
		try {
			inputstream.close();
		} catch (IOException e) {
			logger.error("关闭升级描述文件流异常.",e);
		}
	}
	
	return null;
}

上述解密代码,直接使用InputStream的read(data)方法,现将文件读入字节数组中,然后调用doFinal进行解密。那么对于WinRAR压缩包莫名多出一次的读取操作而言,就有问题。

RSA解密算法会根据待解密数据的长度,得到一个固定长度,然后每次doFinal时只解密指定长度的数据。

byte[] data = new byte[cipher.getOutputSize(inputstream.available())];

这行代码定义的data长度,是128,根据真正的文件长度768得到的。那么InputStream在执行read操作过程中会读768/128=6次,每次读取128字节的数据。360压缩文件是符合这逻辑的。但是WinRAR压缩文件因为莫名多读了一次,导致第6次读取的长度是125,最后3字节还需再读一次。而doFinal在第6次执行时期待128字节,结果只有正常的125字节的数据,所以就直接报异常了。

这里写图片描述

解决办法:通过缓冲区输入流,让不同的压缩文件都read相同的次数,保证doFinal操作能得到正确的数据。修改上述代码如下:

private byte[] decryptDescriptor(InputStream inputstream) {
     //使用缓冲字节流
     BufferedInputStream bufferedInputStream = new BufferedInputStream(inputstream);
     ……
		try {
		 ……

				byte[] data = new byte[cipher.getOutputSize(bufferedInputStream .available())];
		int len = 0;
		//缓冲读取密文,doFinal解密写入字节输出流中
		while ((len = bufferedInputStream .read(data)) > 0) {
			bos.write(cipher.doFinal(data, 0, len));
		}
		
		return bos.toByteArray();
	} catch (){ ……
	}
	
	return null;
}

启示录

处理文件的时候,尽量使用BufferedInputstream,速度高效,减少不必要的IO次数。万一遇到本文这样的问题,就一并解决了。

"人是有惰性的动物,如果过多地沉湎于温柔乡里,就会削弱重新投入风暴的勇气和力量。”摘自路遥先生的《早晨从中午开始》,欠下的债总是要还的,也是对自己的警醒。

已标记关键词 清除标记
表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
相关推荐
程序员的必经之路! 【限时优惠】 现在下单,还享四重好礼: 1、教学课件免费下载 2、课程案例代码免费下载 3、专属VIP学员群免费答疑 4、下单还送800元编程大礼包 【超实用课程内容】  根据《2019-2020年中国开发者调查报告》显示,超83%的开发者都在使用MySQL数据库。使用量大同时,掌握MySQL早已是运维、DBA的必备技能,甚至部分IT开发岗位也要求对数据库使用和原理有深入的了解和掌握。 学习编程,你可能会犹豫选择 C++ 还是 Java;入门数据科学,你可能会纠结于选择 Python 还是 R;但无论如何, MySQL 都是 IT 从业人员不可或缺的技能!   套餐中一共包含2门MySQL数据库必学的核心课程(共98课时)   课程1:《MySQL数据库从入门到实战应用》   课程2:《高性能MySQL实战课》   【哪些人适合学习这门课程?】  1)平时只接触了语言基础,并未学习任何数据库知识的人;  2)对MySQL掌握程度薄弱的人,课程可以让你更好发挥MySQL最佳性能; 3)想修炼更好的MySQL内功,工作中遇到高并发场景可以游刃有余; 4)被面试官打破沙锅问到底的问题问到怀疑人生的应聘者。 【课程主要讲哪些内容?】 课程一:《MySQL数据库从入门到实战应用》 主要从基础篇,SQL语言篇、MySQL进阶篇三个角度展开讲解,帮助大家更加高效的管理MySQL数据库。 课程二:《高性能MySQL实战课》主要从高可用篇、MySQL8.0新特性篇,性能优化篇,面试篇四个角度展开讲解,帮助大家发挥MySQL的最佳性能的优化方法,掌握如何处理海量业务数据和高并发请求 【你能收获到什么?】  1.基础再提高,针对MySQL核心知识点学透,用对; 2.能力再提高,日常工作中的代码换新貌,不怕问题; 3.面试再加分,巴不得面试官打破沙锅问到底,竞争力MAX。 【课程如何观看?】  1、登录CSDN学院 APP 在我的课程中进行学习; 2、移动端:CSDN 学院APP(注意不是CSDN APP哦)  本课程为录播课,课程永久有效观看时长 【资料开放】 课件、课程案例代码完全开放给你,你可以根据所学知识,自行修改、优化。  下载方式:电脑登录课程观看页面,点击右侧课件,可进行课程资料的打包下载。
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值