Node Password Hashing with bcrypt
Learn how to hash passwords with Node using the bcrypt library. We will also go over how to store passwords securely, what a hash is, and what a salt is.
Table of Contents 📖
- How to Store Passwords
- What is a Hash?
- Project Setup
- Hashing Passwords with bcrypt
- What is a Salt?
- What are Salt Rounds?
- Verifying Passwords
- How this Works with a Database
How to Store Passwords
Passwords need to be stored in a database such that even if the database was breached by a hacker, the hacker would have a hard time using the passwords. To do this, the actual password values should not be stored in the database, but rather a hash of the password. This not only protects passwords from hackers, but also from anyone who has access to the database, including developers. The best way to store passwords is to store their hash.
What is a Hash?
A hash is the result of input being passed through a hash function. Hashes are the best way to store passwords as, unlike encrypted passwords, hashes cannot be reverted back to their original values. Therefore, if a hacker accessed a database of hashed passwords, they wouldn't be able to decrypt them. Rather, they would have to pass random passwords through a hash function to try and match one of the hashes stored in the database. A common way to hash passwords with Node is to use bcrypt, an npm package used to hash passwords.
Project Setup
To begin, lets quickly set up the project. We will initialize it as an ES6 npm project and then install bcrypt.
npm init es6 -y
npm i bcrypt
Now lets create a src directory to hold the source code, which for this project will just be an index.js file.
mkdir src
cd src
touch index.js
Hashing Passwords with bcrypt
Now lets start hashing passwords with bcrypt.
import bcrypt from 'bcrypt';
async function hashPassword(password) {
const passwordHash = await bcrypt.hash(password);
console.log(passwordHash);
}
const userPassword = 'password';
hashPassword(userPassword);
- Import the bcrypt library.
- Create a password to hash, which is just the string password.
- Hash the password using bcrypt's hash function. Note that this returns a promise.
- Log the hashed password to the console.
However, when we run this code, we get an error logged to the console.
node_modules/bcrypt/bcrypt.js:137
error = new Error('data and salt arguments required');
^
Error: data and salt arguments required
This error happens because, security wise, storing a hash alone is not enough. This is because the same input provided to a hash function will always give the same output. For example, hashing the string password will always give the same output (if we are using the same hashing algorithm of course). Therefore, giant lists of common passwords and their corresponding hashes can be used by hackers to identify hashed passwords in a database. A better idea is to store a unique hash for each password. This can be done using a salt.
What is a Salt?
A salt is a unique randomly generated string that is added to each password. The salt and password are then hashed together. We can use the bcrypt library to generate a salt for us.
const salt = await bcrypt.genSalt();
console.log(salt);
const passwordHash = await bcrypt.hash(userPassword, salt);
console.log(passwordHash);
Running the program above, we get the following output. Of course, the output will be different each time we run the program because of the salt.
node ./src/index.js
$2b$10$CMM1Zbpbm40uox.7CFoahO
$2b$10$CMM1Zbpbm40uox.7CFoahOyuasLdqNo8EGmah0OfWY0EZ6/5nnq8a
The output hash is the concatenation of different things separated by a $.
$[algorithm]$[cost]$[salt][hash]
Here 2b is the algorithm (2b means BCrypt), there are 10 salt rounds (meaning 2^10 iterations to generate the hash), and the salt and hash are concatenated at the end.
What are Salt Rounds?
The genSalt function also accepts an argument for the amount of salt rounds, or cost. The higher the salt rounds the more secure the password will be, but at the cost of performance due to CPU/GPU time. Specifically, the higher the salt rounds the higher the rounds of hashing. Lets change the function to use 12 salt rounds. The default value is 10.
async function hashPassword(userPassword) {
const salt = await bcrypt.genSalt(12);
console.log(salt);
const passwordHash = await bcrypt.hash(userPassword, salt);
console.log(passwordHash);
}
Running the program above, we get the following output.
node ./src/index.js
$2b$12$GVFHqlVStHoAa17dRBa5bO
$2b$12$GVFHqlVStHoAa17dRBa5bOgIeBN0KG5ms.vslVKrgrF8nWvAYmrPm
Note how the cost is now 12 as opposed to 10. As a rule of thumb, the number of rounds to use should be based the specs of the system performing the hashing. You want your password to be as secure as possible, but you don't want to use a number of rounds that hinders the application performance.
Verifying Passwords
We can also use bcrypt to verify passwords by using the compare function.
async function comparePassword(userPassword, passwordHash) {
const result = await bcrypt.compare(userPassword, passwordHash);
console.log(result);
}
This function returns true if the password matches the hash, and false if it does not. Lets now change our program to use the compare function with the hash generated from the hashPassword function.
import bcrypt from 'bcrypt';
async function hashPassword(userPassword) {
const salt = await bcrypt.genSalt(12);
const passwordHash = await bcrypt.hash(userPassword, salt);
return passwordHash;
}
async function comparePassword(userPassword, passwordHash) {
const result = await bcrypt.compare(userPassword, passwordHash);
return result;
}
async function main() {
const userPassword = 'password';
const passwordHash = await hashPassword(userPassword);
const verified = await comparePassword(userPassword, passwordHash);
console.log('The password verification is ' + verified);
}
main();
Running the program above, we get the following output.
node ./src/index.js
The password verification is true
How this Works with a Database
When it comes to storing hashed passwords in a database, we need to store both the salt and hash in a database. This is so the hash can be recalculated when the user logs in to determine if the password was correct. In other words, when a user logs in, the salt for that user is fetched from the database, appended to the provided password, hashed, and then compared to the hash stored in the database. If they are the same then the password is valid. However, bcrypt handles all this for us. With bcrypt, we simply need to store the hash in the database as it already contains the salt in it.