Password hashing & salting
- How to implement password hashing and salting
- Secure password storage best practices
- Why use salt in password hashing
- How to prevent rainbow table attacks
- Password salting
- What is pepper in password security
- How to secure passwords with salt and pepper hashing
Plain hashing
When handling user PINs or passwords, storing them as plain text creates significant security risks.
One may think of applying a hashing – and store hashed version of password:
- Client sends plain PIN to server (over secure HTTPS)
- Server hashes the PIN and stores it in the DB
- When a user verifies a PIN later, it sends plain PIN over HTTPS again
- Server hashes received PIN and compares it with the one stored in the DB
- Server returns result: yes/no
- We don’t care if DB is leaked, since we don’t store plain passwords (or we do?)
However, there is such a thing called “rainbow tables”. They are precomputed tables in the following format:
pass | hash
----------------------
"1234" | "a1b2c3"
"changeit" | "x7y8z9"
"qwerty" | "m4n5p6"
...millions more...
In case DB with hashed passwords is leaked, attacker may compare leaked hashes with rainbow tables and thereby find out the PIN / pass.
You may say that “wait, there may be hash collisions, so different data may produce the same hash”. The thing is that hash collisions are extremely rare with modern hash functions like SHA-256, SHA-3, or BLAKE2 (that is the reason they’re being used for file integrity checks and digital signatures).
It means that if we see hash x7y8z9 in the DB, we can almost be sure that the password is changeit.
Hashing + salting
The trick to increase cryptographic strength is to add some salt this way:
- Client sends plain PIN to server (over secure HTTPS)
- Server generates salt, for this particular user
- Server hashes PIN + salt, and stores resulting hash in DB PLUS salt (in its original representation)
- When a user verifies a PIN later, it sends plain PIN over HTTPS again
- Server reads the salt for this user from DB, salts received PIN, hashes that combination and compares it with the one stored in the DB
- Server returns result: yes/no
Pay attention:
- Plain PIN is never stored
- Only hash (generated from PIN+salt) and salt itself are stored in the database
- Hashing happens on the server side
- Client always sends plain PIN (but over HTTPS)
- Each user has unique salt
Why does it work?
Without unique salt, attacker could pre-compute hashes for all possible PINs. Then just match against the database.
With unique salt – same PINs lead to different hashes for different users:
User1: PIN "1234" + salt "abc" = hash "x7y8z9"
User2: PIN "1234" + salt "def" = hash "a1b2c3"
Hence, attacker needs to compute a new rainbow table for EACH salt. It makes attack computationally expensive.
Draft implementation code would look like this:
# PIN / password creation
def store_pin(plain_pin):
salt = generate_random_salt()
hash = hash_function(plain_pin + salt)
database.save(user_id, hash, salt)
# PIN / password verification
def verify_pin(received_pin):
stored_salt = database.get_salt(user_id)
stored_hash = database.get_hash(user_id)
calculated_hash = hash_function(received_pin + stored_salt)
return calculated_hash == stored_hash
Bonus: Pepper
- Pepper is a thing that is the same for all users
- It is stored separately (not in DB)
- It is secret value (like an app key)
hash = hash(password + salt + pepper)
DB stores: hash + salt only.
It benefits when a database is compromised, but server config isn’t.
Example implementation:
# Pepper stored in server config/env
PEPPER = "secret_key_123"
def store_password(password):
salt = generate_salt()
hash = hash(password + salt + PEPPER)
db.save(hash, salt) # pepper not stored!
It’s an additional security layer–even with database access, attacker needs server access too to get the pepper.