简体   繁体   English

JavaScript:如何生成像C#这样的Rfc2898DeriveBytes?

[英]JavaScript: How to generate Rfc2898DeriveBytes like C#?

EDIT: Per discussion in the comments, let me clarify that this will be happening server side, behind SSL. 编辑:在评论中的每次讨论中,让我澄清一下,这将发生在SSL后面的服务器端。 I do not intend to expose the hashed password or the hashing scheme to the client. 我不打算将散列密码或散列方案暴露给客户端。

Assume we have an existing asp.net identity database with the default tables (aspnet_Users, aspnet_Roles, etc.). 假设我们有一个现有的asp.net身份数据库,其中包含默认表(aspnet_Users,aspnet_Roles等)。 Based on my understanding, the password hashing algorithm uses sha256 and stores the salt + (hashed password) as a base64 encoded string. 根据我的理解,密码哈希算法使用sha256并将salt +(哈希密码)存储为base64编码的字符串。 EDIT: This assumption is incorrect, see answer below. 编辑:这个假设不正确,请参阅下面的答案。

I would like to replicate the function of the Microsoft.AspNet.Identity.Crypto class' VerifyHashedPassword function with a JavaScript version. 我想用JavaScript版本复制Microsoft.AspNet.Identity.Crypto类' VerifyHashedPassword函数的功能。

Let's say that a password is welcome1 and its asp.net hashed password is ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A== 假设密码是welcome1 ,其asp.net散列密码为ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA / LoVy0E4XCyUHIfJ7dfSY0Id + uJ20DTtG + A ==

So far I have been able to reproduce the parts of the method that get the salt and the stored sub key. 到目前为止,我已经能够重现获取salt和存储的子键的方法部分。

Where the C# implementation does more or less this: 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);

I have the following in JavaScript (not elegant by any stretch): 我在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]);
  }
}

Again, it ain't pretty, but after running this snippet the saltbytes and storedSubKeyBytes match byte for byte what I see in the C# debugger for salt and storedSubkey. 同样,它并不漂亮,但在运行此片段之后,saltbytes和storedSubKeyBytes匹配字节,这是我在C#调试器中看到的salt和storedSubkey。

Finally, in C#, an instance of Rfc2898DeriveBytes is used to generate a new subkey based on the salt and the password provided, like so: 最后,在C#中,Rfc2898DeriveBytes的一个实例用于根据提供的salt和密码生成一个新的子键,如下所示:

byte[] generatedSubkey;
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, PBKDF2IterCount))
{
   generatedSubkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
}

This is where I'm stuck. 这就是我被困住的地方。 I have tried others' solutions such as this one , I have used Google's and Node's CryptoJS and crypto libraries respectively, and my output never generates anything resembling the C# version. 我已经尝试过其他人的解决方案,比如这个 ,我分别使用了Google和Node的CryptoJS和加密库,我的输出永远不会生成类似C#版本的东西。

(Example: (例:

var output = crypto.pbkdf2Sync(new Buffer('welcome1', 'utf16le'), 
    new Buffer(parsedSaltString), 1000, 32, 'sha256');
console.log(output.toString('base64'))

generates "LSJvaDM9u7pXRfIS7QDFnmBPvsaN2z7FMXURGHIuqdY=") 生成“LSJvaDM9u7pXRfIS7QDFnmBPvsaN2z7FMXURGHIuqdY =”)

Many of the pointers I've found online indicate problems involving encoding mismatches (NodeJS / UTF-8 vs. .NET / UTF-16LE), so I've tried encoding using the default .NET encoding format but to no avail. 我在网上找到的许多指针都表明涉及编码不匹配的问题(NodeJS / UTF-8与.NET / UTF-16LE),所以我尝试使用默认的.NET编码格式进行编码,但无济于事。

Or I could be completely wrong about what I assume these libraries are doing. 或者我可能完全错误地认为这些库正在做什么。 But any pointers in the right direction would be much appreciated. 但任何指向正确方向的人都会非常感激。

Ok, I think this problem ended up being quite a bit simpler than I was making it (aren't they always). 好吧,我认为这个问题最终比我做的要简单得多(不是他们总是)。 After performing a RTFM operation on the pbkdf2 spec , I ran some side-by-side tests with Node crypto and .NET crypto, and have made pretty good progress on a solution. pbkdf2规范上执行RTFM操作后,我使用Node crypto和.NET crypto进行了一些并行测试,并在解决方案上取得了相当不错的进展。

The following JavaScript code correctly parses the stored salt and subkey, then verifies the given password by hashing it with the stored salt. 以下JavaScript代码正确解析存储的salt和subkey,然后通过使用存储的salt对其进行散列来验证给定的密码。 There are doubtless better / cleaner / more secure tweaks, so comments welcome. 毫无疑问,更好/更清洁/更安全的调整,所以评论欢迎。

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

The previous solution will not work in all cases. 以前的解决方案并不适用于所有情况。 Let's say that you want to compare a password source against a hash in the database hash , which can be technically possible if the database is compromised, then the function will return true because the subkey is an empty string. 假设您要将密码source与数据库哈希中的hash ,如果数据库被泄露,这在技术上是可行的,那么该函数将返回true因为该子键是空字符串。

Modify the function to catch that up and return false instead. 修改函数以捕获它并返回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!");
}

Here's another option which actually compares the bytes as opposed to converting to a string representation. 这是另一个实际比较字节而不是转换为字符串表示的选项。

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 is the default number of iterations in System.Security.Cryptography.Rfc2898DeriveBytes and 20 is the number of bytes we are using to store the salt (again the default). 1000是System.Security.Cryptography.Rfc2898DeriveBytes中的默认迭代次数,20是我们用于存储salt的字节数(同样是默认值)。

I know this is rather late, but I ran into an issue with reproducing C#'s Rfc2898DeriveBytes.GetBytes in Node, and kept coming back to this SO answer. 我知道这已经很晚了,但我遇到了在Node中复制C#的Rfc2898DeriveBytes.GetBytes的问题,并不断回到这个SO答案。 I ended up creating a minimal class for my own usage, and I figured I'd share in case someone else was having the same issues. 我最终为自己的用法创建了一个最小的类,我想我会分享以防其他人遇到同样的问题。 It's not perfect, but it works. 它并不完美,但它确实有效。

 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; 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM