DEV Community

loading...

Hash your passwords with scrypt using Nodejs crypto module

farnabaz profile image Ahad Birang ・2 min read

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.

Discussion (7)

Collapse
bdougherty profile image
Brad Dougherty

It's probably best to use crypto.timingSafeEqual(a, b) to compare the keys in the verify function to protect against timing attacks.

Collapse
yashraj021 profile image
Yash

Hey Brad, I am new to authentication, can please explain gour point?

Collapse
bdougherty profile image
Brad Dougherty

The Wikipedia article explains it pretty well: en.wikipedia.org/wiki/Timing_attack

In other words, the verify function should look something like this:

import crypto from 'crypto';
import { promisify } from 'util';

const scrypt = promisify(crypto.scrypt);

async function verify(password, hash) {
    const [salt, key] = hash.split(":")
    const keyBuffer = Buffer.from(key, 'hex')
    const derivedKey = await scrypt(password, salt, 64)
    return crypto.timingSafeEqual(keyBuffer, derivedKey)
}
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
gaddmaster profile image
Collapse
_ma4m profile image
Imam T

Cool, Thanks man !

Collapse
unibreakfast profile image
Mykhailo "Ninin" Velykoselskyi

Thanks, much appreciated!

Collapse
monochromer profile image
monochromer

no need to write async for functions

Forem Open with the Forem app