ZipInputStream和RSA算法的纠葛
Posted 毕小宝
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ZipInputStream和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次数。万一遇到本文这样的问题,就一并解决了。
“人是有惰性的动物,如果过多地沉湎于温柔乡里,就会削弱重新投入风暴的勇气和力量。”摘自路遥先生的《早晨从中午开始》,欠下的债总是要还的,也是对自己的警醒。
以上是关于ZipInputStream和RSA算法的纠葛的主要内容,如果未能解决你的问题,请参考以下文章