升级到JDK17,AES解密失败:Given final block not properly padded. Such issues can arise if a bad key is used d
·
描述
项目从JDK8升级到了JDK17,在调试代码的过程中发现有AES解密失败的错误日志,如下:
javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.
at java.base/com.sun.crypto.provider.CipherCore.unpad(CipherCore.java:859)
at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:939)
at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:735)
at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:436)`
at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2205)
at com.papercollection.winterview.wis.AESUtil.operator(AESUtil.java:54)
at com.papercollection.winterview.wis.AESUtil.decrypt(AESUtil.java:77)
at com.papercollection.winterview.wis.AESUtil.main(AESUtil.java:100)
AES加解密代码
import org.springframework.util.Base64Utils;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public class AESUtil {
/**
* AES加密字符串
*
* @param content 需要被加密的字符串
* @param password 加密需要的密码
* @return 密文
*/
public static byte[] encrypt(String content, String password) {
try {
int mode = Cipher.ENCRYPT_MODE;
byte[] byteContent = content.getBytes("utf-8");
return operator(password, mode, byteContent);
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
private static byte[] operator(String password, int mode, byte[] byteContent) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
SecretKey secretKey = getSecretKey(password);
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
cipher.init(mode, secretKey);// 初始化为加密模式的密码器
byte[] result = cipher.doFinal(byteContent);// 加密
return result;
}
private static SecretKey getSecretKey(String password) throws NoSuchAlgorithmException {
KeyGenerator kgen = KeyGenerator.getInstance("AES");// 创建AES的Key生产者
kgen.init(128, new SecureRandom(password.getBytes()));// 利用用户密码作为随机数初始化出
//加密没关系,SecureRandom是生成安全随机数序列,password.getBytes()是种子,只要种子相同,序列就一样,所以解密只要有password就行
SecretKey secretKey = kgen.generateKey();// 根据用户密码,生成一个密钥
printSecretKey(secretKey);
return secretKey;
}
/**
* 解密AES加密过的字符串
*
* @param content AES加密过过的内容
* @param password 加密时的密码
* @return 明文
*/
public static byte[] decrypt(byte[] content, String password) {
try {
int mode = Cipher.DECRYPT_MODE;
return operator(password, mode, content);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws Exception {
String content = "1993";
String password = "加密密码";
System.out.println("需要加密的内容:" + content);
byte[] encrypt = encrypt(content, password);
String base64Encode = Base64Utils.encodeToString(encrypt);
byte[] byte2 = Base64Utils.decodeFromString(base64Encode);
byte[] decrypt = decrypt(byte2, password);
System.out.println("解密后的内容:" + new String(decrypt, "utf-8"));
}
public static void printSecretKey(SecretKey secretKey) {
if (secretKey instanceof SecretKeySpec) {
SecretKeySpec spec = (SecretKeySpec) secretKey;
System.out.println("Algorithm: " + spec.getAlgorithm());
System.out.println("key[]: " + Arrays.toString(spec.getEncoded()));
}
}
}
分析
项目中的代码也是参考的网上样例,代码中有个很奇怪的地方是,每次都要重新生成一次secretKey,尝试打印该变量
JDK8环境运行
JDK17环境运行
可以看到JDK17两次生成的secretKey值不同
手动调试
打断点调试在解密时手动修改SecretKey的值为加密时的值,发现时可以解密成功的
运行结果
小结
那么问题的直接原因应该是JDK8时每次生成的secretKey都相同,而JDK17都不同,导致加密后解密失败
新问题
那么就引申出两个新问题
- AES是对称加密,为什么每次都要使用KeyGenerator生成SecretKey,双方约定使用同样的key不可以吗
- 为什么在JDK8,每次生成的SecretKey相同,而到了JDK17会变的不同了
1. 为什么每次都要使用KeyGenerator生成SecretKey
我搜索了网上的一些样例和代码,每次生都成新的secretKey是不正确的做法,正常的是双方约定好一个16位或者其他符合要求位数的密钥,进行加解密操作
2. 为什么到了JDK17,每次生成的SecretKey会不同
private static SecretKey getSecretKey(String password) throws NoSuchAlgorithmException {
KeyGenerator kgen = KeyGenerator.getInstance("AES");// 创建AES的Key生产者
kgen.init(128, new SecureRandom(password.getBytes()));// 利用用户密码作为随机数初始化出
//加密没关系,SecureRandom是生成安全随机数序列,password.getBytes()是种子,只要种子相同,序列就一样,所以解密只要有password就行
SecretKey secretKey = kgen.generateKey();// 根据用户密码,生成一个密钥
printSecretKey(secretKey);
return secretKey;
}
生成secretKey使用的KeyGenerator是用SecureRandom初始化的,在JDK8中中,使用的随机算法是SHA1PRNG,而到了JDK17中使用的算法是DRBG.
这里需要提一下,JDK17与JDK8相比,SecureRandom的构造方法发生了变化,随机算法的选取规则也不一样了
-
JDK8
-
JDK17
SHA1PRNG算法
验证代码,指定使用SHA1PRNG作为随机算法
private static SecretKey getSecretKey(String password) throws NoSuchAlgorithmException {
KeyGenerator kgen = KeyGenerator.getInstance("AES");// 创建AES的Key生产者
//kgen.init(128, new SecureRandom(password.getBytes()));// 利用用户密码作为随机数初始化出
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(password.getBytes());
kgen.init(128, secureRandom);
SecretKey secretKey = kgen.generateKey();// 根据用户密码,生成一个密钥
printSecretKey(secretKey);
return secretKey;
}
- windows
环境信息:windows11
JDK版本17
- linux
环境信息:centos
JDK版本1.8
结论
SHA1PRNG是跨平台和版本的,生成的SecretKey只和指定的因子有关
更多推荐
已为社区贡献1条内容
所有评论(0)