1

As part of a project I'm working on, I need to support a number of SHA1 hashed & salted passwords created by ASP.NET's MembershipProvider. I need to rewrite this in Node.js and despite scouring the internet, I've been unable to replicate the process.

I've been through almost every available post here on StackOverflow along with other resources, including:

I've also spent considerable time digging through the ASP.NET source code to understand the hashing & keyed hashing algorithms, and my understanding is that the code below should be working.

What I'm trying to do is input a plaintext password along with a base64 salt and output the same base64 SHA1 hash that I have in the database. It should be simple. It should be the bytes of the salt concatenated with the bytes of the password and then hashed with SHA1 and digested to base64. Unfortunately, this doesn't seem to be the case.

For example, I have the following known password & hash:

const passwordPlaintext = 'this-is-the-password'
const passwordHash = 'oVj57RdHdLi1rGSx21qnVxj4ewc='
const passwordSalt = '5hz9hys/gdADzfgikgFLYg=='

I should be able to use the following to hash the salt & password:

function dotnet_membership_password_hash(pass, salt, encoding) {
  var bytes = Buffer.from(pass || '', encoding)
  var src = Buffer.from(salt || '', 'base64')
  var dst = Buffer.alloc(src.length + bytes.length)
  src.copy(dst, 0, 0, src.length)
  bytes.copy(dst, src.length, 0, bytes.length)
  return crypto.createHash('sha1').update(dst).digest('base64')
}

However, the outputs I'm getting don't match up:

// attempted with both utf8 and utf16le
dotnet_membership_password_hash(passwordPlaintext, passwordSalt, encoding)
oVj57RdHdLi1rGSx21qnVxj4ewc= (original)
pCsR7pToO3UiPNmSO8Xr6lqJo/M= (utf8) ❌
AWxwnQ7JDFavjE8jV1J5qkxvKvA= (utf16le) ❌

For what it's worth, I've also tried this with another known password set:

const passwordPlaintext = 'password123'
const passwordHash = 'twEXsCXaU7YnpXyw0vMUjwmVa8E='
const passwordSalt = '0z9kTO9ksSpd4VUCAH9FVw=='

But again, the output isn't as expected:

twEXsCXaU7YnpXyw0vMUjwmVa8E= (original)
gbUyM/OwMzOCAKRZo42c9Vb8vrA= (utf8) ❌
x5yUPGlqJYOeKuwq4miU7Fwbk+8= (utf16le) ❌

In case it has an effect, there is a validation key in the web.config, e.g.

<machineKey
  validationKey="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  decryptionKey="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  decryption="3DES"
  validation="SHA1" />

I've tried to incorporate this along with SHA1 into a hmachash function I found on one of the other posts (originally written for sha256) but no luck (didn't expect much):

const dotnet_membership_password_hmachash = function (pass, salt, vKey, encoding) {
  var key = null
  var bIn = Buffer.from(pass, encoding)
  var bSalt = Buffer.from(salt, 'base64')
  if (vKey.length == bSalt.length) {
    key = bSalt
  } else if (vKey.length < bSalt.length) {
    var bKey = new Buffer(vKey.length)
    bSalt.copy(bKey, 0, 0, bKey.Length)
    key = bKey

  } else {
    var bKey = new Buffer(vKey.length)
    for (var iter = 0; iter < bKey.length;) {
      var len = Math.min(bSalt.length, bKey.length - iter)
      bSalt.copy(bKey, iter, 0, len)
      iter += len
    }
    key = bKey
  }
  return crypto.createHmac('sha1', key).update(bIn).digest('base64')
}

I'm wondering if there's something obvious I'm missing here, an elementary mistake, and it seems like something quite self-contained that some folks might enjoy having a go at. ChatGPT definitely can't solve it so it's down to good old fashioned human programmers!

Looking forward to hearing back.


FWIW, I've also tried hashing the known password in C# which according to the first linked answer above, should work:

using System.IO;
using System;
using System.Text;
using System.Security.Cryptography;

class Program
{

    static string EncodePassword(string pass, string salt)
    {
        byte[] bytes = Encoding.Unicode.GetBytes(pass);
        byte[] src = Convert.FromBase64String(salt);
        byte[] dst = new byte[src.Length + bytes.Length];
        Buffer.BlockCopy(src, 0, dst, 0, src.Length);
        Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);
        HashAlgorithm algorithm = HashAlgorithm.Create("SHA1");
        byte[] inArray = algorithm.ComputeHash(dst);
        return Convert.ToBase64String(inArray);
    }

    static void Main()
    {
        string pass = "this-is-the-password";
        string salt = "5hz9hys/gdADzfgikgFLYg==";
        string hash = Program.EncodePassword(pass,salt);
        Console.WriteLine(hash);
        // outputs AWxwnQ7JDFavjE8jV1J5qkxvKvA
    }
}

But this outputs AWxwnQ7JDFavjE8jV1J5qkxvKvA which is the same as my Node.js utf16le method.

4
  • FWIW, an answer doesn't have to be JavaScript, so long as it works and shows the method I can work with that! All answers and attempts welcome. Commented Jul 25, 2024 at 14:25
  • 1
    There seem to be two distinct methods mentioned in those links, a older, less secure method using SHA1 that you're trying to emulate, and a (presumably newer) more secure method using pbkdf2. The two methods are not compatible. Commented Jul 26, 2024 at 1:01
  • @PresidentJamesK.Polk looking at the machineKey config which states SHA1 with the 160 bit SHA1 hashes, I'm not sure pbkdf2 is at play? But then I don't know enough about the SqlMembershipProvider to know what other configuration options it looks at from web.config that may effect how it hashes the passwords. Any ideas? Commented Jul 26, 2024 at 6:19
  • Try to see if you can debug into .NET Framework source code to evaluate important runtime data, and then compare against those you get from your own Node.js code. That should reveal at which stage your code is wrong and should be fixed. Commented Aug 12, 2024 at 15:49

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.