JavaScript: как генерировать Rfc2898DeriveBytes, такие как C#?
EDIT: за обсуждение в комментариях, позвольте мне уточнить, что это будет происходить на стороне сервера, за SSL. Я не намерен предоставлять хэшированный пароль или схему хэширования клиенту.
предположим, что у нас есть существующий asp.net база данных идентификаторов с таблицами по умолчанию (aspnet_Users, aspnet_Roles и т. д.). Исходя из моего понимания, алгоритм хэширования паролей использует sha256 и сохраняет salt + (хэшированный пароль) в качестве закодированной строки base64. EDIT: это предположение неверно, см. ответ ниже.
Я хотел бы повторить функцию Microsoft.сеть САШ.Тождественность.Crypto class'VerifyHashedPassword функция с версией JavaScript.
предположим, что пароль 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# для соли и storedSubkey.
наконец, в C#, экземпляр Rfc2898DeriveBytes используется для создания нового раздела на основе соли и пароль, вот так:
byte[] generatedSubkey;
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, PBKDF2IterCount))
{
generatedSubkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
}
вот где я застрял. Я пробовал другие решения, такие как этот, я использовал криптографические библиотеки Google и Node соответственно, и мой вывод никогда не генерирует ничего похожего на версию c#.
(например:
var output = crypto.pbkdf2Sync(new Buffer('welcome1', 'utf16le'),
new Buffer(parsedSaltString), 1000, 32, 'sha256');
console.log(output.toString('base64'))
создает "LSJvaDM9u7pXRfIS7QDFnmBPvsaN2z7Fmxurghiuqdy=")
многие из указателей я нашел в интернете указывают на проблемы, связанные с несоответствием кодирования (NodeJS / в UTF-8 против .Нетто / в UTF-16LE), так я пробовал кодировку по умолчанию .Формат объем кодирования, но безрезультатно.
или я могу быть совершенно неправ в том, что я предполагаю, что эти библиотеки делают. Но любые указатели в правильном направлении будут высоко оценены.
3 ответов
хорошо, я думаю, что эта проблема оказалась довольно простой, чем я ее делал (не всегда). После выполнения операции RTFM на спец PBKDF2 с, Я провел несколько параллельных тестов с 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
, что может быть технически возможно, если база данных скомпрометирована, то функция вернет 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-это число итераций по умолчанию в