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-это число итераций по умолчанию в