Java转JavaScript:PBKDF2WithHmacSHA1加密结果不一致求助
PBKDF2 Java转JavaScript结果不一致问题排查与修复
问题描述
我需要将一段Java的PBKDF2密钥派生代码转换为使用window.crypto.subtle.importKey的JavaScript实现,但转换后两者加密结果不一致。
Java代码实现
import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import javax.enterprise.inject.Default; @Default public class SecurityPasswordEncoder { private SecretKeyFactory factory; public SecurityPasswordEncoder() throws NoSuchAlgorithmException { this.factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); } public String encode(CharSequence rawPassword) { return null; } public boolean matches(CharSequence rawPassword, String encodedPassword) { return false; } public boolean matches(String rawPassword, String encodedPassword, String salt) throws InvalidKeySpecException { byte[] osalt = hexStringToByteArray(salt); byte[] oValidate = new byte[32]; String sValidate; PBEKeySpec pbeKeySpec = new PBEKeySpec(rawPassword.toCharArray(), osalt, 10000, 384); Key secretKey = this.factory.generateSecret(pbeKeySpec); System.arraycopy(secretKey.getEncoded(), 0, oValidate, 0, 32); sValidate = byteArrayToHexString(oValidate); return (sValidate.equals(encodedPassword)); } private byte[] hexStringToByteArray(String s) { byte[] b = new byte[s.length() / 2]; for (int i = 0; i < b.length; i++) { int index = i * 2; int v = Integer.parseInt(s.substring(index, index + 2), 16); b[i] = (byte) v; } return b; } private String byteArrayToHexString(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); } private char[] hexArray = "0123456789ABCDEF".toCharArray(); }
原始JavaScript代码
function deriveAKey(password, salt, iterations, hash) { // First, create a PBKDF2 "key" containing the password window.crypto.subtle.importKey( "raw", stringToArrayBuffer(password), {"name": "PBKDF2"}, false, ["deriveKey"]). then(function(baseKey){ // Derive a key from the password return window.crypto.subtle.deriveKey( { "name": "PBKDF2", "salt": stringToArrayBuffer(salt), "iterations": iterations, "hash": hash }, baseKey, {name:"HMAC",hash:"SHA-1"}, // Key we want.Can be any AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH", or "HMAC") true, // Extractable ["sign", "verify"] // For new key ); }).then(function(aesKey) { // Export it so we can display it return window.crypto.subtle.exportKey("raw", aesKey); }).then(function(keyBytes) { // Display key in Base64 format var keyS = arrayBufferToString(keyBytes); var keyB64 = btoa (keyS); console.log(keyB64); }).catch(function(err) { alert("Key derivation failed: " + err.message); }); } //Utility functions function stringToArrayBuffer(byteString){ var byteArray = new Uint8Array(byteString.length/2); for (let i = 0; i < byteArray.length; i++) { const index = i * 2; byteArray[i] = parseInt(byteString.substring(index, index + 2), 16); } return byteArray; } function arrayBufferToString(buffer){ var byteArray = new Uint8Array(buffer.length * 2); var byteString = ''; var hexArray = "0123456789ABCDEF".split(""); for (let j = 0; j < buffer.length; j++) { let v = bytes[j] & 0xFF; byteString[j * 2] = hexArray[v >>> 4]; byteString[j * 2 + 1] = hexArray[v & 0x0F]; } return String.fromCodePoint(byteString); }
问题排查与修复点
1. 密码的编码处理错误
Java中PBEKeySpec接收char[]类型的密码,会将其按UTF-16BE(Java默认字符编码的字节序)转换为字节数组;而原始JS代码错误地将密码当作十六进制字符串解析,导致密码的字节表示完全不符。
修复:将密码字符串转换为UTF-16BE的ArrayBuffer,和Java的处理逻辑对齐。
2. 密钥派生方式错误
Java中直接生成384位(48字节)的原始派生密钥,再截取前32字节;原始JS代码使用deriveKey生成HMAC-SHA1密钥,这会对派生的原始字节进行适配处理,导致结果不一致。
修复:使用deriveBits直接获取PBKDF2派生的原始字节数据,再截取前32字节。
3. 数组转十六进制函数错误
原始JS的arrayBufferToString存在多个问题:
- 变量名错误:使用未定义的
bytes变量,实际应为buffer - 字符串赋值错误:JS字符串不可变,不能通过索引直接赋值
- 结果转换错误:使用
String.fromCodePoint完全不符合十六进制转换逻辑
修复:重新实现正确的ArrayBuffer转大写十六进制字符串的函数。
4. 输出格式不匹配
Java输出的是大写十六进制字符串,原始JS代码输出Base64格式,两者格式完全不同。
修复:将派生的字节转换为和Java一致的大写十六进制字符串。
修正后的JavaScript代码
async function deriveAndMatchPassword(rawPassword, encodedPassword, salt) { const iterations = 10000; const keyLength = 384; // 对应Java的384位密钥长度 try { // 1. 将密码转换为UTF-16BE的ArrayBuffer(对齐Java的char[]处理) const passwordBuffer = stringToUtf16BEArrayBuffer(rawPassword); // 2. 将盐(十六进制字符串)转换为ArrayBuffer const saltBuffer = hexStringToArrayBuffer(salt); // 3. 导入密码作为PBKDF2的基础密钥 const baseKey = await window.crypto.subtle.importKey( "raw", passwordBuffer, { name: "PBKDF2" }, false, ["deriveBits"] ); // 4. 派生384位的原始字节 const derivedBits = await window.crypto.subtle.deriveBits( { name: "PBKDF2", salt: saltBuffer, iterations: iterations, hash: "SHA-1" // 对应Java的PBKDF2WithHmacSHA1 }, baseKey, keyLength ); // 5. 截取前32字节(和Java的System.arraycopy逻辑一致) const validateBuffer = derivedBits.slice(0, 32); // 6. 转换为大写十六进制字符串 const sValidate = arrayBufferToHexString(validateBuffer); // 7. 对比结果 console.log("派生结果:", sValidate); console.log("是否匹配:", sValidate === encodedPassword); return sValidate === encodedPassword; } catch (err) { console.error("密钥派生失败:", err.message); return false; } } // 工具函数:将字符串转换为UTF-16BE的ArrayBuffer(对齐Java的char[]处理) function stringToUtf16BEArrayBuffer(str) { const buffer = new ArrayBuffer(str.length * 2); const bigEndianView = new DataView(buffer); for (let i = 0; i < str.length; i++) { bigEndianView.setUint16(i * 2, str.charCodeAt(i), false); // false表示大端字节序 } return buffer; } // 工具函数:十六进制字符串转ArrayBuffer(和Java的hexStringToByteArray一致) function hexStringToArrayBuffer(hexStr) { const buffer = new ArrayBuffer(hexStr.length / 2); const view = new Uint8Array(buffer); for (let i = 0; i < view.length; i++) { const index = i * 2; view[i] = parseInt(hexStr.substring(index, index + 2), 16); } return buffer; } // 工具函数:ArrayBuffer转大写十六进制字符串(和Java的byteArrayToHexString一致) function arrayBufferToHexString(buffer) { const bytes = new Uint8Array(buffer); const hexArray = "0123456789ABCDEF"; let hexStr = ""; for (let j = 0; j < bytes.length; j++) { const v = bytes[j] & 0xFF; hexStr += hexArray[v >>> 4] + hexArray[v & 0x0F]; } return hexStr; }
使用示例
// 传入和Java相同的参数 deriveAndMatchPassword("yourRawPassword", "expectedEncodedHex", "yourHexSalt");
内容的提问来源于stack exchange,提问作者Chan Yoong Hon
相关产品推荐
相关产品推荐

