Skip to main content

Command Palette

Search for a command to run...

Secure Password Storage: Hashing Salting and Best Practices

Learn: Secure Password Storage: Hashing Salting and Best Practices

Updated
6 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Secure Password Storage: Hashing, Salting, and Best Practices

The Security Problem

Password security represents one of the most critical vulnerabilities in modern applications. When organizations store passwords in plain text, they expose millions of users to catastrophic data breaches. The consequences extend far beyond the initial compromise—attackers gain access to user accounts across multiple platforms, since many users reuse passwords.

The 2023 data breach landscape revealed that 61% of breaches involved compromised credentials. Companies like LinkedIn, Yahoo, and Equifax suffered massive reputational damage and legal consequences after password mishandling. Plain text storage is inexcusable in 2024, yet many legacy systems still maintain this dangerous practice.

Beyond external attackers, internal threats pose significant risks. Database administrators, developers, and support staff shouldn't access user passwords. Proper hashing ensures that even system administrators cannot retrieve original passwords, implementing the principle of least privilege effectively.

How Attacks Work

Dictionary and Brute Force Attacks

Attackers employ multiple strategies to crack passwords. Dictionary attacks use common password lists, testing thousands of combinations per second. Brute force attacks systematically try every possible character combination. Without proper protection mechanisms, modern GPUs can test billions of password combinations hourly.

Rainbow Tables

Rainbow tables are precomputed databases of password hashes. Attackers generate hashes for millions of common passwords, then compare stolen hashes against these tables. A single rainbow table lookup takes microseconds, making unsalted hashes vulnerable regardless of computational complexity.

GPU and ASIC Acceleration

Graphics processing units excel at parallel computation, testing multiple password candidates simultaneously. Specialized ASIC hardware designed specifically for hashing can test trillions of combinations daily. MD5 and SHA-1, once considered secure, now fall to GPU attacks in minutes.

Credential Stuffing

When one service experiences a breach, attackers test stolen credentials against other platforms. This attack succeeds because users reuse passwords. Proper hashing prevents attackers from using stolen hashes across services, containing breach damage.

Protection Mechanisms

Hashing Fundamentals

Hashing converts passwords into fixed-length strings through one-way mathematical functions. Unlike encryption, hashing cannot be reversed—you cannot derive the original password from a hash. This property ensures that even database administrators cannot access user passwords.

Cryptographic hash functions must satisfy three properties:

  • Deterministic: identical inputs always produce identical outputs
  • Quick computation: hashing completes in milliseconds
  • Avalanche effect: tiny input changes produce completely different outputs

Salting Strategy

Salts are random values added to passwords before hashing. Each user receives a unique salt, preventing identical passwords from producing identical hashes. Salts defeat rainbow tables entirely—attackers cannot precompute hashes for millions of passwords when each requires a different salt.

Effective salts must be:

  • Cryptographically random: generated using secure random number generators
  • Sufficiently long: minimum 16 bytes (128 bits) recommended
  • Unique per user: never reuse salts across accounts
  • Stored alongside hashes: salts don't require secrecy, only uniqueness

Modern Hashing Algorithms

Bcrypt implements the Blowfish cipher with built-in salting and configurable work factors. Each bcrypt hash includes the salt, work factor, and algorithm version, making it self-contained and portable.

Argon2 won the Password Hashing Competition in 2015, offering superior resistance to GPU attacks. It requires configurable memory, time, and parallelism parameters, making it intentionally slow and resource-intensive. Argon2id combines Argon2i and Argon2d strengths.

PBKDF2 applies a pseudorandom function repeatedly, with iteration counts reaching 600,000+. While slower than bcrypt, PBKDF2 remains acceptable when properly configured.

Scrypt combines salting, hashing, and key derivation with memory-hard properties, resisting GPU acceleration effectively.

Avoid MD5, SHA-1, and unsalted SHA-256—these lack the computational expense necessary for password security.

Implementation Guide

Step 1: Choose Your Algorithm

Select Argon2id for new applications—it represents the current security standard. Bcrypt serves as an excellent alternative with broader language support. Never implement custom hashing algorithms.

Step 2: Generate Cryptographic Salts

Use your language's cryptographically secure random generator:

  • Python: secrets.token_bytes()
  • Node.js: crypto.randomBytes()
  • Java: SecureRandom
  • PHP: random_bytes()

Step 3: Configure Work Factors

Adjust computational parameters to match your infrastructure:

  • Bcrypt: cost factor 12-14 (2024 standard)
  • Argon2: memory=65536 KB, time=3, parallelism=4
  • PBKDF2: iterations=600,000+

Benchmark on your production hardware—hashing should require 100-500ms per password.

Step 4: Hash During Registration

Hash passwords immediately upon user registration, never storing plain text temporarily.

Step 5: Verify During Authentication

Compare submitted passwords against stored hashes using constant-time comparison functions to prevent timing attacks.

Step 6: Implement Password Upgrade Paths

When users log in with outdated hashing algorithms, rehash their passwords with current standards.

Code Examples

Python with Argon2

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

hasher = PasswordHasher()

# Registration
password = "user_provided_password"
hashed = hasher.hash(password)
# Store hashed in database

# Authentication
try:
    hasher.verify(hashed, password)
    print("Password correct")
except VerifyMismatchError:
    print("Password incorrect")

Node.js with Bcrypt

const bcrypt = require('bcrypt');

// Registration
const password = 'user_provided_password';
const saltRounds = 12;
const hashed = await bcrypt.hash(password, saltRounds);
// Store hashed in database

// Authentication
const isValid = await bcrypt.compare(password, hashed);
if (isValid) {
    console.log('Password correct');
} else {
    console.log('Password incorrect');
}

Java with Argon2

import de.mkammerer.argon2.Argon2Factory;
import de.mkammerer.argon2.Argon2;

Argon2 argon2 = Argon2Factory.create();

// Registration
String password = "user_provided_password";
String hashed = argon2.hash(2, 65536, 1, password);
// Store hashed in database

// Authentication
boolean isValid = argon2.verify(hashed, password);
if (isValid) {
    System.out.println("Password correct");
} else {
    System.out.println("Password incorrect");
}

PHP with Argon2

// Registration
$password = $_POST['password'];
$hashed = password_hash($password, PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,
    'time_cost' => 3,
    'threads' => 4
]);
// Store $hashed in database

// Authentication
if (password_verify($_POST['password'], $hashed)) {
    echo "Password correct";
} else {
    echo "Password incorrect";
}

Testing Security

Unit Tests

def test_password_hashing():
    password = "TestPassword123!"
    hashed = hash_password(password)

    # Different hashes for same password (due to salt)
    hashed2 = hash_password(password)
    assert hashed != hashed2

    # Correct password verifies
    assert verify_password(password, hashed)

    # Incorrect password fails
    assert not verify_password("WrongPassword", hashed)

Performance Benchmarking

import time

start = time.time()
for _ in range(100):
    hash_password("test_password")
elapsed = time.time() - start

# Should require 100-500ms per hash
assert elapsed / 100 > 0.1, "Hashing too fast—increase work factor"

Security Audits

  • Verify no plain text passwords in logs or error messages
  • Confirm salts are cryptographically random
  • Test constant-time comparison functions
  • Validate work factors match current standards
  • Review password reset mechanisms for vulnerabilities

Common Mistakes

Mistake 1: Insufficient Work Factors

Setting bcrypt cost to 4 or PBKDF2 iterations to 1,000 leaves passwords vulnerable. Modern standards require bcrypt cost 12+, PBKDF2 iterations 600,000+, and Argon2 with substantial memory requirements.

Mistake 2: Reusing Salts

Using the same salt for multiple users defeats the entire purpose. Each password requires a unique, random salt.

Mistake 3: Storing Passwords Temporarily

Never store plain text passwords in logs, error messages, or temporary variables. Hash immediately upon receipt.

Mistake 4: Weak Random Number Generators

Using Math.random(), rand(), or mt_rand() produces predictable salts. Always use cryptographically secure generators.

Mistake 5: Comparing Hashes Incorrectly

String comparison functions terminate early upon mismatch, leaking timing information. Use constant-time comparison: hash_equals() in PHP, hmac.compare_digest() in Python.

Mistake 6: Ignoring Algorithm Upgrades

As computing power increases, previously secure algorithms become vulnerable. Implement upgrade paths allowing password rehashing during login.

Mistake 7: Mixing Hashing with Encryption

Encryption is reversible; hashing is not. Never encrypt passwords—always hash them.

Summary

Secure password storage represents a non-negotiable security requirement. The combination of modern hashing algorithms, cryptographic salts, and appropriate work factors creates a defense-in-depth approach that protects user accounts even when databases are compromised.

Key takeaways:

  • Never store plain text passwords—use Argon2id or bcrypt exclusively
  • Generate unique cryptographic salts for each password
  • Configure appropriate work factors requiring 100-500ms per hash
  • Use constant-time comparison during authentication
  • Implement upgrade paths for legacy password hashes
  • Test security thoroughly with unit tests and benchmarks
  • Avoid common mistakes that undermine protection mechanisms

Organizations implementing these practices significantly reduce breach impact, protect user privacy, and demonstrate security maturity. Password security forms the foundation of application security—invest in it properly.