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;