Database Security: Hashing Passwords Correctly
Learn the critical difference between secure and insecure password storage. Discover why proper hashing with bcrypt, scrypt, or Argon2 is essential for protecting user credentials and how to implement it correctly.
Introduction
In the world of database security, few topics are as critical—and as frequently misunderstood—as password hashing. Every year, millions of user accounts are compromised due to poor password storage practices, with devastating consequences for both users and organizations. Yet the solution has been well-established for decades: proper password hashing.
This article isn't just about theory. It's about understanding the real-world implications of password storage decisions that affect billions of users worldwide. Whether you're a developer building your first authentication system, a security professional auditing existing systems, or simply someone curious about how your passwords are (or should be) protected, this guide will provide the knowledge you need to implement or advocate for secure password storage.
The Problem: Why Passwords Need Special Treatment
Passwords are unique among the data we store. Unlike credit card numbers that can be replaced or addresses that can be changed, passwords represent a fundamental security credential that users often reuse across multiple services. When passwords are compromised, the damage extends far beyond a single application.
The Catastrophic Consequences of Poor Password Storage
⚠️ Real-World Breaches and Their Impact
- LinkedIn (2012): 117 million passwords stored as unsalted SHA-1 hashes - cracked within hours
- Adobe (2013): 153 million passwords encrypted (not hashed) with 3DES - passwords recovered en masse
- Yahoo (2014): 500 million accounts with MD5 hashed passwords - most recovered through rainbow tables
- Facebook (2019): Hundreds of millions of passwords stored in plaintext logs - readable by employees
These breaches affected billions of users and resulted in countless secondary attacks on other services.
Common Password Storage Mistakes
❌ Plaintext Storage
Storing passwords as-is. Anyone with database access can read all passwords immediately.
❌ Simple Encoding
Encoding is not encryption. It's trivially reversible and provides zero security.
⚠️ Encryption
Encryption is reversible. If the key is compromised, all passwords are exposed.
⚠️ Simple Hashing
Fast hashes like MD5/SHA-1 are vulnerable to rainbow tables and brute force attacks.
Understanding Password Hashing
Password hashing is a specialized application of cryptographic hash functions designed specifically for password storage. Unlike general-purpose hash functions that prioritize speed, password hashing functions are intentionally slow and resource-intensive.
What Makes a Good Password Hash Function?
Essential Properties
1. Computational Cost (Slowness)
Must be slow enough to make brute force attacks impractical, typically taking 50-500ms per hash.
2. Memory Hardness
Requires significant memory to compute, preventing parallel attacks using GPUs or ASICs.
3. Salt Integration
Built-in salt handling to prevent rainbow table attacks without manual implementation.
4. Configurable Cost
Adjustable difficulty to maintain security as hardware improves over time.
The Anatomy of a Secure Password Hash
bcrypt Example:
$2b$ - Algorithm identifier (bcrypt)
12 - Cost factor (2^12 iterations)
LQv3c1yqBWVHxkd0LHAkCO - Salt (128 bits, base64 encoded)
Yz6TtxMQJqhN8/LewcJXSsZGt.PzWbW - Hash value (184 bits, base64 encoded)
Modern Password Hashing Algorithms
bcrypt: The Industry Standard
bcrypt (1999)
✅ Recommended- Based on: Blowfish cipher
- Default cost: 10-12 (adjust based on your security requirements)
- Memory usage: 4 KB
- Max password length: 72 bytes
- Widely supported: Available in all major programming languages
Best for: Most web applications, especially when wide compatibility is needed
scrypt: Memory-Hard Security
scrypt (2009)
✅ Recommended- Designed for: Memory-hardness against ASIC attacks
- Parameters: N (cost), r (blocksize), p (parallelization)
- Memory usage: 128 * N * r * p bytes (configurable)
- Typical settings: N=16384, r=8, p=1 (16 MB memory)
- Performance: Slower than bcrypt but more resistant to hardware attacks
Best for: High-security applications, cryptocurrency wallets, situations where ASIC resistance is crucial
Argon2: The Modern Choice
Argon2 (2015)
✅ Highly Recommended- Winner: Password Hashing Competition (2015)
- Variants:
- Argon2i - Optimized against side-channel attacks
- Argon2d - Optimized against GPU attacks
- Argon2id - Hybrid (recommended)
- Memory usage: Configurable (typically 64 MB+)
- Parallelism: Multi-threaded support
- Future-proof: Designed with quantum resistance in mind
Best for: New applications, maximum security requirements, future-proofing
Comparison Table
Algorithm | Year | Memory Hard | GPU Resistant | Adoption | Recommendation |
---|---|---|---|---|---|
bcrypt | 1999 | Limited | Moderate | Excellent | ✅ Use |
scrypt | 2009 | Yes | High | Good | ✅ Use |
Argon2id | 2015 | Yes | Excellent | Growing | ✅ Preferred |
PBKDF2 | 2000 | No | Low | Excellent | ⚠️ Legacy |
SHA-256 | 2001 | No | None | Excellent | ❌ Avoid |
MD5 | 1992 | No | None | Legacy | ❌ Never |
Implementation Guide
Step 1: Choose the Right Algorithm
Decision Tree
→ New application? Use Argon2id
→ Need maximum compatibility? Use bcrypt
→ High-security requirements? Use Argon2id or scrypt
→ Limited memory available? Use bcrypt
→ Migrating from MD5/SHA? Implement dual-hashing strategy (see below)
Step 2: Implement Correctly
Example Implementations
PHP with bcrypt:
// Hashing a password $password = 'UserPassword123'; $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); // Verifying a password if (password_verify($password, $hash)) { // Password is correct // Check if rehashing is needed (algorithm or cost change) if (password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 12])) { $newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); // Update database with new hash } }
Python with Argon2:
from argon2 import PasswordHasher ph = PasswordHasher( time_cost=2, # iterations memory_cost=65536, # 64MB parallelism=2 ) # Hashing hash = ph.hash("UserPassword123") # Verifying try: ph.verify(hash, "UserPassword123") # Check if rehash needed if ph.check_needs_rehash(hash): new_hash = ph.hash("UserPassword123") except VerifyMismatchError: # Invalid password
Node.js with bcrypt:
const bcrypt = require('bcrypt'); // Hashing const saltRounds = 12; const hash = await bcrypt.hash('UserPassword123', saltRounds); // Verifying const match = await bcrypt.compare('UserPassword123', hash); if (match) { // Password is correct }
Step 3: Handle Edge Cases
🔒 Password Length Limits
bcrypt has a 72-byte limit. For longer passwords:
🌐 Unicode Handling
Always normalize Unicode passwords:
Migration Strategies
If you're currently using weak hashing (MD5, SHA-1, plain SHA-256), you need a migration strategy that doesn't require users to reset passwords.
Strategy 1: Gradual Migration
Rehash on Login
- Keep existing weak hashes temporarily
- When user logs in successfully:
- Verify against old hash
- Generate new secure hash
- Replace old hash in database
- Mark account as migrated
- After X months, force password reset for unmigrated accounts
Strategy 2: Layered Hashing
Wrap Existing Hashes
- Apply bcrypt/Argon2 to existing hashes: new_hash = bcrypt(existing_md5_hash)
- During verification: md5_result = md5(user_input)
verify = bcrypt_verify(md5_result, stored_hash) - Gradually migrate to pure bcrypt as users log in
Common Pitfalls and How to Avoid Them
❌ Pitfall: Using the Same Salt for All Passwords
A global salt defeats the purpose. Each password needs a unique salt.
Solution: Use libraries that handle salt generation automatically (bcrypt, Argon2)
❌ Pitfall: Storing Salt Separately
Modern algorithms include the salt in the hash output. Separate storage adds complexity without benefit.
Solution: Store the complete hash output (algorithm + salt + hash) as a single field
⚠️ Pitfall: Client-Side Hashing
Hashing passwords in JavaScript before sending them essentially makes the hash the password.
Solution: Always hash server-side. Use HTTPS for transport security.
⚠️ Pitfall: Not Adjusting Cost Over Time
Hardware gets faster. A cost factor appropriate in 2015 may be too low today.
Solution: Review and increase cost factors annually. Use password_needs_rehash() or equivalent.
Performance Considerations
Balancing Security and User Experience
Recommended Timing Targets
Standard Web App
50-100ms
Imperceptible to users
High Security
250-500ms
Noticeable but acceptable
Critical Systems
500ms-1s
Maximum protection
Mitigating DoS Attacks
⚡ Resource Exhaustion Protection
Slow hashing can be exploited for denial-of-service attacks. Implement these protections:
- Rate limiting: Limit login attempts per IP/account
- CAPTCHA: Add after N failed attempts
- Queue system: Process password operations asynchronously
- Proof of work: Require client-side computation before accepting requests
Compliance and Standards
Regulatory Requirements
Regulation | Password Storage Requirements | Recommended Algorithm |
---|---|---|
GDPR | "State of the art" protection, pseudonymization | Argon2id, bcrypt (12+) |
PCI DSS 4.0 | Strong cryptography, salted hashing | bcrypt, scrypt, Argon2 |
NIST 800-63B | Key derivation function with salt | Argon2, PBKDF2 (10,000+ iterations) |
HIPAA | Encryption and integrity controls | Any NIST-approved algorithm |
Testing and Validation
Security Testing Checklist
✓ Essential Tests
Conclusion
Password hashing is not just a technical implementation detail—it's a fundamental security control that protects your users' most sensitive credential. The difference between proper and improper password storage can mean the difference between a minor security incident and a catastrophic breach affecting millions.
Key Takeaways
✅ Do:
- • Use bcrypt, scrypt, or Argon2id
- • Let libraries handle salt generation
- • Adjust cost factors over time
- • Implement rate limiting
- • Plan migration strategies
❌ Don't:
- • Use MD5, SHA-1, or plain SHA-256
- • Store passwords in plaintext or encoded
- • Implement your own crypto
- • Use the same salt for all users
- • Hash on the client side
Remember: Your users trust you with their passwords. Many will reuse these passwords across multiple services, making your security decisions impact their entire digital life. There's no excuse for weak password hashing in modern applications—the tools are freely available, well-documented, and battle-tested.
🚀 Ready to Implement?
Start with these resources:
- Test password hashing with our bcrypt tool
- Generate secure passwords with our password generator
- Learn about hash functions with our SHA-256 calculator
- Review OWASP's Password Storage Cheat Sheet
Further Reading
Continue your security education with these topics:
- Session management and authentication tokens
- Multi-factor authentication implementation
- Secure password reset flows
- Account lockout and brute force protection
- Password policy best practices
- Zero-knowledge password proof systems
Try It Yourself!
Ready to experiment with bcrypt Hash Tool? Use our interactive tool to encrypt and decrypt your own messages.
Use bcrypt Hash ToolRelated Articles
Hash Functions Explained: Why MD5 Isn't Secure
Understand the fundamentals of hash functions and their vulnerabilities.
Securing Your Digital Life: A Complete Privacy Guide
Apply password security knowledge to protect your personal accounts.
Choosing the Right Encryption for Your Needs
Learn how to select appropriate security algorithms for different use cases.