天天看点

Java AES加解密报错javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher的问题原因及2种解决方式

一、问题背景及原因分析

  需求对保密性要求严格点,就用的 AES + 盐值 + 偏移向量 去做,前端加密传递参数,Java 解密参数,然后查询数据,得到数据后再将数据加密返给前端,前端最对数据进行解密,得到具体数据使用。

  在此过程中发现偶尔使用 Java AES 解密前端传递的参数时会报这个异常,如下:

javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
    at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:922)
    at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:833)
    at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
    at javax.crypto.Cipher.doFinal(Cipher.java:2165)
    at com.symmetric.aes.TestAES.testDecrpyt(TestAES.java:200)
    at com.symmetric.aes.TestAES.main(TestAES.java:48)       

  字面理解很容易,就是解密的字符串的数组必须是 16 的倍数。

  这里有一篇文章介绍:https://blog.csdn.net/kzcming/article/details/80019478,可以看一下。

1、分析出现此异常的情况:

  如果不把加密后的数组拼接为字符串,直接返回,然后使用这个加密后的数组进行解密就没有任何错误;

  但是把加密后的数组拼接为字符串,然后解密时在把此字符串转为数组,就会出现此异常

2、具体分析:

  发现当把字节数组转为字符串后,在把 字符串.getBytes() 获得字节数组,发现两个字节数组前后不一样了  ——  这是报错的位置所在。(声明:new String(byte[]) 和 "str".getBytes() 两个方法使用的编码一样,然后换成其他编码也出现这样情况,也就是说不是编码的问题)

3、原因

(1)为什么数组转字符串,字符串然后转数组会出现,前后两个字节数组的值会不同?

  因为并不是每个字节数和编码集上的字符都有对应关系,如果一个字节数在编码集上没有对应,编码 new String(byte[])  后往往解出来的会是一些乱码无意义的符号,例如:��。

  但是解码的时候 � 这个字符也是一个字符在编码表中也有固定的字节数用来表示,所有解码出来的值必定是编码表中对应的值,除非你的字节数组中的字节数正好在编码表中有对应的值,否则编码、解码后的字节数组会不一样。

  误区:误以为所有的字节数组都可以new String(),然后在通过String.getBytes()还原。

(2)再说这个异常报错:解密的字节数组必须是16的倍数,这得从AES的原理说起,AES是把数据按16字节分组加密的,所有如果数组长度不是16的倍数会报错。

  AES原理:AES是对数据按128位,也就是16个字节进行分组进行加密的,每次对一组数据加密需要运行多轮,而输入密钥的长度可以为128、192和256位,也就是16个字节、24个字节和32个字节,如果用户输入的密钥长度不是这几种长度,也会补成这几种长度。

  无论输入密钥是多少字节,加密还是以16字节的数据一组来进行的,密钥长度的不同仅仅影响加密运行的轮数。

4、解决的办法:

(1)可以用 base64 对产生的数组进行编码,然后在解码,这样不会像 new String(byte[])、getBytes() 那样造成数组前后不一致,一开始我看到大部分人都是用 base64,我也只是以为多一层编码看起来安全一些而已,没想到 base64 对数组的处理是不会造成误差的

(2)就是直接返回数组,然后再用数组解密咯

二、解决方案

  而我本身就是采用了base64 编码的,结果还是偶尔出现这个报错,后来发现了规律,就是只有前端加密的字符串包含特殊字符,如 + ,传递给后台去解密就一定会报这个错。而我本身就进行了 encodeURIComponent() 进行传参。

  后来了解到原来原因在这里:由于前台通过 url 传过来的加密后的数据到后台接受丢失特殊字符(url 对字符串进行编码,但是发现+全部都变成了空格),然后断点调试一下,确实 + 变成了 空格

1、解决方式一:Get 参数需要对 URL特殊字符进行转义

// 对前台的代码进行编码
bankCardNumber = bankCardNumber.replace(/\+/g,"%2B");

// 后台再转码回去 - 就是替换
encrypted = encrypted.replaceAll("%2B", "\\+");      

(1)知识

1、URL特殊字符需转义 

2、空格换成加号(+)   

3、正斜杠(/)分隔目录和子目录   

4、问号(?)分隔URL和查询   

5、百分号(%)制定特殊字符   

6、#号指定书签   

7、&号分隔参数 

(2)转义字符的原因:

  如果你的表单使用get方法提交,并且提交的参数中有“&”等特殊符的话,如果不做处理,在service端就会将&后面的作为另外一个参数来看待。例如表单的action为list.jsf?act=Go&state=5,则提交时通过request.getParameter可以分别取得act和state的值。 如果你的本意是act='go&state=5'这个字符串,那么为了在服务端拿到act的准确值,你就必须对&进行转义。

(3)url 转义字符原理:

  将这些特殊的字符转换成ASCII码,格式为:%加字符的ASCII码,即一个百分号%,后面跟对应字符的ASCII(16进制)码值。例如 空格的编码值是"%20"。

(4)URL特殊符号及对应的十六进制值编码:

1、+  URL 中+号表示空格 %2B 

2、空格 URL中的空格可以用+号或者编码 %20  

3、/ 分隔目录和子目录 %2F   

4、? 分隔实际的 URL 和参数 %3F   

5、% 指定特殊字符 %25   

6、# 表示书签 %23   

7、& URL 中指定的参数间的分隔符 %26   

8、= URL 中指定参数的值 %3D

2、解决方式二:使用 post 在 body 里传参即可(这样就不会存在需 URL 转义的问题)

@PostMapping("/downUrl")
public OperationInfo getDownUrl (@RequestBody String jsonStr) throws Exception{
  JSONObject jsonObject = JSONObject.parseObject(jsonStr);
  String idStr = jsonObject.getString("idStr");
  ......
}      

  这里就涉及到 Java 对象与 JSON 对象之间的相互转换。