Why Most Developers Store Passwords Wrong (and How to Fix It)

posted 4 min read

Password security is one of those topics where almost every developer knows the basics — and still gets it subtly wrong under deadline pressure. This post covers the most common mistakes, what to do instead, and how to think about password strength in your own projects.

The Mistakes That Keep Happening

1. MD5 and SHA-1 for Password Hashing

MD5 and SHA-1 are not password hashing algorithms. They are checksums designed to be fast — which is exactly wrong for passwords. A modern GPU can compute billions of MD5 hashes per second. The entire rockyou.txt dataset (14 million passwords) can be cracked against an MD5 database in under a minute on consumer hardware.

If your codebase contains md5($password) or sha1($password), fix it now.

2. Unsalted Hashes

Even bcrypt can be weakened if you hash without a salt. A salt is a random value prepended to each password before hashing, ensuring that two users with the same password produce different hash values. Without it, an attacker can precompute a rainbow table once and crack your entire database.

Modern libraries handle salting automatically. There is no reason to manage salts manually.

3. Rolling Your Own Crypto

If you are writing a password hashing function from scratch, stop. The only people who should write cryptographic primitives are people who have spent years studying them and who will have their work peer-reviewed. Use a library.

4. Encrypting Passwords Instead of Hashing Them

Encryption is reversible. Hashing is not. If you are encrypting passwords (even with AES), you have a key management problem: whoever has the decryption key has every user's password. A breach of the key means a breach of all passwords.

Hash passwords. Never encrypt them.

What to Use Instead

bcrypt

The gold standard for most applications. It is slow by design and has a cost factor you can increase as hardware gets faster. Available in every major language:

``python

Python

import bcrypt

Hash a password

hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

Verify

bcrypt.checkpw(password.encode(), hashed)

`

`javascript
// Node.js


const bcrypt = require('bcrypt');

const hash = await bcrypt.hash(password, 12);

const match = await bcrypt.compare(password, hash);

`

<code>php <p>// PHP (built-in since PHP 5.5)</p> <p>$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);</p> <p>$valid = password_verify($password, $hash);</p> </code>

Argon2

Newer than bcrypt, winner of the 2015 Password Hashing Competition, and the current recommendation in many security guidelines. It has three variants — Argon2d, Argon2i, Argon2id. Use Argon2id for password hashing.

`python
Python (argon2-cffi)
from argon2 import PasswordHasher

ph = PasswordHasher()

hash = ph.hash(password)

ph.verify(hash, password)

`

Scrypt

Memory-hard like Argon2. Available in Python's standard library since 3.6 and in Node.js's built-in crypto module:

<code>javascript <p>// Node.js (built-in)</p> <p>const { scrypt, randomBytes } = require('crypto');</p> <p>const salt = randomBytes(16);</p> <p>scrypt(password, salt, 64, (err, derivedKey) => {</p> <p>// store salt + derivedKey</p> <p>});</p> </code>

Password Strength: What Actually Matters

When your users create passwords, what you enforce shapes the strength of your database. The most important factors are:

Length beats complexity every time. A 20-character password using only lowercase letters has more entropy than a 10-character password using uppercase, lowercase, numbers, and symbols. Entropy grows exponentially with length. Stop enforcing maximum password lengths (yes, this is still common — it is almost always wrong).

Avoid the useless complexity rules. "Must contain a capital letter and a number" trains users to write Password1! — technically complex, trivially guessable. Instead, enforce minimum length (12+ characters) and check against a breach database.

Check against breach databases. The Have I Been Pwned API provides a free, privacy-preserving k-anonymity endpoint to check if a password has appeared in a known breach. Reject any password that has been compromised.

Do not enforce password rotation. Periodic password rotation (e.g. every 90 days) was official guidance for years and has been reversed by NIST (SP 800-63B, 2017). It causes users to make predictable changes (Summer2026!Autumn2026!) and does not improve security unless there is evidence of compromise.

Practical Recommendations for Your Auth System

  1. Use bcrypt (cost 12+) or Argon2id — pick one and use the library
  2. Never store plaintext, MD5, SHA-1, or encrypted passwords
  3. Enforce minimum length of 12 characters; allow up to 128+
  4. Check new passwords against HaveIBeenPwned on registration
  5. Remove maximum length limits
  6. Remove periodic rotation requirements; enforce rotation only on suspected breach
  7. Log failed login attempts and rate-limit or lock accounts after N failures

Testing Password Strength in Your UI

If you want to show users how strong their password is during signup, implement client-side entropy calculation rather than pattern matching. A simple entropy formula:

<code> <p>entropy = password_length × log2(character_pool_size)</p> </code>`

Where character pool size is the number of distinct character types used (e.g. lowercase = 26, + digits = 36, + uppercase = 62, + symbols ≈ 94).

For reference:

  • Under 40 bits: weak
  • 40–79 bits: fair
  • 80–127 bits: strong
  • 128+ bits: very strong

A 16-character fully mixed password sits around 105 bits — effectively uncrackable by brute force.

If you need to generate test passwords or demonstrate strength calculations to your team, SnappyTools' Password Generator shows live entropy and a strength label for every generated password, entirely in the browser.


What auth libraries are you using in your stack? Happy to discuss specific framework configurations in the comments.

Originally published at https://snappytools.app/password-generator/</a></em></p>

1 Comment

1 vote

More Posts

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelskiverified - Mar 19

Comparison: Universal Import vs. Plaid/Yodlee

Pocket Portfolioverified - Mar 12

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

Why most people quit AWS

Ijay - Feb 3

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelskiverified - Apr 23
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

10 comments
2 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!