šŸ” How to Handle Password Hashing in Node.js

Taranjit Singh
The Startup
Published in
5 min read6 days ago
Photo by Towfiqu barbhuiya on Unsplash

If youā€™re building a Node.js application that handles passwords, itā€™s important to know how to deal with password hashing. In this post, weā€™ll explore the usage of the node:crypto module to hash and verify passwords, with a focus on secure practices.

Introduction to node:crypto

Node.js provides the node:crypto module, a powerful tool for encryption, hashing, and securing data. This module includes functions for hashing passwords using established algorithms like PBKDF2, scrypt, bcrypt, and more. Proper use of crypto ensures that passwords are securely hashed, making it significantly harder for attackers to recover user credentials in the event of a data breach.

In this post, weā€™ll focus on using PBKDF2 and scrypt, two robust hashing algorithms that are widely recommended.

How does hashing work

Hashing is a one-way function that converts data into a fixed-length value. When implemented correctly, hashing ensures that even if an attacker gets access to your database, they wonā€™t be able to easily recover the original passwords.

To make hashing secure, we need 3 things that weā€™ll further explore later:

  1. Use unique salts for each password to prevent attacks using precomputed hashes (called rainbow tables).
  2. Adaptive costs: configure parameters that allow to increase the computational cost over time to make attacks impractical.
  3. Use algorithms that are resilient against brute-force and parallelized attacks.

Using PBKDF2 to Hash Passwords

PBKDF2 (Password-Based Key Derivation Function 2) is a widely-used algorithm for hashing passwords. It uses a combination of key stretching (multiple iterations) and salting to secure passwords.

Hereā€™s how to hash a password using PBKDF2 with node:crypto:

const { pbkdf2Sync, randomBytes } = require('node:crypto');

function hashPassword(password) {
const salt = randomBytes(16).toString('hex');
const iterations = 100000;
const keylen = 64;
const digest = 'sha512';

const hash = pbkdf2Sync(password, salt, iterations, keylen, digest).toString('hex');
return `${salt}:${iterations}:${hash}`;
}

console.log(hashPassword('my_secure_password'));

pbkdf2Sync() is a function that generates a hashed password using a specified number of iterations.
It uses a unique salt that we generate for each password using randomBytes(). Itā€™s a random string that ensures that each password hash is unique, even if the passwords are identical.
We use 100,000 iterations, which adds computational cost to make brute-force attacks more difficult. This is the minimum value to slow down attackers while still being usable for legitimate users. The higher the iterations count, the longer it takes to compute the hash. It should take at least 100ms to compute the hash, balancing usability and security.

As hardware continues to improve, you can increase the iteration count because of the increasing computational power.

Using Scrypt for Better Security

Scrypt is a powerful choice for password hashing and is often preferred over PBKDF2 because of its memory-hard nature. Memory-hard algorithms are resistant to attacks using specialized hardware like GPUs because they require a lot of memory, which is expensive to parallelize.

Hereā€™s how to hash a password using scrypt:

const { scryptSync, randomBytes } = require('node:crypto');

function hashPasswordScrypt(password) {
const salt = randomBytes(16).toString('hex');
const keylen = 64;
const hash = scryptSync(password, salt, keylen).toString('hex');
return `${salt}:${hash}`;
}

console.log(hashPasswordScrypt('my_secure_password'));

PBKDF2 vs Scrypt: Which One Should You Use?

Both PBKDF2 and scrypt are good options for password hashing, but each has distinct use cases and benefits.

PBKDF2 is NIST-approved (National Institute of Standards and Technology) and has widespread support across different libraries and platforms. You may want to use it in case you need to adhere to high security standards, especially in regulated industries like healthcare or finance.

Scrypt is a memory-hard algorithm, meaning it requires significant memory in addition to CPU power. This makes it more resistant to attacks using specialized hardware like GPUs. It is ideal for scenarios where security is more important than performance, such as securing highly sensitive user information.

Performance and Resource Considerations

The node:crypto module also offers the scrypt and the pbkdf2 functions that are asynchronous and take a callback function as last argument to call when they are done, this avoids to block the event loop, which could degrade the responsiveness of your application.

Using scrypt with async/await to avoid blocking the event loop

To use scrypt without blocking the event loop, you can easily leverage async/await with the asynchronous scrypt function. This allows your code to wait for the hashing to complete without blocking the entire event loop, meaning other requests can be processed concurrently.

Hereā€™s an updated version using scrypt with async/await:

const { scrypt, randomBytes } = require('node:crypto');

async function hashPasswordScryptAsync(password) {
const salt = randomBytes(16).toString('hex');
const keylen = 64;
return new Promise((resolve, reject) => {
scrypt(password, salt, keylen, (err, derivedKey) => {
if (err) reject(err);
resolve(`${salt}:${derivedKey.toString('hex')}`);
});
});
}

async function route(){
const hashedPassword = await hashPasswordScryptAsync('my_secure_password');
console.log('Hashed Password:', hashedPassword);
};

route();

The scrypt function does not return a Promise, but we can wrap it in one so we can use await to handle the result without blocking the event loop.
This approach ensures that while the hashing operation is running, the event loop is free to handle other requests.

Verifying Passwords Securely

To verify a password, you must hash the provided password with the same salt and compare it to the stored hash. Hereā€™s a function to do that with PBKDF2:

function checkPassword(password, savedPassword) {
const [salt, iterations, key] = savedPassword.split(':');
const hash = pbkdf2Sync(password, salt, parseInt(iterations), 64, 'sha512').toString('hex');
return hash === key;
}

How to avoid Timing Attacks

The direct comparison (hash === key) can be vulnerable to timing attacks, where attackers can measure the time it takes to return different responses. To mitigate this, use crypto.timingSafeEqual() for comparison:

const { timingSafeEqual } = require('node:crypto');

function checkPasswordSafe(password, savedPassword) {
const [salt, iterations, key] = savedPassword.split(':');
const hash = pbkdf2Sync(password, salt, parseInt(iterations), 64, 'sha512');
const keyBuffer = Buffer.from(key, 'hex');
return timingSafeEqual(hash, keyBuffer);
}

With scrypt it would be:

const { timingSafeEqual } = require('node:crypto');

function checkPasswordSafe(password, savedPassword) {
const [salt, key] = savedPassword.split(':');
const keylen = 64;
const hash = scryptSync(password, salt, keylen);
const keyBuffer = Buffer.from(key, 'hex');
return timingSafeEqual(hash, keyBuffer);
}

We can do the same with their equivalent asynchronous functions.

Conclusion

The node:crypto module provides powerful tools to help you securely hash and verify passwords. Choose an algorithm that works best for your use case and make sure to update the parameters as technology evolves.

--

--

The Startup
The Startup

Published in The Startup

Get smarter at building your thing. Follow to join The Startupā€™s +8 million monthly readers & +772K followers.

Taranjit Singh
Taranjit Singh

Written by Taranjit Singh

Graduated in computer science | working as fullstack developer