您可以使用内置的
crypto
模块。
在PHP中
MembershipModel::encript
crypto.pbkdf2Sync
.
function kdf(password, salt, count=1000, keyLen=32, hash='sha1') {
return crypto.pbkdf2Sync(password, salt, count, keyLen, hash);
}
现在我们可以编写一个函数
kdf
返回salt和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函数(创建安全的伪随机数据)。
检查哈希:
MembershipModel::verifypassword
方法使用收到的salt用PBKDF2创建一个密钥,然后将新密钥与收到的密钥进行比较。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
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
函数(需要PHP5.6或更高版本)。同样,您可以使用
openssl_pbkdf2
创建密钥(PHP5.5或更高版本)。
我们可以改善你的生活
MembershipModel
使用这些函数初始化,并使用不需要空检查的类型提示(PHP7),生成更干净的代码。
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;
}
}
但是实现可能有bug(比如我在
成员身份模型::bytearraysequal
在研究了
aspnet-identity-pw
,我发现它使用
内部。密钥由创建
crypto.pbkdf2
16字节salt和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
加密
直接因为它更灵活,而且
aspnet标识pw
doesn't use a constant time algorithm
aspnet标识pw
可能更容易使用,而且对于经验不足的用户来说可能更安全。