How to Store Passwords Securely in NodeJS - Part 1

Learn how to use built-in tools in NodeJS to store user passwords securely.

How to Store Passwords Securely in NodeJS - Part 1
Photo by Towfiqu barbhuiya / Unsplash

Passwords need to be stored in a way that prevents evil people from using them to access a user's resources in your app.

To do that, you use an algorithm that will encode the password that the user provides, and then store it in your database.

When the user needs to login the next time, you'll follow these steps:

  1. Encode the password again.
  2. Compare the result with the one you stored previously.
  3. Log the user in if they're the same.

Choosing an algorithm

The algorithm needs to encode the password in such a way that no one will be able to derive the original password even if they have access to the encoded form.

Usually, attackers will steal your database, and then spend a lot of money to acquire computers that they will use to try to guess the passwords from the encoded ones you stored.

Your job is to use an algorithm that results in such a random encoded password that the attacker will never have enough computers to successfully guess the original password.

So, which algorithms should you choose?

Currently, the following algorithms are recommended by security experts:

  1. Argon2
  2. scrypt
  3. bcrypt
  4. PBKDF2

All of them are known as password based key derivation functions. Meaning, you give them a password, and they give you a key in return. The key will be completely random, and it will be hard (mostly likely impossible) to guess the password that was used to generate it.

In this tutorial, we'll use scrypt because it's available in NodeJS by default as part of the crypto module.

It's also a strong algorithm that has never been compromised.

Using scrypt

Scrypt is configurable using various parameters that will determine how hard the resulting key is to crack.

If you set strong parameters, the key will take longer to crack, but also longer to generate from your side. That's why you need the right balance of security, and resource usage.

You don't want to configure it in a way that uses too many resources on your server because then, authenticating with your app will be slow, resulting in a poor user experience.

The parameters are:

  • Cost in terms of CPU/memory usage. This must be a number that's a power of 2.
  • Block size will determine how much data the function will be able to process so that you can allow users to have long enough passwords.
  • Parallelization that will limit the amount of parallelism the attacker can use to try to guess the password from the key.

We'll use the current guidelines of a cost parameter of 2^17, block size of 8 (1024 bytes of data), and parallelization of 1.

Generating a salt

If two users have the same password of '0pen$esame', the algorithm will return a key that looks similar. An attacker will then be able to know that these two passwords are the same. Now, they will spend all their resources on cracking a single one of them, which will reduce the cost required to crack your database.

That's why the algorithms will also take a salt along with the original password. This salt will be a random string that will be combined with the original password resulting in a random value that will be fed to the algorithm. This ensures that the resulting key is always random.

Use the randomBytes function from the crypto module:

const { randomBytes } = require("node:crypto");

function generateSalt(size = 16) {
  return new Promise((resolve, reject) => {
    randomBytes(size, (err, buf) => {
      if (err) {
        reject(err);
      } else {
        resolve(buf.toString("hex"));
      }
    });
  });
}

This function generates a salt of length 16 bytes by default and returns it as a string in hex format.

A longer salt is more secure but 16 bytes is considered long enough by today's standards.

Encoding the password

Now that you have the salt, use it to derive the password as a secret key.

const { scrypt } = require("node:crypto");

const cost = 131072;
const blockSize = 8;
const parallelization = 1;

function generateHashKey(
  salt,
  plainText,
  options = { cost, blockSize, parallelization },
) {
  return new Promise((resolve, reject) => {
    scrypt(
      plainText,
      salt,
      64,
      {
        cost: options.cost,
        blockSize: options.blockSize,
        parallelization: options.parallelization,
        maxmem: 256 * options.cost * options.blockSize,
      },
      (err, derivedKey) => {
        if (err) {
          reject(err);
        } else {
          resolve(derivedKey);
        }
      },
    );
  });
}


async function encodePassword(plainText) {
  const salt = await generateSalt();
  const hashKey = await generateHashKey(salt, plainText);
  const b64Hash = hashKey.toString("base64");
  return `scrypt$${cost}$${salt}$${blockSize}$${parallelization}$${b64Hash}`;
}

This function will do the following:

  1. Run the salt and password through the algorithm to produce a secret key.
  2. Retrieve the key as a Base64 encoded string.
  3. Return a string formatted in a specific way.

The string we're resolving to is split up into different parts separated by the $ sign.

The first part is the algorithm we're using, which scrypt.

Then, you have the cost, block size, parallelization, salt, and finally, the secret key in base64 format.

This is the string you'll store in your database.

But, why are we storing all this information instead of just the secret key?

Remember how you're supposed to log a user in? You need to be able to encode the password they provide while logging in again. The encoded result needs to match the data you're storing right now exactly, or else you won't be able to confirm that their password is correct.

In order to encode their password again while logging, you'll also need information about which algorithm you used in case you switch to another one later, the parameters you used to configure the algorithm, as well as the random salt you generated. That's why you store them together.

If an attack gains access to your database, even with all this information, they won't be able to derive the user's password.

What about the salt? Isn't it supposed to be encrypted? No, because it's only used make sure that no two passwords are the same. Even with access to the salt, the attacker can't derive the password from the key because all passwords have been turned into random strings using the salt before being fed to the algorithm.

Until next time

Now that you stored the user's password securely, you need to use it to log them in.

However, this isn't as simple as just using === to compared two strings, as you'll see in Part Two of this post.

Subscribe to get notified when it's out.