13.3. Password Hashing
Table of Contents
Why Password Hashing Matters
When you build a backend, you will almost never store user passwords directly. If your database is leaked and you keep passwords in plain text, every account is instantly compromised.
Password hashing is the standard way to protect stored passwords. It turns a human password into a long, random-looking string that is hard to reverse.
You will use password hashing whenever you:
- Implement user registration and login
- Verify a user’s identity before sensitive actions
- Reset or change passwords
In this chapter you will learn what hashing is, what makes password hashing special, and how to do it correctly in a modern backend.
**Never store plain text passwords.
Never log plain text passwords.
Never send plain text passwords by email or chat.**
Hashing vs Encryption
Both hashing and encryption transform data, but they have very different purposes.
One-way vs Two-way
- Encryption
- Goal: Keep data secret while allowing recovery.
- Operation:
ciphertext = encrypt(plaintext, key)plaintext = decrypt(ciphertext, key)- You must keep the key safe.
- Hashing
- Goal: Create a fixed-size fingerprint of data.
- Operation:
hash = H(data)- There is no key, and in practice you cannot get
databack fromhash.
For passwords we want a one-way operation. The server should not be able to recover the original password. It only needs to check if a provided password matches the stored hash.
Important Properties of Cryptographic Hash Functions
Cryptographic hash functions like SHA-256, SHA-512, or SHA-3 are designed with these properties:
- Deterministic:
Same input always gives the same output.
Example:
H("password123")will always produce the same hash. - Preimage resistance:
Given a hash value, it should be hard to find any input that produces it. - Second preimage resistance:
Given one input, it should be hard to find a different input with the same hash. - Collision resistance:
It should be hard to find two different inputs with the same hash. - Avalanche effect:
A tiny change in input causes a completely different hash.
Example with SHA-256:
| Input | SHA-256 (first 16 chars) |
|---|---|
password123 | ef92b778bafe771e… |
password124 | bcbe3365e6ac95ea… |
Even though only one character changed, the hash output is very different.
However, general-purpose hash functions (like SHA-256) are not enough for password storage. You need stronger protection.
Why Plain Hashing Is Not Enough
You might think:
“I will just store SHA-256(password) in the database. That is safe, right?”
This is better than plain text, but still weak. Modern attackers can try billions of passwords per second using GPUs or custom hardware.
Fast Hash Functions Are a Problem
Cryptographic hash functions are designed to be very fast. That is great for integrity checks, but terrible for passwords.
If your hash function is too fast, an attacker can:
- Take a list of common passwords.
- Hash each one with SHA-256.
- Compare to stolen hashes.
- Quickly find matches.
This is called a brute-force or dictionary attack.
Rainbow Tables
A rainbow table is a huge precomputed table of password -> hash pairs.
Example:
| Password | SHA-256 (shortened) |
|---|---|
123456 | 8d969eef6ecad3c2… |
password | 5e884898da280471… |
qwerty | b1b3773a05c0ed01… |
If an attacker steals your database of SHA-256 hashes, they can:
- Look up each hash in the rainbow table.
- Immediately find weak passwords without needing to hash anything.
To defeat this you need salts and slow, password-specific hashing algorithms.
Salting Passwords
A salt is a random value that you generate for each password and store along with the hash.
How a Salt Works
Process:
- Generate a random salt for the user, for example:
salt = "x83Kf7pQzWy1" - Combine password and salt, for example:
input = password + salt - Compute hash:
hash = H(input) - Store
saltandhashin the database.
When the user logs in:
- Retrieve that user’s
saltandhash. - Compute
hash_attempt = H(password_attempt + salt). - Compare
hash_attemptand storedhash.
Example:
| User | Password | Salt | Hash (simplified) |
|---|---|---|---|
| A | password123 | x83Kf7pQ | H("password123x83Kf7pQ") |
| B | password123 | Z1Lm9aT2 | H("password123Z1Lm9aT2") |
Even though both users have the same password, their hashes are completely different.
Why Salts Help
Salts solve several problems:
- No shared hashes for same passwords
Attackers cannot see that user A and user B use the same password. - Rainbow tables become useless
The attacker would need a separate rainbow table for every possible salt. That is practically impossible if salts are long and random.
**Always use a unique, random salt per password.
Never reuse the same salt for all users.
Never use usernames or emails as salts.**
However, salts alone do not make the hash slow. You still need a password hashing algorithm.
Password Hashing Algorithms
Password hashing algorithms are special functions designed to be slow and expensive to compute. This makes brute force attacks much harder.
Popular modern options:
| Algorithm | Type | Recommended today? | Notes |
|---|---|---|---|
| bcrypt | CPU hard | Yes | Widely used, very mature |
| scrypt | Memory hard | Yes | More memory usage |
| Argon2 | Memory hard | Yes, preferred | Winner of PHC, considered modern best |
| PBKDF2 | CPU hard | Acceptable | Often built in, but older |
These algorithms typically include:
- Salt handling (you do not need to manage salt separately)
- Configurable cost parameters: iterations, memory, parallelism
Key Properties of Password Hashers
- Slow to compute
You can tune them to take, for example, 100 ms per hash. For users this delay is almost invisible, but for attackers that try billions of guesses it is extremely painful. - Memory hard (for some algorithms)
Algorithms like Argon2 and scrypt require a lot of memory. This makes attacks with GPUs or specialized hardware more expensive. - Configurable
You can increase cost as hardware gets faster.
**Never use plain SHA-1, SHA-256, or MD5 alone for password storage.
Always use a dedicated password hashing algorithm like Argon2, bcrypt, scrypt, or PBKDF2.**
How Password Hashing Works in Practice
You do not need to implement hashing yourself. You will use a library. The process looks like this:
During Registration
- User sends a password to your backend.
- Your backend calls a password hashing function.
- The function:
- Generates a random salt.
- Applies the password hashing algorithm with the configured cost.
- Returns a password hash string that usually includes:
- Algorithm name
- Cost parameters
- Salt
- Final hash
- You store that hash string in the database.
Example of a bcrypt hash string:
$2b$12$hJf9sZMfH4KbqzCEwmbF6O4y/uUvdGbEgVpeKBUIwZNWqJnnmOegqThis string encodes:
$2b$algorithm version12cost factor- salt and hash combined
You store this single string in a column like password_hash.
During Login
- User sends email and password.
- You load the user from the database and get
password_hash. - Call a verify function provided by the library:
- The function reads algorithm and parameters from the stored hash string.
- It extracts the salt.
- It runs the hashing algorithm on the provided password.
- It compares the result to the stored hash, often using a constant time comparison to resist timing attacks.
- If verification passes, login succeeds.
You never decode the hash back to the original password. You only perform the same hash and compare.
Cost Factors and Tuning
Password hash algorithms have cost parameters that control how slow the hashing is. The idea is to make hashing:
- Fast enough that users do not notice
- Slow enough that attackers cannot try many guesses
Examples of Cost Parameters
- bcrypt
- Uses a "work factor" or "log rounds"
- If cost = 12, that means $2^{12} = 4096$ iterations internally
- Increase cost to make hashing slower
- Argon2
Common parameters: - Memory cost: how much memory to use
- Time cost: how many iterations
- Parallelism: how many lanes / threads
- PBKDF2
- Uses number of iterations
Choosing Cost Values
Typical approach:
- On your server hardware, run a small benchmark.
- Adjust parameters until each hash takes something like:
- 50 ms to 250 ms on the server.
For example, if you use bcrypt:
- Start with cost
12. - Measure.
- If hashing is very fast, raise to
13. - If hashing is too slow, lower to
11.
**Do not leave the cost too low just because it is faster.
Revisit cost settings as hardware improves.**
Storing Password Hashes Safely
You typically have a users table with a column for the password hash.
Example schema:
| Column | Type | Example value |
|---|---|---|
| id | integer | 1 |
| text | alice@example.com | |
| password_hash | text | $2b$12$hJf9sZMfH4KbqzCEwmbF6O4y/uUvdGbEgVpeKBUIwZNWqJnnmOegq |
| created_at | timestamp | 2026-08-27 10:05:00 |
Guidelines:
- Store only the hash string, not the plain password and not the salt separately unless required by your library.
- Use a type that can store enough characters (
TEXTin PostgreSQL,VARCHAR(255 or 512)or similar). - Never log
password_hashin plain logs if you can avoid it, especially not user passwords.
You might also want a way to mark:
- Whether the password is in a legacy format (for migration)
- When the password was set or updated
Migrating and Updating Hash Algorithms
Over time, algorithms and best practices change. For example:
- You have old users stored with bcrypt cost = 8.
- You decide to move to Argon2 or to a higher cost.
You can migrate gradually.
Strategy: Lazy Migration on Login
- Keep logic for both the old and new algorithm.
- When a user logs in:
- Detect the type of hash from the stored string.
- Verify with the old algorithm.
- If verification succeeds and the hash is old:
- Immediately rehash the password with the new algorithm or higher cost.
- Save the new hash value.
- Next time, verification uses the new hash.
This way, users do not need to reset passwords manually.
Example flow:
- Stored hash:
$2b$08$...(bcrypt, cost 8). - Login arrives, you verify with bcrypt cost 8.
- If OK, you compute a new Argon2id hash for the same password.
- Store the Argon2id hash string.
- From now on, login uses Argon2id verification.
**Never try to convert one hash to another without the original password.
You must rehash using the plain password during a successful login or password change.**
Common Mistakes and How to Avoid Them
1. Storing Plain Text Passwords
- Mistake:
passwordcolumn contains exactly what the user typed. - Fix: Store only a secure hash, use a dedicated password hashing library.
2. Using MD5 or SHA-256 Directly
- Mistake:
password_hash = sha256(password) - Problem: Too fast, vulnerable to brute force and rainbow tables.
- Fix: Use Argon2, bcrypt, scrypt, or PBKDF2, not raw hash functions.
3. Using a Single Global Salt
- Mistake: Use the same salt for every user or something predictable like a constant or app name.
- Problem: Does not prevent rainbow tables effectively, shared hashes for same passwords.
- Fix: Use a unique random salt per password. Libraries usually do this for you.
4. Rolling Your Own Cryptography
- Mistake: Implementing hashing logic yourself, mixing multiple hash functions "for extra security."
- Problem: Very easy to get details wrong.
- Fix: Use well reviewed libraries, follow their recommended settings.
5. Returning Too Much Information on Login Failure
- Mistake: Returning messages like:
"User does not exist"vs"Wrong password"separately.- Problem: Helps attackers know which emails are registered.
- Fix: Use a generic message like
"Invalid credentials".
Note that this is more related to authentication logic, but it is often implemented together with password hashing.
Summary
Password hashing is a critical part of building a secure authentication system.
Key ideas:
- Use one-way hashing, not encryption, for storing passwords.
- Add a unique random salt to each password to defeat rainbow tables.
- Use a dedicated password hashing algorithm such as Argon2, bcrypt, scrypt, or PBKDF2, not plain SHA or MD5.
- Configure cost parameters so hashing is slow enough to hurt attackers but fast enough for users.
- Store only the hash string in your database, never plain passwords.
- Plan for algorithm upgrades by rehashing on login or password change.
With these principles, even if your database is leaked, cracking your users’ passwords becomes significantly harder and much more expensive for attackers.
Views: 7
KAHIBARO