You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 23:10:28