Password Security: Generation, Hashing, and Storage Best Practices
Password security is one of the most critical responsibilities in web development. A single breach can expose millions of users. This guide covers the entire password lifecycle — from generating strong passwords to hashing and storing them securely.
Password Entropy: What Makes a Password Strong?
Password strength is measured in bits of entropy — the number of binary decisions an attacker must make to guess it. Higher entropy = harder to crack.
Entropy = log₂(N^L)
Where N is the character pool size and L is the password length.
| Password Type | Pool Size (N) | Length (L) | Entropy (bits) |
|---|---|---|---|
| Lowercase only | 26 | 8 | 37.6 |
| Mixed case + digits | 62 | 8 | 47.6 |
| All printable ASCII | 95 | 12 | 78.8 |
| All printable ASCII | 95 | 16 | 105.1 |
A minimum of 72 bits of entropy is recommended for passwords protecting sensitive accounts. For high-security applications, aim for 90+ bits.
Generate secure passwords instantly: Our Password Generator creates cryptographically random passwords with configurable length, character sets, and entropy display.
Generating Strong Passwords
Rule 1: Use Cryptographic Randomness
Never generate passwords with Math.random(). It uses a pseudo-random number generator (PRNG) that is predictable:
// ❌ INSECURE — predictable PRNG
function badPassword(length) {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}
// ✅ SECURE — cryptographic randomness
function goodPassword(length) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
const array = new Uint32Array(length);
crypto.getRandomValues(array);
return Array.from(array, (n) => chars[n % chars.length]).join('');
}
Rule 2: Minimum 16 Characters
Shorter passwords are exponentially easier to crack. NIST SP 800-63B recommends supporting passwords up to 64 characters and requiring a minimum of 8 — but security experts recommend 16+ characters for any sensitive application.
Rule 3: Include All Character Classes
Use uppercase, lowercase, digits, and special characters. Each added character class significantly increases the search space an attacker must cover.
Hashing: bcrypt, Argon2, and scrypt
Never store passwords in plaintext. Always hash them with a slow, salted hashing algorithm designed specifically for passwords.
bcrypt
The industry standard for over 20 years. Uses the Blowfish cipher with a configurable cost factor:
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12); // cost factor 12
const valid = await bcrypt.compare(input, hash);
Hash and verify bcrypt passwords with our Bcrypt Generator & Verifier.
Argon2id
The winner of the 2015 Password Hashing Competition. Memory-hard design makes GPU attacks expensive:
import argon2 from 'argon2';
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 4,
});
Which to Choose?
| Algorithm | Recommendation |
|---|---|
| Argon2id | Best choice for new projects |
| bcrypt | Excellent, widely supported |
| scrypt | Good alternative when Argon2 unavailable |
| PBKDF2 | Acceptable, but weaker than above |
| SHA-256, MD5 | Never use for passwords |
For a deeper comparison, read our bcrypt vs SHA-256 article.
Secure Storage Practices
1. Hash on the Server, Not the Client
Client-side hashing provides no security benefit — an attacker who intercepts the hash can replay it. Always hash passwords server-side over HTTPS.
2. Never Log Passwords
Ensure password fields are excluded from request logging, error reports, and analytics:
// Express middleware — redact passwords from logs
app.use((req, res, next) => {
if (req.body?.password) {
req.body.password = '[REDACTED]';
}
next();
});
3. Implement Rate Limiting
Limit login attempts to prevent brute-force attacks. A common strategy:
- 5 failed attempts → 30-second lockout
- 10 failed attempts → 15-minute lockout
- 20 failed attempts → Account lockout requiring email verification
4. Use HMAC for API Authentication
For API-to-API authentication, use HMAC (Hash-based Message Authentication Code) instead of sending raw API keys:
import { createHmac } from 'crypto';
const signature = createHmac('sha256', secretKey)
.update(message)
.digest('hex');
Generate and verify HMAC signatures with our HMAC Generator.
Password Policies: NIST Recommendations
NIST SP 800-63B (2024 update) provides modern password guidelines:
| Practice | Recommendation |
|---|---|
| Minimum length | 8 characters (15+ recommended) |
| Maximum length | At least 64 characters |
| Complexity rules | Do not require specific character classes |
| Password expiration | Do not force periodic changes |
| Blocklist | Check against known breached passwords |
| MFA | Always offer multi-factor authentication |
The surprising shift: NIST now recommends against forced complexity rules (Must include uppercase, number, and symbol). These rules lead to predictable patterns like Password1! and don’t meaningfully increase security.
Further Reading
- bcrypt vs SHA-256: Password Hashing Compared
- Encoding and Hashing Guide
- Why Base64 is Not Encryption
This article is part of our Encoding and Hashing Guide series.