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
- Use bcrypt (cost 12+) or Argon2id — pick one and use the library
- Never store plaintext, MD5, SHA-1, or encrypted passwords
- Enforce minimum length of 12 characters; allow up to 128+
- Check new passwords against HaveIBeenPwned on registration
- Remove maximum length limits
- Remove periodic rotation requirements; enforce rotation only on suspected breach
- 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>