上一篇文章 nodejs 中的 bcrypt (1) : bcrypt 的特点与应用 通过一个 web 用户注册登录的案例,简述了 bcrypt 的特点、nodejs 中的 bcrypt 包,并附上了具体代码。
代码跑通了,但一些疑惑没有解决,比如:
- 加盐哈希后得到的一长串字符串包含什么信息?
- 对同一个密码,每次加盐哈希的得到的字符串都不一致,那么比对密码的过程是如何进行的?
- 对比密码使用的 bcrypt.compare() 方法,其参数里并没有 salt 。是这个过程里盐值没用,还是它深藏别处?
这篇来进行解答。
第一个问题
加盐哈希后得到的一长串字符串包含什么信息?
运行以下这段代码三次
testHash.js
const bcrypt = require('bcrypt-nodejs')
const SALT_FACTOR = 10
bcrypt.genSalt(12, function (err, salt) {
if (err) return next(err)
bcrypt.hash('mypassword', salt, null, function (err, hash) {
if (err) return next(err)
console.log(hash);
})
})
分别得到三个60位的字符串
$2a$10$dsZFPb.m2b6cXuw.fQAjneRk7u33cF9ZrWqywX4j5K5ymlhLFwRMS
$2a$10$mWm.uX4tSUKqa4Rk1/DQRuqm3zd.S5EaaWiIEQVrq8YxiebiqkMwy
$2a$10$oMpGJ3NhLD7OtX8lUCq6bOYr04SAgq00J2yLf/TsHZMulnmO9yJGi
容易发现,前7位都是相同的 $2a$10$
将参数 SALT_FACTOR
的值改为 12 ,再运行三次
$2a$12$5XhpRItT.wGNyVJk67QMPOchZQsHwBprwNBLEOot8cJOryihRa74W
$2a$12$0DK/6Q48hUkqq0im/9QyHee4Y/vrbchzmuWNHvapRr6A6aGGrdBxS
$2a$12$JUVxAjk5/ZStnxHgvK2IDetlWfD1jCpT69/cgkgA0vV49SmwS3RfG
这三个字符串的前七位也是相同的: $2a$12$
与之前的三个字符串相比,第 5 第 6 位的值从 10
,变成了 12
。与各自的参数 SALT_FACTOR
一致。
这个参数就是 bcrypt cost parameter ,它决定了迭代的次数,即哈希计算的“缓慢程度”(见上一篇)。
前 4 位的 $2a$
则指示了算法的版本。老版本有 $2$
,更新的版本有 $2x$
$2y$
$2b$
。
字符串的后半段,前 22 位是 salt 盐值,后 31 位就是对应密码的哈希部分。
总结一下
$2a$10$i5btSOiulHhaPHPbgNUGdObga/GC.AVG/y5HHY1ra7L0C9dpCaw8u
2a
指示算法版本10
是 cost 参数i5btSOiulHhaPHPbgNUGdO
是 salt- 最后的31位
bga/GC.AVG/y5HHY1ra7L0C9dpCaw8u
对应密码
这也回答了第一个问题。
后两个问题
比对密码的过程是如何进行的?
查看 bcrypt-nodejs 的源码,其中的 compareSync
函数:
function compareSync(data, encrypted) {
/*
data - [REQUIRED] - data to compare.
encrypted - [REQUIRED] - data to be compared to.
*/
if(typeof data != "string" || typeof encrypted != "string") {
throw "Incorrect arguments";
}
var encrypted_length = encrypted.length;
if(encrypted_length != 60) {
return false;
}
var same = true;
var hash_data = hashSync(data, encrypted.substr(0, encrypted_length-31));
var hash_data_length = hash_data.length;
same = hash_data_length == encrypted_length;
var max_length = (hash_data_length < encrypted_length) ? hash_data_length : encrypted_length;
// to prevent timing attacks, should check entire string
// don't exit after found to be false
for (var i = 0; i < max_length; ++i) {
if (hash_data_length >= i && encrypted_length >= i && hash_data[i] != encrypted[i]) {
same = false;
}
}
return same;
}
函数的两个参数中,data
相当于密码明文,encrypted
相当于从数据库里读出的哈希字符串。
结合第一个问题中提到的知识,哈希值中 substr(0, encrypted_length-31)
这一段是salt。
所以,匹配的思路是:从哈希值字符串中提取出salt,将其添加到密码明文中,再进行一次加盐哈希运算。比对 得到的结果 和 旧有的哈希字符串,由此判断密码是否匹配。
按照这个思路,写一个简化的例子:
解决第一个问题的时候,我们对 'mypassword'
进行了6次加密。任取一个字符串,赋值给 hash
testCompare.js
const bcrypt = require('bcrypt-nodejs')
const hash = '$2a$12$5XhpRItT.wGNyVJk67QMPOchZQsHwBprwNBLEOot8cJOryihRa74W'
const saltInHash = hash.substr(0, hash.length-31)
const newHash = bcrypt.hashSync('mypassword', saltInHash)
console.log('TEST: ' + (newHash === hash))
运行结果:
TEST: true
密码匹配成功!
总结一下
bcrypt
直接将 salt 保存在里加密计算得到的哈希字符串里。- 匹配思路是
- 从哈希值字符串中提取出salt,将其添加到密码明文中,再进行一次加盐哈希运算。
- 比对 得到的结果 和 旧有的哈希字符串,由此判断密码是否匹配。
后两个问题也得到了解答。