Welcome to this reading on how to enhance password security by salting passwords before hashing them. Think of “salting” as sprinkling extra secret “spice” onto a password before it gets turned into a hash, making it much harder for malicious actors to figure out your user’s original plain-text password.
In earlier lessons, you saw how hashing a password can obscure it, but two identical passwords will still produce the same hash. That’s where “salts” come in—random strings that you add to the password before hashing to ensure uniqueness and thwart certain types of attacks, like Rainbow Table Attacks.
Imagine you have a password called "password". By itself, if multiple users choose “password,” they’ll share the same hashed output. That consistency might appear in a Rainbow Table (a giant map of possible inputs to their hashed outputs), letting an attacker recognize the plain-text is likely “password.” But if you salt each user’s “password” differently, they no longer produce the same final hash.
Salting is like adding a secret sauce to a recipe. Even if two people try the same recipe, your sauce changes the flavor so that the final dish can’t be easily replicated or recognized. In security terms, the salt ensures that even if two users share the same password, their salted-and-hashed outputs differ.
To salt a password, you generate a random string of characters (often 32 or more) and append or prepend it to the plain-text password before hashing:
// simplified conceptual example
const salt = generateRandomString(32); // e.g., "ksiblpwiensnfndnfnendks9872kdhf"
const plainTextPassword = "password"; // user input
const saltAndPassword = salt + plainTextPassword;
// e.g., "ksiblpwiensnfndnfnendks9872kdhfpassword"
const finalHash = hashingFunction(saltAndPassword);
This ensures that even if multiple users choose "password," they each have a different salt, so each final hashed value is different.
In practice, you might store both the salt and the hashed password in the database, but never the plain-text password. That way, the next time a user logs in, you retrieve their salt, append it to the incoming password, re-hash, and compare to the stored hash for verification.
Let’s look at an extremely simplified example. Imagine a simplistic “hash” function that scrambles characters, and a short 4-character “salt.” Real-world implementations are far more complex, but this will illustrate the point.
| User | Plain-Text Password | Salt | Salted Password | Salted + Hashed Output |
|---|---|---|---|---|
| 1 | "password" | "jhsd" | "jhsdpassword" | "hjdspfsawsrod" |
| 2 | "newPassword" | "nwsi" | "nwsinewPassword" | "wnispfenPwsawsrod" |
| 3 | "password" | "ksib" | "ksibpassword" | "skbiplsawsrod" |
Notice that Users 1 and 3 originally share the same plain-text password ("password"), but different salts produce different final hashes. Without salts, both would’ve ended up with the same hashed result.
In professional environments, salting is critical for:
Think about a popular ride-share app or social media platform. Millions of users might pick the same easy password (maybe “password” or “qwerty”). If the platform only hashed, the repeated hashed outputs would be a giveaway to attackers. With salting, each user’s password is unique, no matter if they chose the same plain-text.
Let’s do a quick demonstration. This won’t be production-grade code, but you can follow along to see the core idea:
function simpleHash(str) {
// Very simplified hash for demonstration only
// In real life, you'd use a library like bcrypt, scrypt, or Argon2
let hash = 0;
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i);
hash = (hash + charCode * 13) % 999999; // arbitrary simple hashing
}
return hash.toString(16); // convert to hex
}
function generateSalt(length) {
// Generate a random salt of a given length
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let salt = '';
for (let i = 0; i < length; i++) {
salt += chars.charAt(Math.floor(Math.random() * chars.length));
}
return salt;
}
// Follow along:
// 1) User "A" picks the password "cat123"
const saltA = generateSalt(8);
const saltedPasswordA = saltA + "cat123";
const hashA = simpleHash(saltedPasswordA);
// 2) User "B" picks the password "cat123" too
const saltB = generateSalt(8);
const saltedPasswordB = saltB + "cat123";
const hashB = simpleHash(saltedPasswordB);
console.log("User A salt:", saltA);
console.log("User A salted+hashed password:", hashA);
console.log("User B salt:", saltB);
console.log("User B salted+hashed password:", hashB);
// Notice they won't match, even though both used "cat123"
If you run this snippet, you’ll see that even though both users typed the same password, their final salted+hashed outputs differ. This is the heart of how salting defeats attempts to match identical plain-text passwords.
While salting is powerful, it’s not a silver bullet. Attackers can still try to crack salted hashes with enough computational resources, especially if the original password is weak. That’s why most modern systems:
You’ll also see references to “pepper,” a secret key stored separately from the database, used in conjunction with a salt. This practice adds a second line of defense if the database is compromised (the attacker doesn’t have the pepper unless they also breach wherever it’s stored).
By adding a random salt to a plain-text password before hashing, you reduce the risk of Rainbow Table Attacks and make it significantly harder for malicious users to figure out the original passwords—especially if multiple users picked the same ones.
This is a crucial step in password security, but it’s only one part of the bigger picture. You’ll learn next about how to put these principles into practice using frameworks like Sequelize and keep your salted+hashed passwords safe from accidental leaks by hiding them from the client.