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