Salting Explained: Why Two Identical Passwords Shouldn't Hash the Same

By Freddy ·

Hash the string password123 with SHA-256 and you get the same 64 hex characters every single time, on every machine, forever. That is exactly what a hash function is supposed to do, and it is also exactly the property that makes a bare hash a bad way to store passwords. If two users on the same system both pick password123, an unsalted hash makes that fact visible in the database: both rows hold the identical hash. A salt exists to break that link, and understanding why it works clears up a surprising amount of confusion about what actually keeps hashed passwords safe.

What a salt actually is

A salt is a random value, typically 16 bytes or more, generated fresh for every password and stored alongside its hash rather than kept secret. Instead of hashing the password alone, the system hashes the password concatenated with its salt: hash(password + salt). Change the salt and the output changes completely, even for the exact same password, because a cryptographic hash function is designed so that any change to the input, even one character, produces a wholly different, unrelated output. Two users with the password password123 now get two different salts and therefore two different stored hashes, and nothing in the database reveals that their passwords matched.

Why this defeats rainbow tables

A rainbow table is a precomputed lookup: an attacker hashes millions or billions of common passwords once, in advance, and builds a table mapping each hash back to its plaintext. Steal an unsalted password database and the attacker just looks up each hash in that table, an operation that takes milliseconds per password because all the expensive work was done ahead of time, once, against no particular target. A salt breaks this completely, because the table would need a separate precomputed set of hashes for every possible salt value. With a 16-byte random salt, that is not a bigger table, it is an astronomically larger one, to the point of being practically unbuildable. The attacker is forced back to attacking each password individually, computing hash(guess + salt) fresh for that one row, which is exactly the slow, per-password work salting was meant to force.

The salt does not need to be secret

This is the part that trips people up: the salt sits right next to the hash in the database, often literally concatenated into the same stored string, and that is fine. A salt's job is not to be a secret an attacker cannot find, it is to make each password's hash computation unique so that precomputation stops working and so that identical passwords stop producing identical hashes. Even with the salt in plain sight, an attacker who steals the database still has to run the hash function again for every single guess against every single row, with no shortcut from earlier work on other targets or other systems. That per-row cost, multiplied across a large user base, is the actual defense, not secrecy.

Salt vs pepper

A pepper is a related but distinct idea: a single secret value, the same for every password in the system, stored outside the database entirely, typically in an environment variable or a secrets manager. Where a salt is per-user and public, a pepper is system-wide and secret. The point of a pepper is to protect against a database-only breach: if an attacker steals the password table but not the application's secrets, the stolen hashes are useless without the pepper, even with unlimited time to guess passwords. A pepper adds real value against that specific scenario, but it does not replace a salt, since it does nothing to stop two identical passwords from producing the same hash(password + pepper) in the first place. Serious systems use both: a salt per password, plus a system-wide pepper as a second, independent layer.

Why the algorithm still matters

Salting alone does not make a fast hash function like SHA-256 an appropriate choice for password storage, because it only stops precomputed attacks. A salted SHA-256 hash is still fast enough that a modern GPU can attempt billions of guesses per second against a single, specific target row, which makes brute-forcing a weak or common password entirely feasible even with a unique salt in place. This is exactly why bcrypt and Argon2 exist: they are deliberately slow and memory-intensive, and both generate and store a salt automatically as part of their output format, so using them correctly means you get proper salting for free without implementing it by hand. The salt closes the rainbow-table gap; the algorithm's cost closes the brute-force gap, and a production system needs both, not one or the other.

What this means if you are storing passwords

In practice, almost no one should implement salting by hand at all. Libraries built around bcrypt, Argon2, or scrypt generate a cryptographically random salt automatically on every hash call and embed it directly in the output string, so a single stored value contains everything needed to verify a later login attempt. The mistake to actually watch for is reusing the same salt across every user, or worse, hardcoding one into the application, both of which throw away the entire point and put you back in rainbow-table territory. If you are building or auditing an authentication system, the real question is not “do we salt,” it is whether the hashing library handles it automatically and whether a strong enough password was chosen in the first place, since salting protects against precomputation, not against a password that is simply easy to guess.

← Back to KeyForge