使用 JavaScript Web Crypto API 生成 c# 兼容的 pbkdf2 密钥

Using JavaScript Web Crypto API to generate c# compatible pbkdf2 key

我在 c# .net core 5 中使用以下函数生成 pbkdf2 密钥哈希值:

HashPassword = KeyDerivation.Pbkdf2(password, SaltPassword, KeyDerivationPrf.HMACSHA256, 10000, 16);

盐是一个字节数组,密码是一个文本字符串。

我需要能够在 JavaScript 中生成相同的值。我已经让它与 asmCrypto 一起工作,但想切换到更快和标准的 Web Crypto API.

我认为我需要在 JavaScript 中执行此代码(我从另一个示例中提取):

window.crypto.subtle.deriveBits(
    {
        name: "PBKDF2",
        hash: "SHA-256",
        salt: window.crypto.getRandomValues(new Uint8Array(16)),
        iterations: 10000
    },
    key, 
    10000)
    .then(function (bits) {
        //returns the derived bits as an ArrayBuffer
        console.log(new Uint8Array(bits));
    })
    .catch(function (err) {
        console.error(err);
    });

但我未能成功生成正确的 'key'。我试过使用 generateKey() - 不确定它是否是 importKey() - 但我无法让它以任何一种方式工作。我相信 generateKey 需要 HMAC-SHA1 才能与 c# pbkdf2 兼容。

如能提供帮助 运行,我们将不胜感激。 :-) 只是指向可能如何生成密钥的指针,一旦我验证它们生成相同的结果,我就可以 post 响应。

谢谢。

-- Post 回答我post在这里编写我的最终代码,以防它对任何需要尽可能接近 C# 版本的 JS 函数的人有用:

/**
 * @param {string} strPassword The clear text password
 * @param {Uint8Array} salt    The salt
 * @param {string} hash        The Hash model, e.g. ["SHA-256" | "SHA-512"]
 * @param {int} iterations     Number of iterations
 * @param {int} len            The output length in bytes, e.g. 16
 */
async function pbkdf2(strPassword, salt, hash, iterations, len) {
    var password = new TextEncoder().encode(strPassword);

    var ik = await window.crypto.subtle.importKey("raw", password, { name: "PBKDF2" }, false, ["deriveBits"]);
    var dk = await window.crypto.subtle.deriveBits(
        {
            name: "PBKDF2",
            hash: hash,
            salt: salt,
            iterations: iterations
        },
        ik,
        len * 8);  // Bytes to bits

    return new Uint8Array(dk);
}

我认为问题出在密钥长度上。这是生成密码哈希的代码(= en-/decryption 密钥,例如 AES):

HashPassword = KeyDerivation.Pbkdf2(密码, SaltPassword, KeyDerivationPrf.HMACSHA256, 10000, 16);

我在末尾标记了“16”,我相信它告诉我们要获得一个 16 字节长的密钥 (AES-128)。在 WebCrypto 方面,您也需要指定它,因此请注意 我的实现将派生一个 32 字节 = 256 位长的密钥 ,要使其类似于 C# 工作,您需要更改代码

{ name: mode, length: 128 },

这是我的 WebCrypto.subtle 代码(生成一个 32 字节长的密钥):

const pbkdf2 = (password, salt, iterations, hash, mode) =>
  crypto.subtle
    .importKey("raw", password, { name: "PBKDF2" }, false, ["deriveKey"])
    .then(baseKey =>
      crypto.subtle.deriveKey(
        { name: "PBKDF2", salt, iterations, hash },
        baseKey,
        { name: mode, length: 256 },
        true,
        ["encrypt", "decrypt"]
      )
    )
    .then(key => crypto.subtle.exportKey("raw", key));

发布的代码确实有效。它只是错误地指定了密钥大小(正如 所怀疑的那样),这可能只是一个拼写错误。
deriveBits() 需要第三个参数中的密钥大小(以位为单位)。此处,当前代码指定 10000 而不是 C# 代码中应用的 128 位。
更改为 128 位后,发布的代码会产生正确的结果(假设密码短语已正确导入 CryptoKey):

var passphrase = new TextEncoder().encode('a sample passphrase');

// Import passphrase
window.crypto.subtle.importKey("raw", passphrase, { name: "PBKDF2" }, false, ["deriveBits"])
.then(function(passphraseImported){
    
    // Derive key as ArrayBuffer
    window.crypto.subtle.deriveBits(
        {
            name: "PBKDF2",
            hash: 'SHA-256',
            salt: new TextEncoder().encode('a sample salt'), // fix for testing, otherwise window.crypto.getRandomValues(new Uint8Array(16)), 
            iterations: 10000
        },
        passphraseImported, 
        128 // Fix!
    )  
    .then(function (bits) {
        console.log("raw key:", new Uint8Array(bits)); // 7, 167, 39, 145, 34, 48, 60, 159, 242, 209, 254, 79, 78, 150, 215, 88  
        
        // If necessary, import as CryptoKey, e.g. for encryption/decryption with AES-CBC
        window.crypto.subtle.importKey("raw", bits, { name: "AES-CBC" }, false, ["encrypt", "decrypt"])
        .then(function(cryptoKey){
            console.log("CryptoKey:", cryptoKey);
        });
    }); 
});

这会产生 正确的 结果,作为与例如CyberChef 显示。

deriveBits() 导出密钥 的二进制数据,而 与 algorithm/mode 或密钥用法没有任何耦合。这些仅在将二进制数据导入 CryptoKeyimportKey().

时指定

所以如果你需要二进制数据,deriveBits()是最有效的方法。另一方面,如果您想直接生成 CryptoKey ,另一个答案中建议的 deriveKey() 函数是一个更有效的选择,因为它节省了第二次进口。结果当然是一样的。

var passphrase = new TextEncoder().encode('a sample passphrase');

// Import passphrase
window.crypto.subtle.importKey("raw", passphrase, { name: "PBKDF2" }, false, ["deriveKey"])
.then(function(passphraseImported){
    
    // Derive key as CryptoKey, e.g. for encryption/decryption with AES-CBC
    window.crypto.subtle.deriveKey(
        { 
            name: "PBKDF2", 
            hash: 'SHA-256', 
            salt: new TextEncoder().encode('a sample salt'), // fix for testing, otherwise window.crypto.getRandomValues(new Uint8Array(16)), 
            iterations: 10000 
        },
        passphraseImported,
        { name: 'AES-CBC', length: 128 },
        true,
        ["encrypt", "decrypt"]
    )
    .then(function(cryptoKey){
        console.log("CryptoKey:", cryptoKey);  
        
        // If necessary, export as ArrayBuffer
        window.crypto.subtle.exportKey("raw", cryptoKey).then(function (keyRaw) {                       
            console.log("raw key", new Uint8Array(keyRaw)); // 7, 167, 39, 145, 34, 48, 60, 159, 242, 209, 254, 79, 78, 150, 215, 88
        });
    });
});