JavaScript: 如何像 C# 一样生成 Rfc2898DeriveBytes?
JavaScript: How to generate Rfc2898DeriveBytes like C#?
编辑: 根据评论中的讨论,让我澄清一下,这将发生在 SSL 背后的服务器端。我不打算向客户端公开散列密码或散列方案。
假设我们有一个现有的 asp.net 身份数据库,其中包含默认表(aspnet_Users、aspnet_Roles 等)。根据我的理解,密码哈希算法使用 sha256 并将 salt +(哈希密码)存储为 base64 编码字符串。 编辑:这个假设是不正确的,见下面的答案。
我想用 JavaScript 版本复制 Microsoft.AspNet.Identity.Crypto class' VerifyHashedPassword 函数的功能。
假设密码是 welcome1 并且它的 asp.net 散列密码是 ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==
到目前为止,我已经能够重现方法中获取盐和存储的子密钥的部分。
C# 实现或多或少是这样的:
var salt = new byte[SaltSize];
Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, SaltSize);
var storedSubkey = new byte[PBKDF2SubkeyLength];
Buffer.BlockCopy(hashedPasswordBytes, 1 + SaltSize, storedSubkey, 0, PBKDF2SubkeyLength);
我在 JavaScript 中有以下内容(无论如何都不优雅):
var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==";
var hashedPasswordBytes = new Buffer(hashedPwd, 'base64');
var saltbytes = [];
var storedSubKeyBytes = [];
for(var i=1;i<hashedPasswordBytes.length;i++)
{
if(i > 0 && i <= 16)
{
saltbytes.push(hashedPasswordBytes[i]);
}
if(i > 0 && i >16) {
storedSubKeyBytes.push(hashedPasswordBytes[i]);
}
}
同样,它并不漂亮,但是在 运行 这个片段之后,saltbytes 和 storedSubKeyBytes 逐字节匹配我在 C# 调试器中看到的 salt 和 storedSubkey。
最后,在 C# 中,Rfc2898DeriveBytes 的实例用于根据提供的盐和密码生成新的子密钥,如下所示:
byte[] generatedSubkey;
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, PBKDF2IterCount))
{
generatedSubkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
}
这就是我卡住的地方。我尝试过其他人的解决方案,例如 this one,我分别使用了 Google 和 Node 的 CryptoJS 和加密库,但我的输出从未生成任何类似于 C# 版本的内容。
(示例:
var output = crypto.pbkdf2Sync(new Buffer('welcome1', 'utf16le'),
new Buffer(parsedSaltString), 1000, 32, 'sha256');
console.log(output.toString('base64'))
生成 "LSJvaDM9u7pXRfIS7QDFnmBPvsaN2z7FMXURGHIuqdY=")
我在网上找到的许多指示都指出涉及编码不匹配的问题(NodeJS/UTF-8 与 .NET/UTF-16LE),因此我尝试使用默认的 .NET 编码格式进行编码,但是没用。
或者我对这些库所做的假设可能是完全错误的。但是任何指向正确方向的指示都将不胜感激。
好的,我认为这个问题最终比我做的要简单得多(他们不总是这样)。在 pbkdf2 spec 上执行 RTFM 操作后,我 运行 使用 Node crypto 和 .NET crypto 进行了一些并行测试,并在解决方案上取得了相当大的进展。
以下 JavaScript 代码正确解析了存储的盐和子密钥,然后通过使用存储的盐对其进行散列来验证给定的密码。毫无疑问,还有更好/更清洁/更安全的调整,欢迎评论。
// NodeJS implementation of crypto, I'm sure google's
// cryptoJS would work equally well.
var crypto = require('crypto');
// The value stored in [dbo].[AspNetUsers].[PasswordHash]
var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==";
var hashedPasswordBytes = new Buffer(hashedPwd, 'base64');
var hexChar = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
var saltString = "";
var storedSubKeyString = "";
// build strings of octets for the salt and the stored key
for (var i = 1; i < hashedPasswordBytes.length; i++) {
if (i > 0 && i <= 16) {
saltString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f]
}
if (i > 0 && i > 16) {
storedSubKeyString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f];
}
}
// password provided by the user
var password = 'welcome1';
// TODO remove debug - logging passwords in prod is considered
// tasteless for some odd reason
console.log('cleartext: ' + password);
console.log('saltString: ' + saltString);
console.log('storedSubKeyString: ' + storedSubKeyString);
// This is where the magic happens.
// If you are doing your own hashing, you can (and maybe should)
// perform more iterations of applying the salt and perhaps
// use a stronger hash than sha1, but if you want it to work
// with the [as of 2015] Microsoft Identity framework, keep
// these settings.
var nodeCrypto = crypto.pbkdf2Sync(new Buffer(password), new Buffer(saltString, 'hex'), 1000, 256, 'sha1');
// get a hex string of the derived bytes
var derivedKeyOctets = nodeCrypto.toString('hex').toUpperCase();
console.log("hex of derived key octets: " + derivedKeyOctets);
// The first 64 bytes of the derived key should
// match the stored sub key
if (derivedKeyOctets.indexOf(storedSubKeyString) === 0) {
console.info("passwords match!");
} else {
console.warn("passwords DO NOT match!");
}
之前的解决方案并不适用于所有情况。
假设您想将密码 source
与数据库中的哈希 hash
进行比较,如果数据库遭到破坏,这在技术上是可行的,那么该函数将 return true
因为子项是一个空字符串。
修改函数以赶上它并 return false 代替。
// NodeJS implementation of crypto, I'm sure google's
// cryptoJS would work equally well.
var crypto = require('crypto');
// The value stored in [dbo].[AspNetUsers].[PasswordHash]
var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==";
var hashedPasswordBytes = new Buffer(hashedPwd, 'base64');
var hexChar = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
var saltString = "";
var storedSubKeyString = "";
// build strings of octets for the salt and the stored key
for (var i = 1; i < hashedPasswordBytes.length; i++) {
if (i > 0 && i <= 16) {
saltString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f]
}
if (i > 0 && i > 16) {
storedSubKeyString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f];
}
}
if (storedSubKeyString === '') { return false }
// password provided by the user
var password = 'welcome1';
// TODO remove debug - logging passwords in prod is considered
// tasteless for some odd reason
console.log('cleartext: ' + password);
console.log('saltString: ' + saltString);
console.log('storedSubKeyString: ' + storedSubKeyString);
// This is where the magic happens.
// If you are doing your own hashing, you can (and maybe should)
// perform more iterations of applying the salt and perhaps
// use a stronger hash than sha1, but if you want it to work
// with the [as of 2015] Microsoft Identity framework, keep
// these settings.
var nodeCrypto = crypto.pbkdf2Sync(new Buffer(password), new Buffer(saltString, 'hex'), 1000, 256, 'sha1');
// get a hex string of the derived bytes
var derivedKeyOctets = nodeCrypto.toString('hex').toUpperCase();
console.log("hex of derived key octets: " + derivedKeyOctets);
// The first 64 bytes of the derived key should
// match the stored sub key
if (derivedKeyOctets.indexOf(storedSubKeyString) === 0) {
console.info("passwords match!");
} else {
console.warn("passwords DO NOT match!");
}
这是另一个选项,它实际比较字节而不是转换为字符串表示形式。
const crypto = require('crypto');
const password = 'Password123';
const storedHashString = 'J9IBFSw0U1EFsH/ysL+wak6wb8s=';
const storedSaltString = '2nX0MZPZlwiW8bYLlVrfjBYLBKM=';
const storedHashBytes = new Buffer.from(storedHashString, 'base64');
const storedSaltBytes = new Buffer.from(storedSaltString, 'base64');
crypto.pbkdf2(password, storedSaltBytes, 1000, 20, 'sha1',
(err, calculatedHashBytes) => {
const correct = calculatedHashBytes.equals(storedHashBytes);
console.log('Password is ' + (correct ? 'correct ' : 'incorrect '));
}
);
1000 是 System.Security.Cryptography.Rfc2898DeriveBytes 中的默认迭代次数,20 是我们用来存储盐的字节数(同样是默认值)。
我知道这已经很晚了,但我 运行 遇到了在 Node 中重现 C# Rfc2898DeriveBytes.GetBytes 的问题,并一直回到这个 SO 答案。我最终为自己的使用创建了一个最小的 class,我想我会分享以防其他人遇到同样的问题。它并不完美,但它确实有效。
const crypto = require('crypto');
const $key = Symbol('key');
const $saltSize = Symbol('saltSize');
const $salt = Symbol('salt');
const $iterationCount = Symbol('iterationCount');
const $position = Symbol('position');
class Rfc2898DeriveBytes {
constructor(key, saltSize = 32, iterationCount = 1000) {
this[$key] = key;
this[$saltSize] = saltSize;
this[$iterationCount] = iterationCount;
this[$position] = 0;
this[$salt] = crypto.randomBytes(this[$saltSize]);
}
get salt() {
return this[$salt];
}
set salt(buffer) {
this[$salt] = buffer;
}
get iterationCount() {
return this[$iterationCount];
}
set iterationCount(count) {
this[$iterationCount] = count;
}
getBytes(byteCount) {
let position = this[$position];
let bytes = crypto.pbkdf2Sync(Buffer.from(this[$key]), this.salt, this.iterationCount, position + byteCount, 'sha1');
this[$position] += byteCount;
let result = Buffer.alloc(byteCount);
for (let i = 0; i < byteCount; i++) { result[i] = bytes[position + i]; }
return result;
}
}
module.exports = Rfc2898DeriveBytes;
编辑: 根据评论中的讨论,让我澄清一下,这将发生在 SSL 背后的服务器端。我不打算向客户端公开散列密码或散列方案。
假设我们有一个现有的 asp.net 身份数据库,其中包含默认表(aspnet_Users、aspnet_Roles 等)。根据我的理解,密码哈希算法使用 sha256 并将 salt +(哈希密码)存储为 base64 编码字符串。 编辑:这个假设是不正确的,见下面的答案。
我想用 JavaScript 版本复制 Microsoft.AspNet.Identity.Crypto class' VerifyHashedPassword 函数的功能。
假设密码是 welcome1 并且它的 asp.net 散列密码是 ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==
到目前为止,我已经能够重现方法中获取盐和存储的子密钥的部分。
C# 实现或多或少是这样的:
var salt = new byte[SaltSize];
Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, SaltSize);
var storedSubkey = new byte[PBKDF2SubkeyLength];
Buffer.BlockCopy(hashedPasswordBytes, 1 + SaltSize, storedSubkey, 0, PBKDF2SubkeyLength);
我在 JavaScript 中有以下内容(无论如何都不优雅):
var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==";
var hashedPasswordBytes = new Buffer(hashedPwd, 'base64');
var saltbytes = [];
var storedSubKeyBytes = [];
for(var i=1;i<hashedPasswordBytes.length;i++)
{
if(i > 0 && i <= 16)
{
saltbytes.push(hashedPasswordBytes[i]);
}
if(i > 0 && i >16) {
storedSubKeyBytes.push(hashedPasswordBytes[i]);
}
}
同样,它并不漂亮,但是在 运行 这个片段之后,saltbytes 和 storedSubKeyBytes 逐字节匹配我在 C# 调试器中看到的 salt 和 storedSubkey。
最后,在 C# 中,Rfc2898DeriveBytes 的实例用于根据提供的盐和密码生成新的子密钥,如下所示:
byte[] generatedSubkey;
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, PBKDF2IterCount))
{
generatedSubkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
}
这就是我卡住的地方。我尝试过其他人的解决方案,例如 this one,我分别使用了 Google 和 Node 的 CryptoJS 和加密库,但我的输出从未生成任何类似于 C# 版本的内容。
(示例:
var output = crypto.pbkdf2Sync(new Buffer('welcome1', 'utf16le'),
new Buffer(parsedSaltString), 1000, 32, 'sha256');
console.log(output.toString('base64'))
生成 "LSJvaDM9u7pXRfIS7QDFnmBPvsaN2z7FMXURGHIuqdY=")
我在网上找到的许多指示都指出涉及编码不匹配的问题(NodeJS/UTF-8 与 .NET/UTF-16LE),因此我尝试使用默认的 .NET 编码格式进行编码,但是没用。
或者我对这些库所做的假设可能是完全错误的。但是任何指向正确方向的指示都将不胜感激。
好的,我认为这个问题最终比我做的要简单得多(他们不总是这样)。在 pbkdf2 spec 上执行 RTFM 操作后,我 运行 使用 Node crypto 和 .NET crypto 进行了一些并行测试,并在解决方案上取得了相当大的进展。
以下 JavaScript 代码正确解析了存储的盐和子密钥,然后通过使用存储的盐对其进行散列来验证给定的密码。毫无疑问,还有更好/更清洁/更安全的调整,欢迎评论。
// NodeJS implementation of crypto, I'm sure google's
// cryptoJS would work equally well.
var crypto = require('crypto');
// The value stored in [dbo].[AspNetUsers].[PasswordHash]
var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==";
var hashedPasswordBytes = new Buffer(hashedPwd, 'base64');
var hexChar = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
var saltString = "";
var storedSubKeyString = "";
// build strings of octets for the salt and the stored key
for (var i = 1; i < hashedPasswordBytes.length; i++) {
if (i > 0 && i <= 16) {
saltString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f]
}
if (i > 0 && i > 16) {
storedSubKeyString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f];
}
}
// password provided by the user
var password = 'welcome1';
// TODO remove debug - logging passwords in prod is considered
// tasteless for some odd reason
console.log('cleartext: ' + password);
console.log('saltString: ' + saltString);
console.log('storedSubKeyString: ' + storedSubKeyString);
// This is where the magic happens.
// If you are doing your own hashing, you can (and maybe should)
// perform more iterations of applying the salt and perhaps
// use a stronger hash than sha1, but if you want it to work
// with the [as of 2015] Microsoft Identity framework, keep
// these settings.
var nodeCrypto = crypto.pbkdf2Sync(new Buffer(password), new Buffer(saltString, 'hex'), 1000, 256, 'sha1');
// get a hex string of the derived bytes
var derivedKeyOctets = nodeCrypto.toString('hex').toUpperCase();
console.log("hex of derived key octets: " + derivedKeyOctets);
// The first 64 bytes of the derived key should
// match the stored sub key
if (derivedKeyOctets.indexOf(storedSubKeyString) === 0) {
console.info("passwords match!");
} else {
console.warn("passwords DO NOT match!");
}
之前的解决方案并不适用于所有情况。
假设您想将密码 source
与数据库中的哈希 hash
进行比较,如果数据库遭到破坏,这在技术上是可行的,那么该函数将 return true
因为子项是一个空字符串。
修改函数以赶上它并 return false 代替。
// NodeJS implementation of crypto, I'm sure google's
// cryptoJS would work equally well.
var crypto = require('crypto');
// The value stored in [dbo].[AspNetUsers].[PasswordHash]
var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==";
var hashedPasswordBytes = new Buffer(hashedPwd, 'base64');
var hexChar = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
var saltString = "";
var storedSubKeyString = "";
// build strings of octets for the salt and the stored key
for (var i = 1; i < hashedPasswordBytes.length; i++) {
if (i > 0 && i <= 16) {
saltString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f]
}
if (i > 0 && i > 16) {
storedSubKeyString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f];
}
}
if (storedSubKeyString === '') { return false }
// password provided by the user
var password = 'welcome1';
// TODO remove debug - logging passwords in prod is considered
// tasteless for some odd reason
console.log('cleartext: ' + password);
console.log('saltString: ' + saltString);
console.log('storedSubKeyString: ' + storedSubKeyString);
// This is where the magic happens.
// If you are doing your own hashing, you can (and maybe should)
// perform more iterations of applying the salt and perhaps
// use a stronger hash than sha1, but if you want it to work
// with the [as of 2015] Microsoft Identity framework, keep
// these settings.
var nodeCrypto = crypto.pbkdf2Sync(new Buffer(password), new Buffer(saltString, 'hex'), 1000, 256, 'sha1');
// get a hex string of the derived bytes
var derivedKeyOctets = nodeCrypto.toString('hex').toUpperCase();
console.log("hex of derived key octets: " + derivedKeyOctets);
// The first 64 bytes of the derived key should
// match the stored sub key
if (derivedKeyOctets.indexOf(storedSubKeyString) === 0) {
console.info("passwords match!");
} else {
console.warn("passwords DO NOT match!");
}
这是另一个选项,它实际比较字节而不是转换为字符串表示形式。
const crypto = require('crypto');
const password = 'Password123';
const storedHashString = 'J9IBFSw0U1EFsH/ysL+wak6wb8s=';
const storedSaltString = '2nX0MZPZlwiW8bYLlVrfjBYLBKM=';
const storedHashBytes = new Buffer.from(storedHashString, 'base64');
const storedSaltBytes = new Buffer.from(storedSaltString, 'base64');
crypto.pbkdf2(password, storedSaltBytes, 1000, 20, 'sha1',
(err, calculatedHashBytes) => {
const correct = calculatedHashBytes.equals(storedHashBytes);
console.log('Password is ' + (correct ? 'correct ' : 'incorrect '));
}
);
1000 是 System.Security.Cryptography.Rfc2898DeriveBytes 中的默认迭代次数,20 是我们用来存储盐的字节数(同样是默认值)。
我知道这已经很晚了,但我 运行 遇到了在 Node 中重现 C# Rfc2898DeriveBytes.GetBytes 的问题,并一直回到这个 SO 答案。我最终为自己的使用创建了一个最小的 class,我想我会分享以防其他人遇到同样的问题。它并不完美,但它确实有效。
const crypto = require('crypto');
const $key = Symbol('key');
const $saltSize = Symbol('saltSize');
const $salt = Symbol('salt');
const $iterationCount = Symbol('iterationCount');
const $position = Symbol('position');
class Rfc2898DeriveBytes {
constructor(key, saltSize = 32, iterationCount = 1000) {
this[$key] = key;
this[$saltSize] = saltSize;
this[$iterationCount] = iterationCount;
this[$position] = 0;
this[$salt] = crypto.randomBytes(this[$saltSize]);
}
get salt() {
return this[$salt];
}
set salt(buffer) {
this[$salt] = buffer;
}
get iterationCount() {
return this[$iterationCount];
}
set iterationCount(count) {
this[$iterationCount] = count;
}
getBytes(byteCount) {
let position = this[$position];
let bytes = crypto.pbkdf2Sync(Buffer.from(this[$key]), this.salt, this.iterationCount, position + byteCount, 'sha1');
this[$position] += byteCount;
let result = Buffer.alloc(byteCount);
for (let i = 0; i < byteCount; i++) { result[i] = bytes[position + i]; }
return result;
}
}
module.exports = Rfc2898DeriveBytes;