Overview
Storing passwords safely requires a special kind of hashing. General-purpose hashes like MD5 and the SHA family are designed to be fast — which is exactly wrong for passwords, because speed helps attackers guess billions of candidates per second. Password storage instead uses slow, deliberately expensive functions called password-hashing functions or key derivation functions (KDFs). This lesson explains why, and introduces bcrypt, scrypt, and Argon2, in a defensive frame.
Learning objectives
- Explain why fast general-purpose hashes are unsafe for passwords.
- Define a password-hashing function / KDF and the role of a work factor.
- Compare bcrypt, scrypt, and Argon2 at a conceptual level.
- Describe memory-hardness and why it resists specialized hardware.
Core concept
A password-hashing function is a hash built specifically to store passwords. Its defining feature is that it is intentionally slow and tunable: it applies a work factor (also called a cost factor or iteration count) that controls how much computation each hash takes. You set the work factor high enough that a single legitimate login is barely affected (a few tens of milliseconds), but an attacker trying to test enormous numbers of guesses is slowed to a crawl. These functions also handle salting automatically, embedding a unique salt in their output.
The key insight: for passwords, slow is a feature. General-purpose hashes optimize for throughput; password hashes optimize for resistance to guessing.
How it works
The threat is offline guessing. If an attacker steals a password database, they can guess candidate passwords and hash each one to see if it matches — no need to interact with the live system, so rate limiting and lockouts do not apply. With a fast hash, commodity hardware (especially GPUs) can compute billions of guesses per second, so weak and even moderate passwords fall quickly.
Password-hashing functions fight this in two ways:
- Configurable work factor (CPU cost). The function repeats its internal work many times. Raising the cost makes each guess more expensive for everyone equally, so an attacker's guessing rate drops proportionally. As hardware gets faster, defenders raise the cost to keep pace.
- Memory-hardness. Some functions require a large amount of memory to compute, not just CPU time. This matters because attackers use specialized parallel hardware (GPUs, ASICs) that has abundant compute but limited fast memory per core. A memory-hard function denies them that advantage, because each parallel guess needs its own sizeable memory block.
The main modern choices:
- bcrypt — based on the Blowfish cipher's key schedule, with an adjustable cost factor (the number in
$2b$12$...is that cost, and each increment roughly doubles the work). Well-established and widely available. It is CPU-hardened but not strongly memory-hard, and it truncates very long inputs. - scrypt — deliberately memory-hard as well as CPU-costly, with tunable memory and CPU parameters, designed to resist GPU/ASIC cracking.
- Argon2 — winner of the Password Hashing Competition and the modern recommended default. It has tunable time, memory, and parallelism parameters. Its Argon2id variant is the general-purpose recommendation, balancing resistance to both GPU and side-channel attacks.
Practical example
A defender migrating an application in an isolated lab compares two storage schemes for the same leaked database:
Scheme A: store md5(password) -> fast; a GPU tests billions/sec
Scheme B: store bcrypt(cost=12, password) -> slow + salted; guesses drop by orders of magnitude
Under Scheme A, an attacker who steals the database cracks common and medium-strength passwords rapidly, because each guess costs almost nothing. Under Scheme B, the same attacker is throttled by the deliberately high per-guess cost and gains no benefit from precomputation (bcrypt salts automatically), so only very weak passwords are realistically at risk. The defender picks Scheme B and, ideally, Argon2id for a new system, tuning its memory and time parameters to the server's capacity.
[!WARNING] Never store passwords with raw MD5, SHA-1, or a single pass of SHA-256, even "with a salt." These are too fast. Use a purpose-built slow function — bcrypt, scrypt, or Argon2 — with an appropriate work factor, and let it manage the salt.
Cybersecurity relevance
Password storage is one of the most consequential design decisions in an application, and using a fast hash is a serious, common vulnerability. When breaches expose password databases, the difference between "passwords protected by Argon2 with a high cost" and "unsalted MD5" is the difference between attackers recovering almost nothing and recovering most accounts. In assessments and code review, confirming that passwords use a slow, salted KDF — and that its work factor is set appropriately and raised over time — is a standard, high-priority check. In CTF cracking challenges, recognizing a bcrypt or Argon2 hash tells you the intended password is likely weak by design, since brute-forcing a strong password against a slow hash is infeasible.
Common mistakes
- Using fast general-purpose hashes (MD5, SHA-256) for passwords, even with a salt.
- Setting the work factor too low, so the function is no longer meaningfully slow.
- Never revisiting the cost as hardware improves, leaving old parameters underpowered.
- Rolling a custom scheme (e.g. many rounds of SHA-256 by hand) instead of a vetted KDF that also handles salting.
Key takeaways
- Fast hashes are unsafe for passwords because attackers can guess billions of candidates per second offline.
- Password-hashing functions (KDFs) are deliberately slow, with a tunable work factor, and handle salting for you.
- bcrypt (CPU cost), scrypt (memory-hard), and Argon2 (tunable time/memory/parallelism, Argon2id recommended) are the modern choices.
- Memory-hardness resists specialized parallel hardware; raise the work factor over time to keep pace with faster hardware.
Check your understanding
- Why is being "slow" a desirable property for a password-hashing function but not for a general-purpose hash?
- What does memory-hardness protect against that a purely CPU-costly function does not?

