How to Store Passwords Securely
Learn how to store passwords securely including encrypted vs hashed passwords, what a salt is, how to encrypt and decrypt passwords with Node, how to hash passwords with Node, and how logging in works with hashed and salted passwords.
Table of Contents 📖
- How to Store Passwords
- Never Store Plain Text Passwords
- Never Store Encrypted Passwords
- Storing Hashed Passwords
- Salting Passwords
- Installing bcrypt
- Hashing Passwords with bcrypt
- 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. Before we go any deeper, lets talk about the ways passwords should NOT be stored in a database.
Never Store Plain Text Passwords
Storing passwords as plaintext is never a good idea. If a hacker manages to access a database containing plaintext passwords then they have access to every user's password. Even if a user created a really strong password (varying characters, numbers, special characters), the hacker would be able to see the password in plaintext in the database.
Never Store Encrypted Passwords
Encrypted passwords are passwords that have been transformed from plaintext into unreadable ciphertext. Encrypted passwords are better than plaintext passwords because if a hacker gains access to the database containing encrypted passwords, they would have to decrypt the passwords to use them. We can encrypt plaintext with Node using the crypto library.
import crypto from 'crypto';
const algorithm = "aes-256-cbc";
const initVector = crypto.randomBytes(16);
const securityKey = crypto.randomBytes(32);
function encrypt(messageToEncrypt) {
const cipher = crypto.createCipheriv(algorithm, securityKey, initVector);
let encryptedMessage = cipher.update(messageToEncrypt, "utf-8", "hex");
encryptedMessage += cipher.final("hex");
return encryptedMessage;
}
const encryptedMessage = encrypt("Encrypt me!");
console.log(encryptedMessage); // 99d34f9a505a29e0b4ae701b18c3cd76
Encrypting plaintext requires a cryptographic algorithm and a key. In this instance we encrypt the plaintext "Encrypt me!" into ciphertext using the aes-256-cbc algorithm and a randomly generated key. However, storing encrypted passwords is not the best idea either as they can be decrypted if the key is discovered.
function decrypt(messageToDecrypt) {
const decipher = crypto.createDecipheriv(algorithm, securityKey, initVector);
let decryptedMessage = decipher.update(messageToDecrypt, "hex", "utf-8");
decryptedMessage += decipher.final("utf8");
return decryptedMessage;
}
const decryptedMessage = decrypt(encryptedMessage);
console.log(decryptedMessage); // Encrypt me!
Here we use the same key to decrypt the ciphertext as we did to encrypt it. As a result, if a hacker got hold of the key they could decrypt all the encrypted passwords in the database.
Storing Hashed Passwords
The best way to store passwords is to store their 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. To demonstrate hashing, lets use Node.
function hashPassword(password) {
const hash = crypto.createHmac('sha256', '')
.update(password)
.digest('hex');
return hash;
}
const hash = hashPassword("cheese");
console.log(hash); // 36915330430f5d484a434f8507
Here we create a hashing function with the SHA256 algorithm, pass the password to it, and then calculate the hash. However, storing a hash alone is not enough. This is because the same input provided to the hash will always give the same output.
function hashPassword(password) {
const hash = crypto.createHmac('sha256', '')
.update(password)
.digest('hex');
return hash;
}
const hash1 = hashPassword("cheese");
console.log(hash1); // 36915330430f5d484a434f8507
const hash2 = hashPassword("cheese");
console.log(hash2); // 36915330430f5d484a434f8507
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.
Salting Passwords
A salt is a unique randomly generated string that is added to each password. The salt and password are then hashed together. For example, we can change our Node hashPassword function to accept a salt and then hash the same password with different salts.
function hashPassword(password, salt) {
const hash = crypto.createHmac('sha256', salt)
.update(password)
.digest('hex');
return hash;
}
const hash1 = hashPassword("cheese", "salt1");
console.log(hash1); // 0de4bb033ecd999ff5
const hash2 = hashPassword("cheese", "salt2");
console.log(hash2); // a52ba71e64e685d4aa
Now even though the password is the same, the salt makes the hash different. However, the way the password was hashed and salted here is merely a demonstration of hashing and salting. In production this is not a secure way to hash passwords as the algorithm isn't strong enough. Instead, a 3rd party library should be used. An example of one of these libraries is bcrypt.
Installing bcrypt
To begin, lets quickly set up the project so we can 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(userPassword) {
const salt = await bcrypt.genSalt(12);
const passwordHash = await bcrypt.hash(userPassword, salt);
return passwordHash;
}
async function main() {
const userPassword = 'password';
const passwordHash = await hashPassword(userPassword);
console.log(passwordHash);
}
main();
- Import the bcrypt library.
- Generate a salt with 12 salt rounds using the genSalt function. The more rounds the more secure the hash but the more CPU and GPU performance required. Specifically, the higher the salt rounds the higher the rounds of hashing. 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.
- Hash the password using the hash function. It takes the string to hash as the first argument and the salt as the second.
- Return the password hash.
- Create a main function to run the hashPassword function and log the result to the console.
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$12$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 12 salt rounds (meaning 2^12 iterations to generate the hash), and the salt and hash are concatenated at the end.
Verifying Passwords
We can verify passwords with bcrypt 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.