9

EDIT: Per discussion in the comments, let me clarify that this will be happening server side, behind 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.). Based on my understanding, the password hashing algorithm uses sha256 and stores the salt + (hashed password) as a base64 encoded string. 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.

Let's say that a password is welcome1 and its asp.net hashed password is 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.

Where the C# implementation does more or less this:

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):

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.

Finally, in C#, an instance of Rfc2898DeriveBytes is used to generate a new subkey based on the salt and the password provided, like so:

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.

(Example:

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

generates "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.

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.

Community
  • 1
  • 1
Gojira
  • 2,941
  • 2
  • 21
  • 30
  • Are you trying to generate password hash on the client and pass the hash down to the server for validation? – trailmax Feb 25 '15 at 00:53
  • No, I'm trying to generate the hash server side in node.js. Essentially, keeping the database the same but swapping out the IIS / asp.net layer for node. I'm not a security expert but I would be wary of trying to do any password operations client side. – Gojira Feb 25 '15 at 16:28
  • Ah, that clarifies my concerns. I'd mention this in your question. Sorry, can't actually help with JS-side of things here( – trailmax Feb 25 '15 at 17:01
  • @trailmax Could you please explain your concerns with client-side hashing a little further? – bonh Jun 17 '15 at 19:12
  • 1
    @bonh See this explanation http://security.stackexchange.com/a/53606 – trailmax Jun 17 '15 at 19:14

4 Answers4

16

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.

The following JavaScript code correctly parses the stored salt and subkey, then verifies the given password by hashing it with the stored 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!");
}
Gojira
  • 2,941
  • 2
  • 21
  • 30
  • 5
    You sir have just saved my life. Thanks a lot. I'm migrating from ASP.NET to node.js and now I don't have to tell my users their password has expired! :D – javorosas Mar 29 '15 at 09:20
  • If you let me, I will post some keywords so someone like me can find this easier in the future: SimpleMembershipProviider hash algorythm ASP.NET MVC password hashing compare – javorosas Mar 29 '15 at 09:25
  • Sure thing... what do I need to do to help post the keywords? – Gojira Mar 29 '15 at 12:45
  • Oh, nothing, I just thought google can now crawl in here and relate this thread with those keywords. – javorosas Mar 29 '15 at 19:32
  • 2
    This also saved the day for my ASP.Net to Node project conversion as well! Thank you! –  Dec 22 '15 at 22:00
  • Actually, I see this is to compare a known hash to its source for logging in. I also need to be able to create and update passwords going forward. Have you figured out that part? Seems to be that the Rfc2898DeriveBytes method in C# is what generates a salt based on the source password, but it eventually calls a DLL so I'm not sure if it can be reproduced in Node. –  Dec 22 '15 at 22:25
  • 1
    I ended up writing this as a node module here: https://github.com/CmdrShepardsPie/JavaScript-Helpers/blob/master/node-password-hashing.js –  Jan 08 '16 at 23:16
  • 2
    @GojiraDeMonstah Thank you! Working on a Node app that needs to use existing .Net db for authentication purposes, this is exactly what I needed. I'd upvote twice if I could ;) – BFree May 18 '17 at 18:03
  • @GojiraDeMonstah Thank you sir greatly for saving my life :D – Nhím Hổ Báo Jun 22 '17 at 15:20
  • It does not completely work. if u use `source` as the password and `hash` as the hashed password, this function returns true while it should return `false`. – Complexity Apr 17 '18 at 17:48
2

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).

philwilks
  • 669
  • 5
  • 16
1

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.

Modify the function to catch that up and return false instead.

// 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!");
}
Complexity
  • 5,682
  • 6
  • 41
  • 84
1

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. 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;
Hedzer
  • 119
  • 6
  • oh! and if you're using AESManaged, the Microsoft docs say it's Rijndael 128, but in reality it's not. Use Node's crypto 'aes-256-cbc' for that. Hoping this spares someone the time/pain I went through. – Hedzer Dec 19 '18 at 14:44