Security • ~11 min read

Salting and Hashing: Password Security Best Practices

Storing passwords securely is critical for protecting user accounts. This guide covers cryptographic hashing, salt generation, rainbow table attacks, and modern algorithms like bcrypt and Argon2 for bulletproof password storage.

Why hash passwords?

Storing passwords in plaintext is a catastrophic security failure. When a database is breached, attackers gain instant access to all user accounts. Hashing transforms passwords into irreversible digests, so even if your database is compromised, the original passwords remain protected.

High-profile breaches (LinkedIn 2012, Adobe 2013, Yahoo 2013-2014) exposed millions of inadequately protected passwords. Proper hashing and salting would have prevented mass credential theft.

What is hashing?

A cryptographic hash function takes an input (password) and produces a fixed-size output (hash/digest). Hash functions are designed to be:

Example

Input: "password123"

SHA-256: "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"

Input: "password124"

SHA-256: "8bb6118f8fd6935ad0876a3be34a717d32708ffd27258c28e26e0a03f7e55c12"

Note how a single character change produces a completely different hash.

Rainbow table attacks

A rainbow table is a precomputed database mapping common passwords to their hashes. Attackers can instantly "crack" hashed passwords by looking them up in the table.

How the attack works

  1. Attacker precomputes hashes for millions of common passwords.
  2. Database breach exposes password hashes.
  3. Attacker looks up each hash in their rainbow table.
  4. Match found = password cracked in milliseconds.

Example scenario

Database stores: "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"

Rainbow table lookup:

ef92b778... → "password123" ✓

Without additional defenses, unsalted hashes provide minimal protection against determined attackers with rainbow tables.

Salt: the defense

A salt is a unique, random string added to each password before hashing. This makes every hash unique, even for identical passwords, completely defeating rainbow table attacks.

How salting works

  1. Generate cryptographically secure random salt (16+ bytes).
  2. Combine salt + password.
  3. Hash the combined string.
  4. Store salt + hash together in database (salt is not secret).

Example with salt

User 1:

Password: "password123"

Salt: "a4f8d2e1c9b7"

Hash(salt + password): "3d5e7a1b..."

User 2 (same password!):

Password: "password123"

Salt: "x7k2m9n4p1q8"

Hash(salt + password): "9f2c4e6a..."

Same password → different salts → completely different hashes. Rainbow tables become useless because attackers must recompute hashes for every salt.

Why salts work

Hash functions compared

Fast hash functions (DO NOT use for passwords)

These algorithms are excellent for file integrity verification but terrible for password storage because they're optimized for speed.

Slow hash functions (Password-specific)

Modern password hashing

bcrypt

bcrypt remains the gold standard for most applications. Key features:

bcrypt output example:

$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYqJxN2.aSe

$2b$ = algorithm | $12$ = cost | salt + hash

Argon2

State-of-the-art password hashing. Recommended for new projects:

Argon2id output example:

$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG

Cost/work factor tuning

Password hashing should be slow enough to frustrate brute force attacks but fast enough not to degrade user experience:

Implementation guide

Registration flow

  1. User submits password over HTTPS.
  2. Server validates password strength (length, complexity).
  3. Generate salt and hash password (bcrypt/Argon2).
  4. Store username + salt + hash in database.
  5. Discard plaintext password immediately.

Login flow

  1. User submits credentials over HTTPS.
  2. Retrieve stored salt + hash for that username.
  3. Hash submitted password with stored salt.
  4. Compare computed hash with stored hash (constant-time comparison).
  5. Grant/deny access based on match.

Code example (Node.js + bcrypt)

const bcrypt = require('bcrypt');

// Registration
async function hashPassword(password) {
  const saltRounds = 12;
  const hash = await bcrypt.hash(password, saltRounds);
  return hash; // Store this in database
}

// Login
async function verifyPassword(password, storedHash) {
  const match = await bcrypt.compare(password, storedHash);
  return match; // true if valid
}

Common mistakes

Best practices

Algorithm selection

Configuration

Security measures

Ongoing maintenance