c# - 使用 nodejs 生成 ASP.NET webpages_membership 密码

标签 c# php node.js

现有系统在 C# 上运行,但我们已决定将 c# 网站的一些模块移动到 nodejs 中,因此我也可以通过 c# 和 nodejs 登录。使用 c# 注册,它使用一些现有的 asp.net 库生成一些随 secret 码,它将密码存储到 "webpages_membership" 表中。

C# 生成的随 secret 码:“JWvppSSfnzOQ+uMd+BORpT/8aQorC8y05Bjbo/8w/9b/eiG4WLzUFRQSSiKZqo3C”是“123456”字符串的散列密码。

所以现在有一些其他模块现在将在 nodejs 中,但其余部分将仅在 c# 中。所以现在登录我必须通过 Node 登录。

我正在尝试使用以下方法在 nodejs 中比较 c# 生成的密码 图书馆 https://www.npmjs.com/package/aspnet-identity-pw

但它返回 False。

c# 为“123456”生成密码 => “JWvppSSfnzOQ+uMd+BORpT/8aQorC8y05Bjbo/8w/9b/eiG4WLzUFRQSSiKZqo3C”

请帮助我在 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

供引用的 PHP 代码:

<?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);

最佳答案

您可以使用内置的 crypto 模块将您的工作 PHP 代码移植到 Node.js。

创建哈希:

在您的 PHP MembershipModel::encript 方法中,您使用 PBKDF2 实现来创建 key 。我们可以使用 crypto.pbkdf2Sync 在 Node.js 中创建相同的 key 。

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

现在我们可以编写一个函数,它使用 kdf 并返回盐和 key ,base64 编码 - 与您的 PHP 和 C# 代码相同的格式。

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');
}

对于盐,我使用了 crypto.randomBytes,它是一个 CSPRNG 函数(创建安全的伪随机数据)。

检查哈希:

在您的 PHP MembershipModel::verifypassword 方法中,您使用接收到的盐通过 PBKDF2 创建 key ,然后将新 key 与接收到的 key 进行比较。 Node.js 等效项:

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);
}

我正在使用 crypto.timingSafeEqual 来比较键;它执行恒定的时间比较。

测试:

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

我们可以看到 hashPassword 函数生成与您的 PHP 代码兼容的哈希值,并且 verifyPassword 可以成功验证它们。


关于您的 PHP 代码的一些注释:

我假设 MembershipModel::bytearraysequal 方法应该使用恒定时间算法,但它会在第一次出现不相等字符时返回 false。更好的实现,使用按位运算符:

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);
}

此函数检查所有字符和字符串的长度。但是,最好使用内置的 hash_equals 函数(需要 PHP 5.6 或更高版本)。同样,您可以使用 openssl_pbkdf2 创建 key (PHP 5.5 或更高版本)。

我们可以使用这些函数和类型提示 (PHP 7) 改进您的 MembershipModel 类,它不需要空值检查,并生成更清晰的代码。

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;
    }
}

您的 key 派生方案似乎足够安全:具有随机盐和长 key 的 PBKDF2。您可以增加迭代次数以获得更好的安全性,但这会花费时间和性能。

然而,该实现可能存在错误(例如我在 MembershipModel::bytearraysequal 中发现的错误),这些错误可能会降低代码的安全性。如果可能,最好使用内置函数。


更新

在研究了aspnet-identity-pw的源代码后,我发现它内部使用了crypto。 key 由 crypto.pbkdf2 使用 16 字节盐和 1000 次迭代创建。唯一的区别是它创建了一个 49 字节的散列,前面有一个零字节。

散列格式是0 + salt[16] + key[32],所以如果我们切掉第一个字节就可以使用这个散列。例如:

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

此代码还会生成与您的 PHP 代码兼容的结果。就个人而言,我宁愿直接使用crypto,因为它更灵活,并且在比较哈希时也使用aspnet-identity-pw doesn't use a constant time algorithm。但我知道 aspnet-identity-pw 可能更容易使用,对于经验不足的用户来说可能更安全。

关于c# - 使用 nodejs 生成 ASP.NET webpages_membership 密码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52444530/

相关文章:

c# - 将 ObjectResult<int?> 转换为 List<int>

c# - 在 C# 中重新解释_cast

javascript - 将所有innerText从页面拉入json的最佳方法?

php - 单击按钮执行 MySQL 删除

php - 奇怪的变量/方法/类名 - PHP

javascript - 通过 TVJS-tvOS 使用 API JSon 调用

javascript - 仅在Express中针对某些路由运行中间件的模式

c# - TFS 内部版本 : the auto merge oprtion is not supported for the conflict on item

c# - 在 Kestrel .NET Core 中加载由中间 CA 签名的 SSL 证书的正确方法

php - 通过过程或面向对象风格调用函数的区别