13.2. Password Storage
Table of Contents
Why Password Storage Matters
When users create an account, they trust you with their passwords. If you store those passwords incorrectly, a database leak can instantly expose every account.
A backend developer must never store passwords in plain text. Even storing them with "simple encryption" is not enough. Proper password storage uses strong one‑way hashing with additional protections.
NEVER store plain text passwords. NEVER be able to read users' passwords.
In this chapter, you will see how to store passwords safely, what can go wrong, and how to use hashing correctly.
Plain Text Storage and Why It Is Dangerous
What Is Plain Text Storage?
Plain text storage means you keep the password exactly as the user typed it.
Example table in your database:
| user_id | password | |
|---|---|---|
| 1 | alice@example.com | mysecret123 |
| 2 | bob@example.com | 123456 |
| 3 | charlie@example.com | qwerty |
If an attacker gets a copy of this table, they immediately know every user’s password.
Dangers of Plain Text Passwords
Plain text passwords are dangerous because:
- Any data leak reveals all passwords immediately.
- Developers, admins, or support staff can see user passwords.
- Users reuse passwords. Attackers will try the same password on:
- Email accounts
- Social media
- Other websites
- Your service loses trust. If users know you stored plain text, your reputation is damaged.
Even if you think your database is "internal" or "safe," you must assume it can be stolen one day.
If you can tell a user their current password, your storage is already wrong.
Why Encryption Is Not Enough
What Is Encryption?
Encryption is a two‑way process:
- You encrypt data with a key to hide it.
- You can decrypt it with the same key (symmetric) or a related key (asymmetric).
Example idea (not real code):
encrypted_password = encrypt("mysecret123", key)
plain_password = decrypt(encrypted_password, key)Encryption is used for:
- Securing data in transit (HTTPS, TLS)
- Securing data at rest where you need to read it again (stored credit card numbers in some systems, secrets, etc.)
Why Encryption Is Wrong for Password Storage
For login, you never need to know the original password, you only need to verify "does the user know the password?"
If you use encryption:
- Anyone who gets the key can decrypt every password.
- The backend itself must keep the key available to verify logins.
- Malicious insiders or attackers who get server access can read all passwords.
So encryption turns password storage into "hidden plain text." It does not remove the danger.
Passwords must be stored using one‑way hashing, not reversible encryption.
One‑Way Hashing
What Is a Hash Function?
A hash function turns any input into a fixed‑length output.
Example idea:
hash("mysecret123") = "e99a18c428cb38d5f260853678922e03"
hash("hello") = "5d41402abc4b2a76b9719d911017c592"Characteristics of a cryptographic hash function:
- Deterministic: same input, same output.
- One‑way: you cannot easily find the original input from the hash.
- Sensitive to changes: small change in input produces a very different hash.
- Resistant to collisions: hard to find two inputs with same hash.
Common cryptographic hash functions:
| Algorithm | Type | Notes |
|---|---|---|
| MD5 | Hash | Obsolete, insecure |
| SHA‑1 | Hash | Obsolete, insecure |
| SHA‑256 | General hash | Secure for many uses, not ideal alone for passwords |
| SHA‑512 | General hash | Same as above |
These general hash functions are too fast for password storage.
Basic Hashing Example
Naive idea:
- When user registers:
- Store
hash(password)in database. - When user logs in:
- Compute
hash(input_password)and compare with stored hash.
Example data:
| user_id | password_hash | |
|---|---|---|
| 1 | alice@example.com | e99a18c428cb38d5f260853678922e03 |
This is better than plain text, but still unsafe on its own. Attackers can use rainbow tables and brute force attacks.
Salting: Defending Against Rainbow Tables
What Is a Salt?
A salt is a random string that you generate for each password.
You then compute:
$$
\text{stored\_hash} = H(\text{salt} \;||\; \text{password})
$$
Here, $H$ is a hash function and $||$ means "concatenate".
Example:
- Password:
mysecret123 - Salt:
K9n3!aZ@2# - Hash input:
K9n3!aZ@2#mysecret123 - Hash output:
5f16b8...(some long string)
You store both the salt and the hash.
Table example:
| user_id | salt | password_hash | |
|---|---|---|---|
| 1 | alice@example.com | K9n3!aZ@2# | 5f16b8... |
Why Salting Helps
Without salt, many users share the same hash for the same password:
| user_id | password_hash | |
|---|---|---|
| 1 | alice@example.com | e10adc3949ba59abbe56e057f20f883e |
| 2 | bob@example.com | e10adc3949ba59abbe56e057f20f883e |
If e10adc... is known to mean 123456, the attacker instantly knows both passwords.
With salts:
| user_id | salt | password_hash | |
|---|---|---|---|
| 1 | alice@example.com | abcd1234 | AAAA... |
| 2 | bob@example.com | xyz9876 | BBBB... |
Even if both users chose 123456, their stored hashes are different. There is no single "hash for 123456" anymore, because the salt is different each time.
This prevents precomputed rainbow table attacks, where attackers use large tables of precomputed hash values.
Salt Requirements
A good salt must be:
- Random: generated with a secure random generator.
- Unique per password.
- Long enough, for example 16 bytes or more (often encoded as 32+ characters in hex or Base64).
You do not need to keep salts secret. You can store them in the same table as the hash.
Why Fast Hashes Are Not Enough
Even with salts, using a fast hash algorithm like SHA‑256 is not enough.
Reason: attackers can try billions of guesses per second with GPUs and specialized hardware.
Example idea:
- Attacker steals database with salted SHA‑256 hashes.
- They write a program that:
- Takes a guess "password123".
- Computes H(salt || "password123") for every user.
- Compares with each stored hash.
- Because SHA‑256 is very fast, they can run this millions or billions of times per second.
So although salts prevent precomputed rainbow tables, they do not stop mass brute‑force attempts if the hashing is too cheap.
You need functions that are:
- Slow on purpose.
- Memory‑hard or configurable.
- Designed for passwords, not for general hashing.
Password Hashing Algorithms
Key Idea: Make Hashing Expensive
For password storage, you intentionally make hashing expensive. Not too slow for your users, but slow enough that guessing millions of passwords becomes impractical.
You use special functions called password hashing functions or key derivation functions.
Common choices:
| Algorithm | Type | Status / Recommendation |
|---|---|---|
| bcrypt | Password hashing | Very common, good choice |
| scrypt | Password hashing, memory‑hard | Strong, good choice |
| Argon2id | Password hashing, memory‑hard | Modern recommended standard |
| PBKDF2 | Key derivation, slower | Widely used, acceptable with enough iterations |
Any of these is far better than a raw hash like SHA‑256 alone.
Do not design your own password hashing algorithm. Use a well tested, standard one like bcrypt, scrypt, Argon2, or PBKDF2.
bcrypt
- Adds its own salt automatically.
- Has a cost factor (also called work factor) that controls speed.
- Output includes:
- Algorithm version
- Cost
- Salt
- Hash
Example bcrypt hash string:
$2b$12$9C8p4pY/.gQ1qrnOZwZQBuTVqYsZQkUpY..sh1BfXZ4VtaxGE7C2qYou do not manage the salt manually. The library handles it.
scrypt and Argon2
- Designed to be memory‑hard.
- These make attacks harder even with GPUs.
- You can configure:
- Time cost
- Memory cost
- Parallelism
They are considered very strong for password storage.
How Password Verification Works
The main idea:
- At registration or password change:
- Take the plain text password.
- Use a password hashing algorithm (argon2, bcrypt, etc.).
- Store only the hash output (which also encodes the salt and parameters).
- At login:
- Take the user’s input password.
- Run the same hashing algorithm with the stored parameters.
- Compare the resulting hash with the stored hash.
- If they match, the password is correct.
You never decrypt anything. You only recompute and compare.
Example: Using bcrypt in Practice
The exact code depends on the language and framework. This is a conceptual example in Python style, which you can adapt.
Registration Flow
- User sends:
{
"email": "alice@example.com",
"password": "MySuperSecret!"
}- Your backend:
- Generates a bcrypt hash:
import bcrypt
plain_password = "MySuperSecret!".encode("utf-8")
hashed = bcrypt.hashpw(plain_password, bcrypt.gensalt()) # gensalt chooses salt and cost
print(hashed) # something like b"$2b$12$9C8p4pY/.gQ1qrnOZwZQBuTVqYsZQkUpY..sh1BfXZ4VtaxGE7C2q"- Stores the hash in the database:
| user_id | password_hash | |
|---|---|---|
| 1 | alice@example.com | $2b$12$9C8p4pY/.gQ1qrnOZwZQBuTVqYsZQkUpY..sh1BfXZ4VtaxGE7C2q |
You store only this string, not the plain password and not a separate salt.
Login Flow
- User sends:
{
"email": "alice@example.com",
"password": "MySuperSecret!"
}- Your backend:
- Looks up
password_hashin the database. - Verifies:
stored_hash = b"$2b$12$9C8p4pY/.gQ1qrnOZwZQBuTVqYsZQkUpY..sh1BfXZ4VtaxGE7C2q"
plain_password = "MySuperSecret!".encode("utf-8")
if bcrypt.checkpw(plain_password, stored_hash):
# Password correct
else:
# Password incorrectbcrypt extracts the salt and cost from the stored hash, then compares in a safe way.
Handling Password Changes
When a user changes their password:
- Verify current password using the existing stored hash.
- If correct, hash the new password using your password hashing function.
- Replace the old hash with the new one.
Old password is gone forever. You cannot retrieve it.
Upgrading Hash Algorithms
You might start with one algorithm and later choose a stronger one. For example:
- Old: PBKDF2
- New: Argon2id
You cannot convert old hashes directly, because hashing is one‑way. Instead, you upgrade gradually.
Typical strategy:
- Store information about which algorithm and parameters were used, usually encoded in the hash string.
- On login:
- Verify using the appropriate algorithm for the stored hash.
- If login succeeds and the hash uses an old algorithm or weak parameters:
- Rehash the password with the new algorithm.
- Store the new hash.
Example flow:
- User has an old PBKDF2 hash stored.
- They log in successfully.
- Backend:
- Checks: "This hash uses old parameters."
- Recomputes hash using Argon2id.
- Replaces stored hash with new Argon2 hash.
Over time, as users log in, their password hashes get upgraded without forcing everyone to reset passwords at once.
Common Mistakes in Password Storage
Here are patterns to avoid.
1. Storing Plain Text or Reversible Encryption
Bad:
password = "MySecret123"password_encrypted = encrypt("MySecret123", key)
If someone has database + key, they have every password.
2. Using Fast Hashes Alone
Bad patterns:
hash = SHA256(password)hash = MD5(salt + password)
Even if salted, fast hashes are easy to brute force.
3. Custom or Obscure Algorithms
Bad idea:
- Designing your own hash algorithm.
- Chaining multiple general hash functions, for example:
SHA1(SHA256(SHA512(password)))
Security is hard. Use standard, tested algorithms implemented by experts.
4. Short or Non‑Random Salts
Avoid:
- Using user ID as salt.
- Using the same salt for all users.
- Using predictable salts such as "abc123" or current timestamp without randomness.
Use secure random salts, unique per password, and let libraries manage them when possible.
Practical Recommendations
For beginner backend developers, follow these simple rules.
Password Storage Rules
- Never store plain text passwords.
- Never use reversible encryption for passwords.
- Always use a password hashing function such as bcrypt, scrypt, Argon2, or PBKDF2.
- Always use salts. Prefer libraries that manage salts for you.
- Configure hashing to be slow enough to resist brute force, but still fast enough for users.
- Plan for algorithm upgrades over time.
Suggested Algorithms
If your environment supports it:
- Prefer Argon2id where available.
- Otherwise, use bcrypt with a reasonable cost factor.
For example:
- bcrypt with cost 10 to 12, depending on your server performance.
- Test by measuring how long hashing takes on your servers. Aim for perhaps 50 to 250 milliseconds per hash for login.
Example: Data Model for User Passwords
A typical users table might have:
| Column | Type | Description |
|---|---|---|
| id | integer PK | User identifier |
| string | Unique email address | |
| password_hash | string | Full password hash (algorithm + params + salt) |
| created_at | timestamp | Creation time |
| updated_at | timestamp | Last update time |
You do not need separate columns for salt or algorithm if you use formats that encode them inside the hash string, such as bcrypt or Argon2.
How This Fits With Other Authentication Topics
In the broader authentication system you will:
- Use these hashed passwords for login and registration.
- Combine password verification with:
- Sessions or tokens (JWT, etc.).
- Additional features such as multi factor authentication.
But all of that depends on this fundamental step: store passwords correctly and safely.
Once your password storage is secure, you can build the rest of your authentication system with much more confidence.
Views: 6
KAHIBARO