DEV Community

Ahad Birang
Ahad Birang

Posted on

Hash your passwords with scrypt using Nodejs crypto module

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)

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
 
inalbant profile image
Ibrahim • Edited

This is how I did it it TypeScript with the help of your code and @bdougherty ;

import { scrypt, randomBytes, timingSafeEqual } from 'crypto';
import { promisify } from 'util';

const scryptPromise = promisify(scrypt);

async function hash(password: string) {
  const salt = randomBytes(8).toString('hex');
  const derivedKey = await scryptPromise(password, salt, 64);
  return salt + ':' + (derivedKey as Buffer).toString('hex');
}

async function verify(password: string, hash: string) {
  const [salt, key] = hash.split(':');
  const keyBuffer = Buffer.from(key, 'hex');
  const derivedKey = await scryptPromise(password, salt, 64);
  return timingSafeEqual(keyBuffer, derivedKey as Buffer);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
advename profile image
Lars

The comments highlight some security flaw in the authors implementation, such as:

  • you should compare the hashed values with timingSafeEqual, which accepts Buffer only and thereby preventing timing attack
  • NodeJS Docs recommend using 16 bytes salts, and not 8 bytes

Here's a complete version, without util.promisify as that approach may lead to Maximum callstack issues.

import { scrypt, randomBytes, timingSafeEqual } from "crypto";

const keyLength = 32;
/**
 * Has a password or a secret with a password hashing algorithm (scrypt)
 * @param {string} password
 * @returns {string} The salt+hash
 */
export const hash = async (password) => {
    return new Promise((resolve, reject) => {
        // generate random 16 bytes long salt - recommended by NodeJS Docs
        const salt = randomBytes(16).toString("hex");

        scrypt(password, salt, keyLength, (err, derivedKey) => {
            if (err) reject(err);
            // derivedKey is of type Buffer
            resolve(`${salt}.${derivedKey.toString("hex")}`);
        });
    });
};

/**
 * Compare a plain text password with a salt+hash password
 * @param {string} password The plain text password
 * @param {string} hash The hash+salt to check against
 * @returns {boolean}
 */
export const compare = async (password, hash) => {
    return new Promise((resolve, reject) => {
        const [salt, hashKey] = hash.split(".");
        // we need to pass buffer values to timingSafeEqual
        const hashKeyBuff = Buffer.from(hashKey, "hex");
        scrypt(password, salt, keyLength, (err, derivedKey) => {
            if (err) reject(err);
            // compare the new supplied password with the hashed password using timeSafeEqual
            resolve(timingSafeEqual(hashKeyBuff, derivedKey));
        });
    });
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
wh1t3h47 profile image
Antรดnio Martos Harres

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

Collapse
 
gaddmaster profile image
Daniel Gadd

Thanks !

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