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

编辑:在评论中的每次讨论中,让我澄清一下,这将发生在SSL后面的服务器端。 我不打算将散列密码或散列方案暴露给客户端。

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

我想用JavaScript版本复制Microsoft.AspNet.Identity.Crypto类’ VerifyHashedPassword函数的function。

假设密码是welcome1 ,其asp.net哈希密码是ADOEtXqGCnWCuuc5UOAVIvMVJWJANOA / LoVy0E4XCyUHIfJ7dfSY0Id + uJ20DTtG + A ==

到目前为止,我已经能够重现获取salt和存储的子键的方法部分。

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 0 && i  0 && i >16) { storedSubKeyBytes.push(hashedPasswordBytes[i]); } } 

同样,它并不漂亮,但在运行此代码片段后,saltbytes和storedSubKeyBytes匹配字节的字节,我在C#调试器中看到salt和storedSubkey。

最后,在C#中,Rfc2898DeriveBytes的一个实例用于根据提供的salt和密码生成一个新的子键,如下所示:

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

这就是我被困住的地方。 我已经尝试过其他人的解决方案,比如这个 ,我分别使用了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规范上执行RTFM操作后,我使用Node crypto和.NET crypto进行了一些并行测试,并在解决方案上取得了相当不错的进展。

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

 // 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 ,如果数据库被泄露,这在技术上是可行的,那么该函数将返回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!"); } 

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

 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的字节数(同样是默认值)。