描述

项目从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只和指定的因子有关

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐