Password Security 101 - Understanding How Hashing Works
5 minute read | Apr 19, 2023
engineering
Password hashing is a crucial aspect of cybersecurity and is used by developers to avoid storing actual passwords in a database, which is at risk of hacks and leaks. This article demonstrates how hackers can exploit hashing vulnerabilities and why it's essential for developers to use unique salts when hashing passwords.
1. How does password hashing work?
To illustrate how password hashing works, we will use bcrypt, a popular Node.js password hashing algorithm.
Hashing passwords
In simple terms, hashing works as follows when a user first registers a password:
bcrypt(password + salt) => salt + hashed_password
Where:
- bcrypt is our hashing function
- password is our plain text password, for example "chicken"
- salt is a unique key
- hashed_password is a converted set of characters after passing through bcrypt(password + salt)
The salt + hashed_password combination is what is stored in the database against the unique user ID or email, not the original password.
The function bcrypt(password + salt) will always generate the same result, being salt + hashed_password.
The function is designed to be one-way, meaning if you have the salt and hashed_password, you cannot reverse engineer the original password.
Verifying passwords
In simple terms, verifying that the submitted password matches the hash stored in the database works as follows:
- Receive the user email and password combination, for example, jo@email.com and "chicken"
- Look up the hash stored against jo@email.com
salt+hashed_password
- Extract the salt from the hash (remember the hash is
salt+hashed_password
) - Run the password and salt through the bcrypt function to return the
salt+hashed_password
combination - Verify that the
hashed_password
matches the hash stored in the database
The code below illustrates this. The next section talks about the dangers of using the same salt when hashing passwords.
const bcrypt = require('bcrypt');
// Our plain text password
const password = "chicken"
// Our random salt, a unique key generated by bcrypt.genSaltSync()
const salt = '$2b$10$QyI8c1WJnKjBn7pGg1cn8e';
// The function returns a hash
const hash = bcrypt.hashSync(password, salt)
// hash: $2b$10$QyI8c1WJnKjBn7pGg1cn8eRs1DsEeTppMu.RodMnUP6bzQJdvYWIe
// hash has two components: salt+hashed_password
// $2b$10$QyI8c1WJnKjBn7pGg1cn8e + Rs1DsEeTppMu.RodMnUP6bzQJdvYWIe
// Compare password
// extract the salt from the hash above
const saltFromHash = '$2b$10$QyI8c1WJnKjBn7pGg1cn8e'
// Give the password and salt from hash to bcrypt to generate the hash
const compare = bcrypt.hashSync("chicken", saltFromHash)
// Compare the result against the stored hash
if (compare === hash) {
return true
} else {
return false
}
2. Dangers of fixed salts
To illustrate the dangers of using fixed salts, let's take a look at a simple example in Node.js with bcrypt.
We follow the same example above by hashing the password "chicken." Imagine our developer makes a mistake and uses the same salt to hash our passwords. Now, each time a user registers the password "chicken," we will generate the same hash.
const bcrypt = require('bcrypt');
// Users who share the same plain text password
const password = "chicken"
// ======= DANGER EXAMPLE ==========
// Use an example fixed salt to hash our password, generated by bcrypt.genSaltSync(10) and reused
const fixedSalt = '$2b$10$QyI8c1WJnKjBn7pGg1cn8e';
// Hash the password using the fixed salt
const dangerUser1 = bcrypt.hashSync(password, fixedSalt)
const dangerUser2 = bcrypt.hashSync(password, fixedSalt)
const dangerUser3 = bcrypt.hashSync(password, fixedSalt)
// DANGER RESULT: Users that have the same passwords have the same hashed passwords
// dangerUser1: $2b$10$QyI8c1WJnKjBn7pGg1cn8eRs1DsEeTppMu.RodMnUP6bzQJdvYWIe
// dangerUser2: $2b$10$QyI8c1WJnKjBn7pGg1cn8eRs1DsEeTppMu.RodMnUP6bzQJdvYWIe
// dangerUser3: $2b$10$QyI8c1WJnKjBn7pGg1cn8eRs1DsEeTppMu.RodMnUP6bzQJdvYWIe
This becomes a major security vulnerability. If a hacker gets a list of all hashes and knows that one belongs to "chicken," they can simply do a quick search to access other users' accounts.
3. Secure password hashing
To protect password security, developers should generate random salts each time they hash passwords.
In the example below, we modify the code to generate a random salt each time we hash the same password. Notice this time we generate a unique hash for each user. This is possible because we are using a random salt each time we hash a newly registered password.
Now if hackers crack one password hash, they will not be able to search for others.
const bcrypt = require('bcrypt');
// Users who share the same plain text password
const password = "chicken"
// ======= SECURE EXAMPLE ==========
// Use a random salt to hash our password every time, generated by bcrypt.genSaltSync(10) but not reused
const secureUser1 = bcrypt.hashSync(password, bcrypt.genSaltSync(10))
const secureUser2 = bcrypt.hashSync(password, bcrypt.genSaltSync(10))
const secureUser3 = bcrypt.hashSync(password, bcrypt.genSaltSync(10))
// SECURE RESULT: Users with the same passwords have unique hash
// secureUser1: $2b$10$8WVMo207.PukGs.2MFInvOuJxDIMBNXmAUWkedOuZHtCXnui5k8Ou
// secureUser2: $2b$10$AtmhuYfG8dtctiX7ctES5.Omr2Ew/qN05/qLK8KOe4LlF7r6uGBay
// secureUser3: $2b$10$jB54SBsF6YGfIFmZbv.qauXyLCK2d8SR9R.wGBiOSl4SwXVWBTJ96
Don't forget the salt 🧂
Want more tips?
Get future posts with actionable tips in under 5 minutes and a bonus cheat sheet on '10 Biases Everyone Should Know'.
Your email stays private. No ads ever. Unsubscribe anytime.