In this guide
Your project has user accounts. Registration takes a password; login checks it. The obvious implementation — store the password, compare at login — is also a catastrophe waiting for a database leak: one backup file, one misconfigured server, and every user's password is exposed. And users reuse passwords, so your leak compromises their other accounts too.
The fix is password hashing: store a one-way transformation of the password, never the password itself. But "hash the password" hides a decade of hard lessons — MD5 and SHA-256 are the wrong tools, salt is non-negotiable, and the work factor is the actual security. This guide explains what password hashing must achieve, why general-purpose hashes fail, how bcrypt and Argon2 work conceptually, and the integration patterns that keep student projects safe.
What password hashing must achieve
Three properties, all non-negotiable:
- One-way. Given the hash, recovering the password must be computationally infeasible. (Encryption is reversible by design — it is the wrong tool.)
- Unique per user (salting). Two users with the same password must get different hashes; otherwise one cracked password reveals every matching account, and precomputed tables crack entire databases at once.
- Slow (work factor). Hashing must be deliberately expensive — tunable so it stays expensive as hardware improves. Legitimate logins pay milliseconds; an attacker guessing billions of passwords pays years.
If your scheme lacks any one of these, it is not password hashing — it is decoration.
Why MD5/SHA-256 are wrong for passwords
Fast hashes (MD5, SHA-1, SHA-256) are designed for speed — checksums, file integrity, blockchains. That speed is exactly what password cracking wants: commodity GPUs compute billions of SHA-256 hashes per second, which turns an 8-character password into minutes of work. Salting a fast hash fixes the precomputation problem but not the speed problem.
Password hashing needs the opposite design: memory-hard, deliberately slow, tunable. That is what bcrypt and Argon2 are.
Note: you will still find tutorials hashing passwords with a single round of SHA-256. They are outdated and dangerous. If your project currently does this, migrating to bcrypt or Argon2 is one of the highest-value security fixes you can make.
bcrypt: the battle-tested standard
bcrypt (from 1999, based on the Blowfish cipher's key schedule) has protected passwords for over two decades. Conceptually:
- Built-in salt: every hash embeds a random salt automatically — you never manage salts yourself.
- Cost factor: a tunable parameter (the "work factor") sets how expensive each hash is; increase it over the years as hardware improves.
- Self-describing output: the hash string encodes the algorithm, cost, salt, and result together, so verification needs only the stored string and the candidate password.
Minimal usage with a standard library (Python shown; every language has an equivalent):
import bcrypt
# Registration: hash once, store the resulting string
stored = bcrypt.hashpw(b"user-chosen-password", bcrypt.gensalt(rounds=12))
# Login: compare candidate against stored hash (constant-time)
if bcrypt.checkpw(b"candidate-password", stored):
print("authenticated")
Two details that matter: use the library's checkpw-style comparison (it is timing-safe), and pick the cost factor so hashing takes on the order of a few hundred milliseconds on your server — noticeable to no one at login, punishing at cracking scale.
Argon2: the modern choice
Argon2 won the Password Hashing Competition (2015) and is the current recommended default. Its key advance over bcrypt is memory-hardness: it requires significant RAM per hash, which defeats the GPUs and custom chips that parallelize compute-bound cracking. Three variants exist — Argon2id (the recommended hybrid) blends data-dependent and data-independent passes.
| bcrypt | Argon2id | |
|---|---|---|
| Age / track record | Decades of production use | Newer (2015), competition winner |
| GPU resistance | Good (cost factor) | Stronger (memory-hard) |
| Tuning knobs | Cost factor | Memory, iterations, parallelism |
| Recommendation | Solid, widely supported | Preferred for new projects |
For a student project, either is defensible; Argon2id is the forward-looking pick, bcrypt the maximum-compatibility pick. What is not defensible is MD5, SHA-256, or no hashing.
Integration patterns that matter
Never cap password length short. Some old bcrypt implementations truncate at 72 bytes — know your library's behavior, and prefer libraries without the limit.
Pepper optionally, salt automatically. A pepper (a secret value added server-side, stored separately from the database) adds defense if the database leaks but the application config does not. It is a bonus layer — the salt, which bcrypt/Argon2 handle for you, is the requirement.
Hash on the server, not the client. Client-side hashing just makes the hash become the password — anyone who steals the hash can replay it. The server must do the hashing.
Rate-limit login attempts. Hashing being slow does not stop online guessing against your login endpoint. Add attempt limits and delays; hashing protects the database leak scenario, rate limiting protects the live endpoint scenario. They defend different attacks.
Plan for cost-factor upgrades. Store hashes in the self-describing format and, at each successful login, check whether the stored cost is below your current target — if so, rehash with the new cost transparently. Security that upgrades itself.
Migration: fixing an existing project
If your project already stores weak hashes (or plaintext — fix that first):
- Add a column/flag marking the hash scheme per user.
- At each login, verify against the old scheme, then immediately rehash with bcrypt/Argon2 and update the flag.
- Users who never log in keep the old hash — consider forcing a password reset after a grace period.
This migrates the user base without a flag day, and it is a genuinely good story in a project report: you identified the weakness and engineered the transition.
Tuning the work factor and planning upgrades
The work factor is not a magic number — it is a budget decision. The method:
- Benchmark on your actual server hardware. Time a single hash at several cost settings. Aim for roughly a quarter to half a second per hash on production hardware — imperceptible at login, expensive at scale.
- Remember hardware improves. A cost factor chosen today will feel cheap in three years. Schedule a periodic review (yearly is fine) rather than setting and forgetting.
- Rehash transparently. Because modern hash strings encode their own parameters, your login code can detect an outdated cost factor at each successful login and rehash the password with the current target. Users upgrade silently; attackers face the new cost across the whole database.
- Argon2 has three knobs (memory, iterations, parallelism) rather than bcrypt's one. Published guidance suggests starting values, but the principle is identical: benchmark on your hardware, pick the slowest settings your login latency budget allows, and revisit periodically. Memory-hardness means the benchmark should also watch RAM usage under concurrent logins — a setting that is fine for one login can exhaust a small server under load.
Also plan the failure mode: if hashing ever becomes a bottleneck (traffic spike, tiny server), the answer is never to lower the work factor under pressure — it is to fix capacity. The work factor is a security parameter, not a performance dial.
Common mistakes checklist
- Fast hash (MD5/SHA-256) for passwords. Wrong tool; use bcrypt or Argon2.
- No salt / hand-rolled salt scheme. Use the library's built-in salting.
- Client-side hashing as the defense. The server must hash.
- Cost factor left at the minimum. Tune it to cost real time on your hardware.
- No rate limiting on login. Hashing does not stop online guessing.
- Plaintext or reversible "encryption" anywhere — logs, backups, admin views.
- Passwords in git history. Rotate anything ever committed; git history is forever.
Where to go from here
- JWT authentication explained for students — what happens after the password verifies: sessions and tokens.
- OWASP Top 10 for student web apps — credential handling in the broader vulnerability landscape.
- SQL vs NoSQL: which database for your project — choose and harden the data layer that will hold your password hashes.
- More secure-web builds in the Web Development branch hub.