使用 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 或密钥用法没有任何耦合。这些仅在将二进制数据导入 CryptoKey
和 importKey()
.
时指定
所以如果你需要二进制数据,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
});
});
});
我在 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 或密钥用法没有任何耦合。这些仅在将二进制数据导入 CryptoKey
和 importKey()
.
所以如果你需要二进制数据,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
});
});
});