现有系统在 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/