How to Securely Store Passwords (Hint: Don't Just Use MD5)

How to Securely Store Passwords (Hint: Don't Just Use MD5)

The $200 Million Password Breach

In 2012, LinkedIn suffered a catastrophic breach. Hackers stole 6.5 million password hashes. The company thought they were safe because the passwords were hashed using SHA-1. There was just one problem: the hashes had no salt. Within days, security researchers cracked millions of passwords using rainbow tables. Accounts were compromised. Trust was shattered. The damage to LinkedIn's reputation was estimated in the hundreds of millions.

This disaster was entirely preventable. The lesson? Hashing alone is not enough. You need salt, you need slow algorithms, and you absolutely need to abandon ancient relics like MD5.

Why MD5 is a Death Sentence

MD5 (Message Digest 5) was designed in 1991. For password storage, it has three fatal flaws:

1. It is fast. MD5 can compute millions of hashes per second. That sounds good, but it is terrible for passwords. An attacker with a modern GPU can try billions of password guesses per second by brute force.

2. It is broken. Cryptographers have demonstrated "collision attacks" where two different inputs produce the same MD5 hash. While this is not directly exploitable for password cracking, it erodes trust in the algorithm.

3. Rainbow tables exist. Because MD5 is so common, attackers have precomputed databases of MD5 hashes for billions of common passwords. If your system stores the MD5 hash of "password123", an attacker just looks it up in the table. Instant crack.

The Right Way: bcrypt, scrypt, and Argon2

Modern password hashing uses "slow" algorithms specifically designed to be computationally expensive. These are called Key Derivation Functions (KDFs).

bcrypt

bcrypt is the industry standard. It includes a built-in salt (random data added to the password before hashing) and a "cost factor" that controls how slow the hashing process is. As computers get faster, you can increase the cost factor to keep the algorithm slow.

A bcrypt hash looks like this:
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

That string contains the algorithm version, the cost, the salt, and the hash itself. Everything needed to verify a password is embedded in one line.

scrypt

scrypt is even more hardcore. It is designed to be not just slow, but memory-intensive. This makes it resistant to attacks using specialized hardware like GPUs and ASICs (Application-Specific Integrated Circuits). Attackers would need massive amounts of RAM to crack passwords at scale, which is prohibitively expensive.

Argon2

Argon2 is the new king. It won the Password Hashing Competition in 2015 and is recommended by security experts worldwide. It offers three variants (Argon2i, Argon2d, Argon2id), with Argon2id being the balanced, general-purpose choice. Like scrypt, it is memory-hard. Like bcrypt, it has tunable parameters. It is the gold standard for 2025 and beyond.

Salting: The Secret Ingredient

A salt is a random string added to a password before hashing. Even if two users have the same password ("password123"), their hashes will be completely different because each has a unique salt.

Without salt:
User A: password123 → MD5 → `482c811da5d5b4bc6d497ffa98491e38`
User B: password123 → MD5 → `482c811da5d5b4bc6d497ffa98491e38`

An attacker cracks one, they crack both.

With salt:
User A: password123 + salt_A → bcrypt → `$2a$10$abc...`
User B: password123 + salt_B → bcrypt → `$2a$10$xyz...`

Now they are unique. The attacker must crack each one individually.

Step-by-Step: Storing a Password Securely

  1. User registers. They submit a password.
  2. Generate a random salt. Use a cryptographically secure random number generator. The salt should be at least 16 bytes.
  3. Hash the password using bcrypt/Argon2. Combine the password and salt, then run it through the KDF.
  4. Store the hash (and salt) in the database. Do NOT store the plaintext password. Ever.

When the user logs in:
1. Retrieve the stored hash from the database.
2. Extract the salt (bcrypt includes it in the output).
3. Hash the submitted password with the same salt.
4. Compare the new hash to the stored hash. If they match, access granted.

Common Mistakes

Using the same salt for everyone

If every user has the same salt, it is called a "pepper," and while better than nothing, it is nowhere near as secure as unique salts.

Not updating legacy systems

If you have an old system using MD5, migrate immediately. Hash the MD5 hashes again using bcrypt. Yes, you can hash a hash. This is called "peppering" or "double hashing."

Forgetting about timing attacks

When comparing hashes, use a constant-time comparison function. Do not use a simple string equality check, as timing differences can leak information.

Conclusion

Password storage is not a feature you can implement quickly. It is a liability that requires deliberate, informed design. If you are still using MD5, you are holding a ticking time bomb. Upgrade to bcrypt or Argon2, salt every password, and sleep better knowing your users are protected.