如何解密PBKDF2与AES/GCM加密数据?Nonce解码问题求助
Phantom钱包导出数据解密问题:Nonce解码失败与Tag mismatch错误
我尝试解密从Phantom钱包导出的如下格式数据(解密密码为123123123):
{ "encrypted": "Cj7psrZSx4oyzdMHpmemhHT21HrRzQxZ9Qyk9v3g3sg3", "nonce": "Gs1UtjfmNJ48rwqNPwfv5MMxcwFYpvtsd", "kdf": "pbkdf2", "salt": "JHjFEhDPmLFsRVshHYMprr", "iterations": 10000, "digest": "sha256" }
编写了如下Java代码解密,但Base64解码nonce时出现错误Last unit does not have enough valid bits,修剪nonce后又出现Tag mismatch错误:
public static void main(String[] args) throws Exception { String encrypted = "Cj7psrZSx4oyzdMHpmemhHT21HrRzQxZ9Qyk9v3g3sg3"; String nonce = "Gs1UtjfmNJ48rwqNPwfv5MMxcwFYpvtsd"; String salt = "JHjFEhDPmLFsRVshHYMprr"; SecretKey key = getAESKeyFromPassword("123123123".toCharArray(), salt.getBytes()); byte[] encryptedBytes = Base64.getDecoder().decode(encrypted); byte[] nonceBytes = Base64.getDecoder().decode(nonce); String decrypted = decryptAES(encryptedBytes, key, nonceBytes); } private static SecretKey getAESKeyFromPassword(char[] password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec spec = new PBEKeySpec(password, salt, 10000, 256); return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); } private static String decryptAES(byte[] cText, SecretKey secret, byte[] nonce) throws Exception { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, secret, new GCMParameterSpec(128, nonce)); byte[] plainText = cipher.doFinal(cText); return new String(plainText, UTF_8); }
请问该如何处理这个nonce?
问题分析与解决方案
1. Nonce解码失败的核心原因
Phantom钱包对nonce、encrypted等字段使用的是无填充的Base64URL编码,而你代码中用的Base64.getDecoder()是标准Base64解码器,它要求数据必须是4的倍数长度(不足时用=填充),因此会因缺少填充字符报错。
2. Tag mismatch的触发原因
修剪nonce会直接破坏其完整性,导致AES-GCM模式的认证标签验证失败;另外还要注意:
- Phantom的
encrypted字段包含了密文+GCM认证标签,无需拆分,只需正确解码 - KDF参数(迭代次数、密钥长度、哈希算法)必须和导出数据完全匹配,否则生成的密钥错误也会引发标签不匹配
3. 修正后的完整代码
import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.PBEKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; public class PhantomDecryptor { public static void main(String[] args) throws Exception { String encrypted = "Cj7psrZSx4oyzdMHpmemhHT21HrRzQxZ9Qyk9v3g3sg3"; String nonce = "Gs1UtjfmNJ48rwqNPwfv5MMxcwFYpvtsd"; String salt = "JHjFEhDPmLFsRVshHYMprr"; String password = "123123123"; // 使用Base64URL解码器,自动处理无填充的编码格式 Base64.Decoder urlDecoder = Base64.getUrlDecoder(); byte[] encryptedBytes = urlDecoder.decode(encrypted); byte[] nonceBytes = urlDecoder.decode(nonce); byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8); // 明确指定UTF-8编码,避免平台差异 SecretKey key = getAESKeyFromPassword(password.toCharArray(), saltBytes); String decrypted = decryptAES(encryptedBytes, key, nonceBytes); System.out.println("解密结果:" + decrypted); } private static SecretKey getAESKeyFromPassword(char[] password, byte[] salt) throws Exception { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); // 严格匹配Phantom导出的KDF参数:10000次迭代、256位密钥 PBEKeySpec spec = new PBEKeySpec(password, salt, 10000, 256); return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); } private static String decryptAES(byte[] cText, SecretKey secret, byte[] nonce) throws Exception { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // GCM标签长度固定128位,和Phantom的加密配置一致 GCMParameterSpec spec = new GCMParameterSpec(128, nonce); cipher.init(Cipher.DECRYPT_MODE, secret, spec); byte[] plainText = cipher.doFinal(cText); return new String(plainText, StandardCharsets.UTF_8); } }
关键修正点
- 将标准Base64解码器替换为
Base64.getUrlDecoder(),适配Phantom的无填充Base64URL编码 - 明确指定字符串转字节数组时的编码为
UTF_8,避免不同平台默认编码差异导致的错误 - 确保KDF参数(迭代次数、密钥长度)和导出数据完全一致,保证密钥生成正确
内容的提问来源于stack exchange,提问作者Alexander Prag
相关产品推荐
相关产品推荐

