简体   繁体   中英

Generate ASP.NET webpages_membership Password using nodejs

There is existing system is working on C#, but we have decided to move some modules of c# website into nodejs, so i will be able to login through c# and nodejs too. using c# registration it generated some random password using some existing library of asp.net it will store password into "webpages_membership" table.

C# generated random password : "JWvppSSfnzOQ+uMd+BORpT/8aQorC8y05Bjbo/8w/9b/eiG4WLzUFRQSSiKZqo3C" is hashed password for "123456" string.

so now there is some other module which will be now in nodejs but rest of the things will be in c# only. so login now i have to login through node.

I am trying to compare c# generated password in nodejs using following library https://www.npmjs.com/package/aspnet-identity-pw

but it returns False.

c# generated password for "123456" => "JWvppSSfnzOQ+uMd+BORpT/8aQorC8y05Bjbo/8w/9b/eiG4WLzUFRQSSiKZqo3C"

Please help me to achieve same in nodejs.

nodejscode

var passwordHasher = require('aspnet-identity-pw');

var hashedPassword = passwordHasher.hashPassword('123456');
console.log(hashedPassword);

var isValid = passwordHasher.validatePassword('JWvppSSfnzOQ+uMd+BORpT/8aQorC8y05Bjbo/8w/9b/eiG4WLzUFRQSSiKZqo3C', hashedPassword);
console.log("Result:"+isValid);
//Return False

I tried same thing in php using below code which is working fine, using below php code i am able to compare c# generated password and also able to generate new password from php and able to login from C#.

Working PHPcode for reference:

<?php

/*
 * Author  : Mr. Juned Ansari
 * Date    : 15/02/2017 
 * Purpose : It Handles Login Encryption And Decryption Related Activities
 */

class MembershipModel {

    function bytearraysequal($source, $target) {
        if ($source == null || $target == null || (strlen($source) != strlen($target)))
            return false;
        for ($ctr = 0; $ctr < strlen($target); $ctr++) {
            if ($target[$ctr] != $source[$ctr])
                return false;
        }
        return true;
    }
    //This Function is Used to verifypassword
    function verifypassword($hashedPassword, $password) {

        $PBKDF2IterCount = 1000; // default for Rfc2898DeriveBytes
        $PBKDF2SubkeyLength = 32; // 256 bits       
        $SaltSize = 16; // 128 bits


        if ($hashedPassword == null) {
            return false;
            //show_error("hashedPassword is null");
        }
        if ($password == null) {
            return false;
            //show_error("Password is null");
        }

        $hashedPasswordBytes = base64_decode($hashedPassword);

        if (strlen($hashedPasswordBytes) != 48) {
            return false;
        }

        $salt = substr($hashedPasswordBytes, 0, $SaltSize);

        $storedSubkey = substr($hashedPasswordBytes, $SaltSize, $PBKDF2SubkeyLength);

        $generatedSubkey = $this->encript('sha1', $password, $salt, $PBKDF2IterCount, $PBKDF2SubkeyLength, true);

        return $this->bytearraysequal($storedSubkey, $generatedSubkey);
    }

    function encript($algorithm, $password, $salt, $count, $key_length, $raw_output = false) {
        $algorithm = strtolower($algorithm);
        if (!in_array($algorithm, hash_algos(), true))
            return false;
        //show_error('PBKDF2 ERROR: Invalid hash algorithm.');
        if ($count <= 0 || $key_length <= 0)
            return false;
        //show_error('PBKDF2 ERROR: Invalid parameters.');

        $hash_length = strlen(hash($algorithm, "", true));
        $block_count = ceil($key_length / $hash_length);

        $output = "";
        for ($i = 1; $i <= $block_count; $i++) {

            $last = $salt . pack("N", $i);

            $last = $xorsum = hash_hmac($algorithm, $last, $password, true);

            for ($j = 1; $j < $count; $j++) {
                $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
            }
            $output .= $xorsum;
        }
        return substr($output, 0, $key_length);
    }

}
---------------------------------
echo MembershipModel::verifypassword("JWvppSSfnzOQ+uMd+BORpT/8aQorC8y05Bjbo/8w/9b/eiG4WLzUFRQSSiKZqo3C","123456");
//Returns True for every c# generated password

$salt = openssl_random_pseudo_bytes(16);
$dev = MembershipModel::encript('sha1', $Password, $salt, 1000, 32, true);
$HashedPassword = base64_encode($salt.$dev);

You can port your working PHP code to Node.js using the built-in crypto module.

Creating a hash:

In your PHP MembershipModel::encript method you're using a PBKDF2 implementation to create a key. We can create the same key in Node.js with crypto.pbkdf2Sync .

function kdf(password, salt, count=1000, keyLen=32, hash='sha1') {
    return crypto.pbkdf2Sync(password, salt, count, keyLen, hash);
}

Now we can write a function that uses kdf and returns the salt and key, base64 encoded - the same format as your PHP and C# code.

function hashPassword(password) {
    var salt = crypto.randomBytes(16);
    var key = kdf(password, salt, 1000, 32, 'sha1');
    var sk = Buffer.concat([salt, key]);
    return sk.toString('base64');
}

For the salt I've used crypto.randomBytes which is a CSPRNG function (creates secure pseudo-random data).

Checking a hash:

In your PHP MembershipModel::verifypassword method you're using the received salt to craete a key with PBKDF2, then compare the new key with the received key. A Node.js equivalent:

function verifyPassword(hashedPassword, password) {
    var data = new Buffer(hashedPassword, 'base64');
    var salt = data.slice(0, 16);
    var key = data.slice(16);
    var hash = kdf(password, salt);
    return crypto.timingSafeEqual(key, hash);
}

I'm using crypto.timingSafeEqual to compare the keys; it performs constant time comparison.

Testing:

var password = "123456";
var hash = hashPassword(password);
console.log(hash);
//umzeh4aAeD1Ee6z4oN/BS9f2s2GQ7gswtbrguEr2C32c8XK99UjI8LkgYapbX8/N
console.log(verifyPassword(hash, password));
//true

hash = "JWvppSSfnzOQ+uMd+BORpT/8aQorC8y05Bjbo/8w/9b/eiG4WLzUFRQSSiKZqo3C";
console.log(verifyPassword(hash, password));
//true

We can see that the hashPassword function produces hashes that are compatible with your PHP code and verifyPassword can verify them successfully.


Some notes about your PHP code:

I assume that the MembershipModel::bytearraysequal method is supposed to use a constant time algorithm, but it returns false with the first occurrence of unequal characters. A better implementation, using bitwise operators:

function bytearraysEqual(string $hash1, string $hash2): bool {
    $result = 0;
    for ($i=0; $i<strlen($hash1) && $i<strlen($hash2); $i++) {
        $result |= ord($hash1[$i]) ^ ord($hash2[$i]);
    }
    return $result === 0 && strlen($hash1) === strlen($hash2);
}

This function checks all the characters, and the length of strings. However, it's best to use the built-in hash_equals function (requires PHP 5.6 or higher). Similarly, you can use openssl_pbkdf2 to create the key (PHP 5.5 or higher).

We can improve your MembershipModel class using those functions, and type hinting (PHP 7) which doesn't require null checks, and produces cleaner code.

class MembershipModel {

    const PBKDF2_ALGORITHM = "SHA1"; 
    const PBKDF2_ITERATIONS = 1000; 
    const KEY_LENGTH = 32;    
    const SALT_LENGTH = 16;

    function hashPassword(string $password): string {
        $salt = openssl_random_pseudo_bytes(16);
        $key = MembershipModel::kdf($password, $salt);
        return base64_encode($salt.$key);
    }

    function verifyPassword(string $hashedPassword, string $password): bool {
        $hashedPasswordBytes = base64_decode($hashedPassword);
        $salt = substr($hashedPasswordBytes, 0, MembershipModel::SALT_LENGTH);
        $key1 = substr($hashedPasswordBytes, MembershipModel::SALT_LENGTH);
        $key2 = MembershipModel::kdf($password, $salt);
        return hash_equals($key1, $key2);
    }

    private function kdf(string $password, string $salt): string {
        $key = hash_pbkdf2(
            MembershipModel::PBKDF2_ALGORITHM, $password, $salt, 
            MembershipModel::PBKDF2_ITERATIONS, MembershipModel::KEY_LENGTH,
            true
        );
        return $key;
    }
}

Your key derivation scheme seems secure enough: PBKDF2 with a random salt and long key. You could increase the number of iterations for better security, but that would cost time and performance.

The implementation however may have bugs (like the one I found in MembershipModel::bytearraysequal ) that could reduce the security of your code. It's best to use built-in functions, if that is possible.


Update

After studying the source code of aspnet-identity-pw , I discovered that it uses crypto internally. The key is created by crypto.pbkdf2 with 16 byte salt and 1000 iterations. The only difference is that it creates a 49 byte hash, with a zero byte at the front.

The hash format is 0 + salt[16] + key[32] , so could use this hash if we slice off the first byte. For example:

const passwordHasher = require('aspnet-identity-pw');

function hashPassword(password) {
    var hash = passwordHasher.hashPassword(password);
    var bytes = Buffer(hash, 'base64');
    return bytes.slice(1).toString('base64');
}

function verifyPassword(hashedPassword, password) {
    var bytes = new Buffer(hashedPassword, 'base64');
    var hash = Buffer.concat([new Buffer([0x00]), bytes]).toString('base64');
    return passwordHasher.validatePassword(password, hash);
}

var hash = "JWvppSSfnzOQ+uMd+BORpT/8aQorC8y05Bjbo/8w/9b/eiG4WLzUFRQSSiKZqo3C";
console.log(verifyPassword(hash, '123456'))
//true

This code also produces results that are compatible with your PHP code. Personally, I would rather use crypto directly because it is more flexible, and also aspnet-identity-pw doesn't use a constant time algorithm when comparing hashes. But I understand that aspnet-identity-pw may be easier to use, and it may be safer for less experienced users.

According to the documentation you should send in the password as the first parameter in validatePassword. So try this:

 var passwordHasher = require('aspnet-identity-pw'); var hashedPassword = passwordHasher.hashPassword('123456'); console.log(hashedPassword); var isValid = passwordHasher.validatePassword('123456', hashedPassword); console.log("Result:"+isValid); 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM