如何基于密码和盐生成javax.crypto.SecretKey解决DESede解密报错
问题描述
- 开发需求:基于密码(password)与盐值(salt)生成
javax.crypto.SecretKey实例,用于加解密操作 - 约束条件:现有系统使用
DESede/ECB/PKCS5Padding算法完成加解密,数据库已存储大量存量加密数据,不允许修改现有加解密逻辑 - 故障现象:自行实现PBKDF2派生密钥逻辑后,对接现有解密流程抛出如下异常:
key.getAlgorithm(): DESede encryptedData: [B@31dc339b Exception in thread "main" javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. at com.sun.crypto.provider.CipherCore.unpad(CipherCore.java:975) at com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1056) at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853) at com.sun.crypto.provider.DESedeCipher.engineDoFinal(DESedeCipher.java:294) at javax.crypto.Cipher.doFinal(Cipher.java:2168) at com.arjun.mytest.PMAdminKeyTest.main(PMAdminKeyTest.java:41)
- 故障复现代码:
import java.security.KeyStore; import java.security.Provider; import java.security.spec.KeySpec; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; public class PMAdminKeyTest { public static void main(String[] args) throws Exception { // Requirement is to generate Key based on password and salt SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec keySpec = new PBEKeySpec("password".toCharArray(), "salt".getBytes(), 65536, 192); SecretKey key = new SecretKeySpec(secretKeyFactory.generateSecret(keySpec).getEncoded(), "DESede"); System.out.println("key.getAlgorithm(): " + key.getAlgorithm()); byte[] data = "12345678".getBytes("UTF8"); // Existing encrypt and decrypt code. There is already lot of data in DB // encrypted in this manner. I dont think I can change this code. Cipher cipher = Cipher.getInstance(key.getAlgorithm() + "/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] encryptedData = cipher.doFinal(data); System.out.println("encryptedData: " + encryptedData.toString()); cipher.init(Cipher.DECRYPT_MODE, key); byte[] decryptedData = cipher.doFinal(data); System.out.println("decryptedData: " + decryptedData.toString()); } }
故障根因
两个问题共同导致异常抛出:
- 代码笔误:解密调用
cipher.doFinal()时传入的是原始明文data,而非加密生成的encryptedData,属于低级逻辑错误 - 密钥不合法:DESede(3DES)算法要求密钥每个字节的最低位为奇偶校验位,每个字节中二进制1的个数必须为奇数。PBKDF2直接派生的192位密钥字节不满足该校验规则时,JCE在初始化Cipher时会自动调整密钥位完成校验,导致实际用于加解密的密钥和生成的原始密钥不一致,就算修正笔误,也会出现密钥不匹配导致的解密失败。
兼容实现方案
按照以下步骤调整即可兼容现有加解密逻辑:
- 修正解密阶段的参数错误,传入加密后的字节数组进行解密
- 对PBKDF2派生的原始密钥字节做奇偶校验位调整,生成完全符合DESede规范的密钥,避免JCE自动修改密钥位
- 固定PBKDF2的迭代次数、盐值、密钥长度参数,保证相同密码输入下每次生成的密钥完全一致
调整后的可运行代码如下:
import java.security.spec.KeySpec; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; public class PMAdminKeyTest { public static void main(String[] args) throws Exception { // 固定配置参数,生产环境请妥善保管盐值、迭代次数配置,不要随意修改 String password = "password"; byte[] salt = "salt".getBytes(); int iterationCount = 65536; int keyLength = 192; // PBKDF2派生原始密钥 SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength); byte[] rawKey = secretKeyFactory.generateSecret(keySpec).getEncoded(); // 调整密钥字节,满足DESede奇偶校验要求 adjustDESedeParityBit(rawKey); SecretKey key = new SecretKeySpec(rawKey, "DESede"); System.out.println("key.getAlgorithm(): " + key.getAlgorithm()); byte[] data = "12345678".getBytes("UTF8"); // 现有加解密逻辑无需任何修改 Cipher cipher = Cipher.getInstance(key.getAlgorithm() + "/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] encryptedData = cipher.doFinal(data); System.out.println("encryptedData: " + encryptedData); // 修正参数错误:解密传入加密后的字节数组 cipher.init(Cipher.DECRYPT_MODE, key); byte[] decryptedData = cipher.doFinal(encryptedData); System.out.println("decryptedData: " + new String(decryptedData, "UTF8")); } /** * 调整3DES密钥的奇偶校验位,保证每个字节二进制1的个数为奇数 */ private static void adjustDESedeParityBit(byte[] key) { for (int i = 0; i < key.length; i++) { int b = key[i] & 0xFE; int count = Integer.bitCount(b); // 若1的个数为偶数,将最低位置1保证奇数校验 key[i] = (byte) ((count % 2 == 0) ? (b | 0x01) : b); } } }
注意:DESede算法本身安全强度较低,ECB模式也存在固有安全风险,若后续有系统重构机会建议替换为AES-GCM等更安全的加密方案,当前方案仅用于兼容存量业务,不要用于新业务开发。
内容的提问来源于stack exchange,提问作者mee
相关产品推荐
相关产品推荐

