Security Best Practices

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.

September 17, 2025
25 min read
Intermediate to Advanced

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

password: "myPassword123"

Storing passwords as-is. Anyone with database access can read all passwords immediately.

❌ Simple Encoding

password: base64("myPassword123")
// Result: bXlQYXNzd29yZDEyMw==

Encoding is not encryption. It's trivially reversible and provides zero security.

⚠️ Encryption

password: AES_encrypt("myPassword123", key)

Encryption is reversible. If the key is compromised, all passwords are exposed.

⚠️ Simple Hashing

password: MD5("myPassword123")
// Result: 146e0a0d6e9a2f47...

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:

Password: "SecurePassword123"
Salt: (generated automatically)
Hash: $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewcJXSsZGt.PzWbW

$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:

1. Pre-hash with SHA-256
2. Base64 encode the result
3. Pass to bcrypt

🌐 Unicode Handling

Always normalize Unicode passwords:

password = unicodedata.normalize('NFKD', password)
// Prevents issues with different encodings

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

  1. Keep existing weak hashes temporarily
  2. When user logs in successfully:
    • Verify against old hash
    • Generate new secure hash
    • Replace old hash in database
    • Mark account as migrated
  3. After X months, force password reset for unmigrated accounts

Strategy 2: Layered Hashing

Wrap Existing Hashes

  1. Apply bcrypt/Argon2 to existing hashes:
    new_hash = bcrypt(existing_md5_hash)
  2. During verification:
    md5_result = md5(user_input)
    verify = bcrypt_verify(md5_result, stored_hash)
  3. 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:

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 Tool

Related Articles