Search code examples
javascriptc#.netcryptographypbkdf2

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


I'm using the following function in c# .net core 5 to generate a pbkdf2 key hash value:

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

the salt is a byte array, the password is a text string.

I need to be able to generate the same value in JavaScript. I've made it work with asmCrypto but would like to switch to the faster & standard Web Crypto API.

I believe that I need to execute this code in JavaScript (which I lifted from another example):

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);
    });

But I have been unsuccessful in generating a proper 'key'. I've tried with generateKey() - unsure if maybe it's importKey() - but I am unable to make it work either way. I believe generateKey needs HMAC-SHA1 to be compatible with c# pbkdf2.

Any help to make it run would be greatly appreciated. :-) Just a pointer to maybe how to generate the key and I can post the response once I validate they generate identical results.

Thank you.

-- Post answer I'm posting my final code here just in case it's useful for anyone needing a JS function as close to the C# version as possible:

/**
 * @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);
}

Solution

  • The posted code actually works. It just specifies the key size incorrectly (as suspected in the other answer), which may simply be a typo.
    deriveBits() expects the key size in bits in the 3rd parameter. Here, the current code specifies 10000 instead of the 128 bits applied in the C# code.
    With the change to 128 bits, the posted code produces the correct result (assuming the passphrase was imported correctly into a 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);
            });
        }); 
    });

    which produces the correct result, as a comparison with e.g. CyberChef shows.

    deriveBits() derives the binary data of the key without any coupling to an algorithm/mode or key usage. These are only specified when the binary data is imported into a CryptoKey with importKey().

    So if you need the binary data, deriveBits() is the most efficient way. If, on the other hand, you want to generate a CryptoKey directly, the deriveKey() function suggested in the other answer is a more efficient alternative, since it saves the second import. The results are identical, of course.

    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
            });
        });
    });