From Nodejs v10, crypto module has a built-in implementation of scrypt algorithm that can be used as a password hashing algorithm. To the best of my knowledge, the state-of-art algorithm to hash and store passwords in Nodejs is bcrypt. bcrypt is a very popular module in NPM with nearly half a million downloads per week. I'm not a security expert to tell which one is better, but if you want to use Scrypt as another powerful hash algorithm, its simple:
Hash Password
Scrypt is a salted hashing algorithm. In order to hash passwords using Scrypt you need to create a unique salt on every hash.
The salt should be as unique as possible. It is recommended that a salt is random and at least 16 bytes long. See NIST SP 800-132 for
const crypto = require("crypto")
async function hash(password) {
return new Promise((resolve, reject) => {
// generate random 16 bytes long salt
const salt = crypto.randomBytes(16).toString("hex")
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
if (err) reject(err);
resolve(salt + ":" + derivedKey.toString('hex'))
});
})
}
It is important to save generated salt with your hash, because without the salt there is no way to verify the password, and yes, you can store the salt in plaintex.
Check Password
As I said before we need salt to verify password. The salt can be extraced from result of hash
function.
async function verify(password, hash) {
return new Promise((resolve, reject) => {
const [salt, key] = hash.split(":")
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
if (err) reject(err);
resolve(key == derivedKey.toString('hex'))
});
})
}
Putting all together
const crypto = require("crypto")
async function hash(password) {
return new Promise((resolve, reject) => {
const salt = crypto.randomBytes(8).toString("hex")
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
if (err) reject(err);
resolve(salt + ":" + derivedKey.toString('hex'))
});
})
}
async function verify(password, hash) {
return new Promise((resolve, reject) => {
const [salt, key] = hash.split(":")
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
if (err) reject(err);
resolve(key == derivedKey.toString('hex'))
});
})
}
(async function run () {
const password1 = await hash("123456")
const password2 = await hash("123456")
console.log("password1", await verify("123456", password1));
console.log("password2", await verify("123456", password2));
console.log("password1 == password2", password1 == password2);
})()
And here is the result:
password1 true
password2 true
password1 == password2 false
As you can see from the result, hashing single string with different salts results different outputs, but they both can verified.
Top comments (10)
It's probably best to use
crypto.timingSafeEqual(a, b)
to compare the keys in theverify
function to protect against timing attacks.Hey Brad, I am new to authentication, can please explain gour point?
The Wikipedia article explains it pretty well: en.wikipedia.org/wiki/Timing_attack
In other words, the verify function should look something like this:
The key has to be converted into a buffer because
crypto.timingSafeEqual
only accepts buffers for the arguments.Doing it this way means that the comparison operation takes the same amount of time every single time.
This is how I did it it TypeScript with the help of your code and @bdougherty ;
The comments highlight some security flaw in the authors implementation, such as:
timingSafeEqual
, which accepts Buffer only and thereby preventing timing attackHere's a complete version, without
util.promisify
as that approach may lead to Maximum callstack issues.This == check may be dangerous. There are some vulnerabilities about type confusion, I'm not sure if it affects node, but surely would affect PHP, whereas a hash starting with 0e would be interpreted as an integer
Thanks !
Cool, Thanks man !
Thanks, much appreciated!
no need to write
async
for functions