【问题标题】:JavaScript: How to generate Rfc2898DeriveBytes like C#?JavaScript:如何像 C# 一样生成 Rfc2898DeriveBytes?
【发布时间】:2015-04-26 16:58:01
【问题描述】:

编辑: 根据 cmets 中的讨论,让我澄清一下,这将发生在服务器端,位于 SSL 后面。我不打算向客户端公开散列密码或散列方案。

假设我们有一个现有的 asp.net 身份数据库,其中包含默认表(aspnet_Users、aspnet_Roles 等)。根据我的理解,密码哈希算法使用 sha256 并将 salt +(哈希密码)存储为 base64 编码字符串。 编辑:这个假设不正确,请参阅下面的答案。

我想用 JavaScript 版本复制 Microsoft.AspNet.Identity.Crypto 类的函数 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]);
  }
}

再一次,它并不漂亮,但是在运行这个 sn-p 之后,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 编码格式进行编码,但无济于事.

或者我可能完全错误地认为这些库在做什么。但是任何指向正确方向的指针都将不胜感激。

【问题讨论】:

  • 您是否尝试在客户端生成密码哈希并将哈希传递给服务器进行验证?
  • 不,我正在尝试在 node.js 中生成哈希服务器端。本质上,保持数据库相同,但将 IIS / asp.net 层换成节点。我不是安全专家,但我会警惕尝试在客户端进行任何密码操作。
  • 啊,这澄清了我的担忧。我会在你的问题中提到这一点。抱歉,这里实际上无法帮助处理 JS 方面的事情(
  • @trailmax 您能否进一步解释一下您对客户端哈希的担忧?
  • @bonh 看这个解释security.stackexchange.com/a/53606

标签: javascript c# asp.net-identity cryptojs rfc2898


【解决方案1】:

我知道这已经很晚了,但是我遇到了在 Node 中复制 C# 的 Rfc2898DeriveBytes.GetBytes 的问题,并且一直回到这个 SO 答案。我最终为我自己的使用创建了一个最小的类,我想我会分享以防其他人遇到同样的问题。它并不完美,但它确实有效。

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;

【讨论】:

  • 哦!如果您使用的是 AESManaged,Microsoft 文档说它是 Rijndael 128,但实际上并非如此。为此,请使用 Node 的加密“aes-256-cbc”。希望这可以节省我所经历的时间/痛苦。
【解决方案2】:

这是另一个实际比较字节而不是转换为字符串表示的选项。

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 是我们用来存储 salt 的字节数(同样是默认值)。

【讨论】:

  • 可能比我正在做的要好得多。我只是抓住了一根稻草,试图理解基本的步骤顺序。
  • 我也用过表情符号
【解决方案3】:

之前的解决方案并非在所有情况下都有效。 假设您想将密码source 与数据库hash 中的哈希值进行比较,如果数据库被入侵,这在技术上是可行的,那么函数将返回true,因为子键是一个空字符串。

修改函数以赶上它并改为返回 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!");
}

【讨论】:

    【解决方案4】:

    好的,我认为这个问题最终比我做的要简单得多(并非总是如此)。在pbkdf2 spec 上执行 RTFM 操作后,我使用 Node crypto 和 .NET crypto 进行了一些并行测试,并且在解决方案上取得了相当不错的进展。

    以下 JavaScript 代码正确解析存储的 salt 和子密钥,然后通过使用存储的 salt 对其进行散列来验证给定的密码。毫无疑问,有更好/更清洁/更安全的调整,所以欢迎 cmets。

    // 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!");
    }
    

    【讨论】:

    • 您先生刚刚救了我的命。非常感谢。我正在从 ASP.NET 迁移到 node.js,现在我不必告诉我的用户他们的密码已过期! :D
    • 如果你允许的话,我会发布一些关键字,以便像我这样的人以后可以更轻松地找到这个:SimpleMembershipProviider hash algorythm ASP.NET MVC password hashing compare
    • 这也为我的 ASP.Net 到 Node 项目的转换节省了时间!谢谢!
    • 我最终把它写成一个节点模块:github.com/CmdrShepardsPie/JavaScript-Helpers/blob/master/…
    • @GojiraDeMonstah 谢谢!在需要使用现有 .Net db 进行身份验证的 Node 应用程序上工作,这正是我所需要的。如果可以的话,我会投票两次;)
    猜你喜欢
    • 2021-03-10
    • 1970-01-01
    • 1970-01-01
    • 2012-05-02
    • 2013-09-25
    • 2023-04-03
    • 2018-08-06
    • 1970-01-01
    • 2010-12-27
    相关资源
    最近更新 更多