Why a Little Salt Can Be Great for Your Passwords (But Not Pepper!)
As a full-stack developer, one of the most critical aspects of building secure applications is properly handling user passwords. It‘s a grave responsibility – if passwords are compromised, it can lead to devastating consequences for both users and the company. Two essential techniques for securely storing passwords are hashing and salting. In this article, we‘ll dive deep into what these terms mean, why salting is crucial, and how it helps defend against password cracking attempts. We‘ll also explore why a technique called peppering is less practical and secure than salting alone.
Hashing and Salting: A Quick Primer
First, let‘s define some key terms:
-
Password Hashing: The process of taking a plain-text password and running it through a one-way cryptographic hash function (like SHA-256 or MD5) to produce a fixed-size string of characters. Hashing is irreversible – you can‘t directly decrypt a hash to recover the original password.
-
Salting: Adding a unique, randomly generated string (the "salt") to each password before hashing. Each salt should be different for every password hash. The salt is usually stored in plain text alongside the hashed password in a database.
Here‘s a simplified illustration of hashing with and without salt:
Hashing without salt:
PASSWORD123 --> fd0bce76a
abc123 --> c06ead3a2
password --> 5f4dcc3b5
Hashing with salt:
PASSWORD123 + fje84 --> 3fda76cc4
PASSWORD123 + t0m1x --> cc1f7de32
abc123 + d9r7q --> 8jk4522bc
password + 8ngl3 --> a76fd436b
The purpose of hashing is to avoid storing passwords in plain text. So if an attacker manages to breach the password database, they don‘t immediately have access to everyone‘s credentials. However, hashing alone is not sufficient, as we‘ll see shortly. This is where salting comes into play.
Why Salt Is Worth Its Weight in Security
Salting is critical for one primary reason – it makes cracking passwords from a stolen hash database significantly harder and slower. To understand why, we need to look at a common attack technique called rainbow tables.
Death of the Rainbow (Tables)
A rainbow table is a huge list of precomputed password hashes that allows an attacker to quickly reverse a hash into its original password. It takes a lot of time and computing power to generate a comprehensive rainbow table, but once it‘s created, looking up a hash becomes almost instant. Free and commercial rainbow tables are widely available for many common hashing algorithms.
Here‘s a simple example of a rainbow table:
Password | MD5 Hash |
---|---|
123456 | e10adc3949ba59abbe56e057f20f883e |
password | 5f4dcc3b5aa765d61d8327deb882cf99 |
qwerty | d8578edf8458ce06fbc5bb76a58c5ca4 |
abc123 | e99a18c428cb38d5f260853678922e03 |
letmein | 0d107d09f5bbe40cade3de5c71e9e9b7 |
If an attacker gets ahold of a database with unsalted password hashes, they can quickly look up each hash in a precomputed rainbow table to find the corresponding plain-text password. Game over.
However, rainbow tables become nearly useless against properly salted passwords. Remember, each password is appended with a unique salt before hashing. So even if two users have the same password, their salted hashes will be different:
User 1:
Password: Tr0ub4dor&3
Salt: 5d41402abc4b2a76b9719d911017c592
Salted Hash: 5fcb62afba0437a30d9c5082d233b6c0f51d0a13429cab6ce6f4e98b8068255c
User 2:
Password: Tr0ub4dor&3
Salt: 3a04b8a93c029325006c4f8e92069d10
Salted Hash: 87fa9c993022eee561bea5fd0f1f11d29a2185137f6ce67df7c97baa882216f6
To crack these salted hashes with rainbow tables, an attacker would have to generate a separate rainbow table for every possible salt value. If each salt is long and randomly generated, that becomes computationally infeasible. Salting forces attackers to fall back on brute-force guessing techniques that are orders of magnitude slower.
The Salty Stats
Now let‘s look at some eye-opening statistics that underscore the importance of password salting:
-
In a 2019 Google/Harris Poll survey, 52% of respondents reported reusing the same password for multiple accounts. Even worse, 13% used the same password for all their accounts! Salting ensures that even if a user‘s password hash is cracked on one site, it can‘t be used to compromise their accounts on other sites that share the same password.
-
According to the UK‘s National Cyber Security Centre, 23.2 million victim accounts worldwide used "123456" as their password. The top 5 most common passwords ("123456", "123456789", "qwerty", "password", "1111111") accounted for over 30 million compromised accounts. If websites had used proper salting (and other secure password storage techniques we‘ll discuss later), many of these accounts would have been much harder to breach.
-
In the massive Yahoo data breach of 2013, over 3 billion user accounts were compromised. The breach went undisclosed until 2016. Even worse, the stolen passwords were hashed with the weak MD5 algorithm and were not salted. This allowed attackers to quickly crack a large percentage of the password hashes using rainbow tables and common password lists.
The bottom line is that password reuse and weak passwords are rampant. Salting is a critical line of defense to mitigate the damage of credential stuffing and password spraying attacks when password databases are inevitably breached.
Beyond Simple Salting
Salting alone is a huge step up from storing unsalted password hashes. However, we can beef up password storage security even more with a few additional measures:
-
Use a deliberately slow hash function designed for password storage, like Bcrypt, Scrypt, or PBKDF2. Unlike general-purpose hash functions such as SHA-256 or MD5 which are designed to be fast, password hashing functions are intentionally slow to thwart brute-force guessing attempts. They achieve this by iterating the hash function many times and/or requiring a lot of memory to compute the hash. The goal is to make cracking passwords too time and resource-intensive to be worthwhile.
-
Set a high cost factor or iteration count. This controls how much computation is required to hash a password. Higher values make cracking slower, but also increase the time needed to verify a legitimate login attempt. Aim for a hash time between 0.5 to 1 second on your server hardware – slow enough to frustrate attackers, but not so slow that it noticeably degrades the user experience.
-
Generate salts using a cryptographically secure pseudorandom number generator (CSPRNG). This ensures that salts are unpredictable and have good entropy. Most programming languages have built-in CSPRNG functions, such as
os.urandom()
in Python orcrypto.randomBytes()
in Node.js. Avoid usingMath.random()
or similar for salts as they are not cryptographically secure. -
Make salts at least 16 bytes (128 bits) long. This provides sufficient randomness to make precomputed rainbow tables infeasible. Longer salts (e.g., 32 bytes) are even better, but have diminishing returns security-wise.
Here‘s an example of securely hashing a password in Node.js using the Bcrypt library:
const bcrypt = require(‘bcrypt‘);
const SALT_ROUNDS = 12; // Cost factor
async function hashPassword(password) {
const salt = await bcrypt.genSalt(SALT_ROUNDS);
const hashedPassword = await bcrypt.hash(password, salt);
return hashedPassword;
}
And here‘s how you would verify a password attempt against the stored hash:
async function checkPassword(password, hashedPassword) {
const isMatch = await bcrypt.compare(password, hashedPassword);
return isMatch;
}
Bcrypt handles salt generation automatically and stores the salt with the hash, so you only need to persist the hashed result in your database. The SALT_ROUNDS
constant controls the cost factor – higher values make hashing slower.
To Pepper or Not to Pepper?
Now that we‘ve established salting as essential and looked at additional hardening techniques, what about peppering? Peppering refers to the practice of appending a single secret value to all passwords before hashing, in addition to the unique-per-password salt.
The purported benefit of peppering is that even if an attacker manages to obtain the password hash database and the salts, they would still need the secret pepper value to crack the hashes. The pepper is usually hard-coded in the application code or stored in a separate secrets vault.
However, peppering has some significant drawbacks that make it less practical and secure compared to salting alone:
-
The security of peppered password hashes depends entirely on the secrecy of the pepper value. If the pepper is ever compromised (e.g., through source code repository leaks, accidental check-ins, disgruntled employees) then all peppered hashes are now vulnerable to cracking. There‘s no easy way to rotate the pepper without requiring all users to reset their passwords.
-
Peppering violates the principle of "separation of concerns" in security. The purpose of a password hash database is to store a verifier for user authentication – it shouldn‘t contain any application secrets. Mixing in a secret pepper value blurs this line.
-
Using a single global pepper means that all password hashes can be attacked in parallel once the pepper is known. In contrast, salting ensures that each password hash must be attacked individually even if the salt is known.
-
Peppering is not a standard, vetted construct like salting. It‘s an ad-hoc scheme that‘s more error-prone to implement securely. For example, if the pepper is ever logged or exposed client-side, it negates any security benefit.
-
Many password hashing libraries (like Bcrypt) don‘t support peppering out of the box. Developers may resort to rolling their own peppering scheme on top of these libs, which is risky. Crypto code is notoriously difficult to get right.
For the vast majority of applications, a strong password hashing scheme with per-password salting and a tuned cost factor is sufficient and preferable to peppering. Reserve peppering for narrow high-security scenarios where the pepper can be managed separately from application secrets and rotated frequently.
Putting It All Together
Congratulations, you now have a solid understanding of password salting, why it‘s important, and how to implement it securely! Let‘s recap the key points:
-
Always hash passwords before storing them. Never store plain-text passwords.
-
Hash passwords with a salt that is unique for each password. Salts should be random and at least 16 bytes long.
-
Use a deliberately slow hash function designed for password storage, such as Bcrypt, Scrypt, or PBKDF2.
-
Tune the cost factor or iteration count of your password hash function to make brute-force guessing attacks impractical.
-
Generate salts using a cryptographically secure random number generator. Avoid using
Math.random()
or similar. -
Consider peppering passwords only in narrow high-security scenarios, and only if you can manage the pepper separately from application secrets. For most cases, stick to salting.
-
Educate your users on creating strong, unique passwords and enable two-factor authentication whenever possible. No amount of hashing can save a weak password!
-
Implement additional login security measures such as IP-based rate limiting, account locking after failed attempts, and checking user passwords against known data breach lists.
By following these practices, you‘ll be well on your way to storing user credentials securely and thwarting many common attacks. However, remember that password storage is just one piece of the application security puzzle. You‘ll also need to harden your infrastructure, keep dependencies up to date, validate user input, and much more.
As a full-stack developer, it‘s your responsibility to stay abreast of the latest security threats and defensive techniques in our perpetual cat-and-mouse game against attackers. But equipped with a solid understanding of hashing, salting, and peppering, you‘ll be in a much better position to protect your users‘ precious passwords from compromise. Happy (and salty) coding!