DEV Community

Cover image for Salt, Pepper, and Secret Sauce: The Recipe for Uncrackable Passwords
Igor Nosatov
Igor Nosatov

Posted on

Salt, Pepper, and Secret Sauce: The Recipe for Uncrackable Passwords

The Three Layers of Password Defense

Think of password security like cooking the perfect dish:

πŸ₯” Plain Password     = Raw potato (anyone can eat it)
πŸ§‚ Password + Salt    = Salted potato (better, but still guessable)
🌢️ Password + Pepper  = Spicy mystery (now we're talking)
πŸ‘¨β€πŸ³ Full Recipe        = Salt + Pepper + Secret Technique (uncrackable)
Enter fullscreen mode Exit fullscreen mode

Let's cook.

Layer 1: The Raw Ingredient (Don't Serve This)

// 🚫 NEVER EVER DO THIS
const user = {
  email: "bob@example.com",
  password: "Bob123456"  // Plain text = disaster waiting to happen
}

await db.users.insert(user);
Enter fullscreen mode Exit fullscreen mode

This is like serving raw chicken. Everyone gets food poisoning (hacked).

Layer 2: Adding Salt (The Standard Recipe)

Salt is a random value added to each password before hashing. It ensures that even if two users have the same password, they get different hashes.

const bcrypt = require('bcrypt');

// Salt is automatically generated by bcrypt
async function hashPassword(password) {
  const SALT_ROUNDS = 12;
  // bcrypt generates a unique salt for EACH password
  const hash = await bcrypt.hash(password, SALT_ROUNDS);
  return hash;
}

// Example outputs for the SAME password:
// User 1: $2b$12$R9h/cIPz0gi.URNNX3kh2OqW0W.Qs8U6oaXd6aVd3YDLV3W9Bu
// User 2: $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LdwVCiDM.nPqR8/Xm
//         ^^^^^^^^ <- Different salt in each hash!
Enter fullscreen mode Exit fullscreen mode

How Salt Works: The Visual Guide

User 1: "password123"                    User 2: "password123"
        ↓                                        ↓
   [Random Salt A]                          [Random Salt B]
        ↓                                        ↓
   "aj7$kL9m" + "password123"             "9Xm#pQ2n" + "password123"
        ↓                                        ↓
   Hash 4,096 times                         Hash 4,096 times
        ↓                                        ↓
   $2b$12$aj7$kL9m...XYZ                   $2b$12$9Xm#pQ2n...ABC
        ↓                                        ↓
   Completely different hashes! βœ…
Enter fullscreen mode Exit fullscreen mode

Why salt matters:

// WITHOUT SALT (bad)
hash("password123") = "ef92b778..."  // Always the same
hash("password123") = "ef92b778..."  // Attacker builds a rainbow table

// WITH SALT (good)
hash("password123" + "salt_A") = "a3d4e5f6..."  // Different every time
hash("password123" + "salt_B") = "z9x8y7w6..."  // Rainbow tables useless
Enter fullscreen mode Exit fullscreen mode

Layer 3: Adding Pepper (The Secret Ingredient)

Pepper is a secret value stored OUTSIDE the database, typically in environment variables. Unlike salt (which is unique per password), pepper is the same for ALL passwords.

Think of it like this:

  • Salt = Unique seasoning for each dish
  • Pepper = Your restaurant's secret ingredient that goes in everything
const crypto = require('crypto');
const bcrypt = require('bcrypt');

// Pepper is stored in environment variables, NOT in the database
const PEPPER = process.env.PASSWORD_PEPPER; // e.g., "mySecretPepper2024!@#"

async function hashPasswordWithPepper(password) {
  // Step 1: Add pepper using HMAC
  const pepperedPassword = crypto
    .createHmac('sha256', PEPPER)
    .update(password)
    .digest('hex');

  // Step 2: Hash with bcrypt (which adds salt automatically)
  const hash = await bcrypt.hash(pepperedPassword, 12);

  return hash;
}

async function verifyPasswordWithPepper(password, hash) {
  // Step 1: Add pepper the same way
  const pepperedPassword = crypto
    .createHmac('sha256', PEPPER)
    .update(password)
    .digest('hex');

  // Step 2: Compare with bcrypt
  return await bcrypt.compare(pepperedPassword, hash);
}
Enter fullscreen mode Exit fullscreen mode

The Security Difference

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    ATTACKER SCENARIO                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                           β”‚
β”‚  WITH ONLY SALT:                                         β”‚
β”‚  Attacker steals database β†’ Has everything needed        β”‚
β”‚  Can attempt to crack passwords offline                  β”‚
β”‚                                                           β”‚
β”‚  WITH SALT + PEPPER:                                     β”‚
β”‚  Attacker steals database β†’ Missing pepper!              β”‚
β”‚  All hashes are useless without the pepper value         β”‚
β”‚  Must compromise BOTH database AND server environment    β”‚
β”‚                                                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

The Complete Recipe: Production-Ready Implementation

Let's build a real authentication system with all layers:

// config.js
require('dotenv').config();

module.exports = {
  PEPPER: process.env.PASSWORD_PEPPER,
  SALT_ROUNDS: 12,
  MAX_PASSWORD_LENGTH: 72, // bcrypt limit
  MIN_PASSWORD_LENGTH: 12
};

// password-service.js
const crypto = require('crypto');
const bcrypt = require('bcrypt');
const config = require('./config');

class PasswordService {
  /**
   * Apply pepper to password using HMAC-SHA256
   * This creates a deterministic but unpredictable transformation
   */
  _applyPepper(password) {
    if (!config.PEPPER) {
      throw new Error('PASSWORD_PEPPER environment variable not set!');
    }

    return crypto
      .createHmac('sha256', config.PEPPER)
      .update(password)
      .digest('hex');
  }

  /**
   * Hash a password with pepper and salt
   * @param {string} password - Plain text password
   * @returns {Promise<string>} - Hashed password
   */
  async hash(password) {
    // Validation
    if (!password || typeof password !== 'string') {
      throw new Error('Password must be a non-empty string');
    }

    if (password.length < config.MIN_PASSWORD_LENGTH) {
      throw new Error(`Password must be at least ${config.MIN_PASSWORD_LENGTH} characters`);
    }

    if (password.length > config.MAX_PASSWORD_LENGTH) {
      throw new Error(`Password must not exceed ${config.MAX_PASSWORD_LENGTH} characters`);
    }

    // Step 1: Apply pepper (secret ingredient)
    const pepperedPassword = this._applyPepper(password);

    // Step 2: Apply bcrypt with salt
    const hash = await bcrypt.hash(pepperedPassword, config.SALT_ROUNDS);

    return hash;
  }

  /**
   * Verify a password against a hash
   * @param {string} password - Plain text password to verify
   * @param {string} hash - Stored hash to compare against
   * @returns {Promise<boolean>} - Whether password matches
   */
  async verify(password, hash) {
    if (!password || !hash) {
      return false;
    }

    try {
      // Apply same pepper transformation
      const pepperedPassword = this._applyPepper(password);

      // Compare with bcrypt
      return await bcrypt.compare(pepperedPassword, hash);
    } catch (error) {
      console.error('Password verification error:', error);
      return false;
    }
  }

  /**
   * Check if a hash needs rehashing (algorithm update or cost factor change)
   * @param {string} hash - The hash to check
   * @returns {boolean} - Whether rehashing is needed
   */
  needsRehash(hash) {
    try {
      const rounds = bcrypt.getRounds(hash);
      return rounds < config.SALT_ROUNDS;
    } catch (error) {
      return true; // If we can't parse it, definitely rehash
    }
  }
}

module.exports = new PasswordService();
Enter fullscreen mode Exit fullscreen mode

Real-World User Authentication Flow

// auth-controller.js
const passwordService = require('./password-service');
const db = require('./database');

class AuthController {
  /**
   * Register a new user
   */
  async register(req, res) {
    const { email, password, name } = req.body;

    try {
      // Check if user exists
      const existingUser = await db.users.findOne({ email });
      if (existingUser) {
        return res.status(409).json({ error: 'Email already registered' });
      }

      // Hash password with salt and pepper
      const hashedPassword = await passwordService.hash(password);

      // Store user (NEVER store plain password)
      const user = await db.users.insert({
        email,
        name,
        password: hashedPassword,
        createdAt: new Date(),
        lastLogin: null
      });

      // Remove password from response
      delete user.password;

      return res.status(201).json({ user });
    } catch (error) {
      console.error('Registration error:', error);
      return res.status(500).json({ error: 'Registration failed' });
    }
  }

  /**
   * Login user
   */
  async login(req, res) {
    const { email, password } = req.body;

    try {
      // Find user
      const user = await db.users.findOne({ email });

      if (!user) {
        // Still hash to prevent timing attacks
        await passwordService.hash(password);
        return res.status(401).json({ error: 'Invalid credentials' });
      }

      // Verify password
      const isValid = await passwordService.verify(password, user.password);

      if (!isValid) {
        return res.status(401).json({ error: 'Invalid credentials' });
      }

      // Check if password needs rehashing (security upgrade)
      if (passwordService.needsRehash(user.password)) {
        const newHash = await passwordService.hash(password);
        await db.users.update(
          { _id: user._id },
          { password: newHash }
        );
      }

      // Update last login
      await db.users.update(
        { _id: user._id },
        { lastLogin: new Date() }
      );

      // Generate session/JWT (implementation depends on your auth strategy)
      const token = generateAuthToken(user);

      // Remove password from response
      delete user.password;

      return res.json({ user, token });
    } catch (error) {
      console.error('Login error:', error);
      return res.status(500).json({ error: 'Login failed' });
    }
  }

  /**
   * Change password
   */
  async changePassword(req, res) {
    const { currentPassword, newPassword } = req.body;
    const userId = req.user.id; // From authenticated session

    try {
      const user = await db.users.findOne({ _id: userId });

      // Verify current password
      const isValid = await passwordService.verify(currentPassword, user.password);

      if (!isValid) {
        return res.status(401).json({ error: 'Current password is incorrect' });
      }

      // Check new password isn't the same as old
      const isSameAsOld = await passwordService.verify(newPassword, user.password);
      if (isSameAsOld) {
        return res.status(400).json({ error: 'New password must be different' });
      }

      // Hash new password
      const newHash = await passwordService.hash(newPassword);

      // Update password
      await db.users.update(
        { _id: userId },
        { 
          password: newHash,
          passwordChangedAt: new Date()
        }
      );

      return res.json({ success: true });
    } catch (error) {
      console.error('Password change error:', error);
      return res.status(500).json({ error: 'Password change failed' });
    }
  }
}

module.exports = new AuthController();
Enter fullscreen mode Exit fullscreen mode

Environment Setup: The Secret Recipe Card

# .env file (NEVER commit this to git!)
PASSWORD_PEPPER=your-super-secret-pepper-value-here-2024
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=another-secret-for-jwt-tokens
Enter fullscreen mode Exit fullscreen mode
# .env.example (THIS is safe to commit)
PASSWORD_PEPPER=change-this-in-production
DATABASE_URL=your-database-url
JWT_SECRET=your-jwt-secret
Enter fullscreen mode Exit fullscreen mode
// .gitignore
.env
node_modules/
*.log
Enter fullscreen mode Exit fullscreen mode

Advanced: Key Rotation (When You Change the Pepper)

What happens when you need to rotate your pepper? Here's how to do it safely:

// config.js
module.exports = {
  PEPPER: process.env.PASSWORD_PEPPER,
  OLD_PEPPER: process.env.OLD_PASSWORD_PEPPER, // Keep old pepper during transition
  SALT_ROUNDS: 12
};

// password-service.js (enhanced)
class PasswordService {
  async verify(password, hash) {
    try {
      // Try with current pepper first
      const pepperedPassword = this._applyPepper(password, config.PEPPER);
      const isValid = await bcrypt.compare(pepperedPassword, hash);

      if (isValid) {
        return { valid: true, needsRotation: false };
      }

      // If that fails and we have an old pepper, try that
      if (config.OLD_PEPPER) {
        const oldPepperedPassword = this._applyPepper(password, config.OLD_PEPPER);
        const isValidOld = await bcrypt.compare(oldPepperedPassword, hash);

        if (isValidOld) {
          return { valid: true, needsRotation: true }; // Flag for rehashing
        }
      }

      return { valid: false, needsRotation: false };
    } catch (error) {
      console.error('Password verification error:', error);
      return { valid: false, needsRotation: false };
    }
  }
}

// In your login handler
async login(req, res) {
  // ... existing code ...

  const verificationResult = await passwordService.verify(password, user.password);

  if (!verificationResult.valid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // If password was verified with old pepper, rehash with new pepper
  if (verificationResult.needsRotation) {
    const newHash = await passwordService.hash(password);
    await db.users.update(
      { _id: user._id },
      { password: newHash }
    );
    console.log(`Rotated password for user ${user._id}`);
  }

  // ... rest of login logic ...
}
Enter fullscreen mode Exit fullscreen mode

The Security Comparison Chart

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Security Level Comparison                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Method             β”‚ DB Breach β”‚ Timing Attackβ”‚ Quantum Safe? β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Plain text         β”‚ ☠️ FATAL  β”‚ ☠️ FATAL     β”‚ ☠️ FATAL       β”‚
β”‚ MD5/SHA1           β”‚ πŸ”΄ SEVERE β”‚ 🟑 MODERATE  β”‚ πŸ”΄ SEVERE      β”‚
β”‚ SHA256 (no salt)   β”‚ πŸ”΄ SEVERE β”‚ 🟑 MODERATE  β”‚ 🟑 MODERATE    β”‚
β”‚ bcrypt (salt only) β”‚ 🟑 MEDIUM β”‚ 🟒 GOOD      β”‚ 🟒 GOOD        β”‚
β”‚ bcrypt + pepper    β”‚ 🟒 GOOD   β”‚ 🟒 GOOD      β”‚ 🟒 GOOD        β”‚
β”‚ Argon2 + pepper    β”‚ 🟒 BEST   β”‚ 🟒 BEST      β”‚ 🟒 BEST        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Common Mistakes and How to Avoid Them

Mistake #1: Storing Pepper in the Database

// ❌ WRONG - Pepper in database defeats the purpose
const user = {
  email: "user@example.com",
  password: hashedPassword,
  pepper: "secret123"  // NO! This makes it just another salt
}

// βœ… CORRECT - Pepper in environment variables
// .env file
PASSWORD_PEPPER=actual-secret-value
Enter fullscreen mode Exit fullscreen mode

Mistake #2: Using the Same Salt for Multiple Passwords

// ❌ WRONG - Reusing salt
const GLOBAL_SALT = "my-app-salt";
const hash = await bcrypt.hash(password + GLOBAL_SALT, 10);

// βœ… CORRECT - Let bcrypt generate unique salt each time
const hash = await bcrypt.hash(password, 12);
Enter fullscreen mode Exit fullscreen mode

Mistake #3: Applying Pepper After Hashing

// ❌ WRONG - Pepper comes BEFORE hashing
const hash = await bcrypt.hash(password, 12);
const pepperedHash = applyPepper(hash); // Too late!

// βœ… CORRECT - Pepper before hashing
const pepperedPassword = applyPepper(password);
const hash = await bcrypt.hash(pepperedPassword, 12);
Enter fullscreen mode Exit fullscreen mode

Mistake #4: Not Handling Pepper Rotation

// ❌ WRONG - Users locked out after pepper change
const PEPPER = "new-pepper-value"; // Changed, old users can't log in

// βœ… CORRECT - Support old pepper during transition
const PEPPER = "new-pepper-value";
const OLD_PEPPER = "old-pepper-value"; // Keep for existing users
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

// Benchmark different approaches
const benchmark = async () => {
  const password = "TestPassword123!";

  console.time('bcrypt only (12 rounds)');
  await bcrypt.hash(password, 12);
  console.timeEnd('bcrypt only (12 rounds)');
  // ~ 350-400ms

  console.time('HMAC-SHA256 + bcrypt (12 rounds)');
  const peppered = crypto.createHmac('sha256', 'pepper').update(password).digest('hex');
  await bcrypt.hash(peppered, 12);
  console.timeEnd('HMAC-SHA256 + bcrypt (12 rounds)');
  // ~ 351-401ms (negligible overhead)
};

// The pepper step adds < 1ms overhead!
Enter fullscreen mode Exit fullscreen mode

Key takeaway: The HMAC pepper step is computationally cheap. The bcrypt hashing is what intentionally takes time (to slow down attackers).

Bonus: Storing Other Sensitive Data

While we're seasoning our security, let's talk about other sensitive data:

const crypto = require('crypto');

class DataEncryptionService {
  constructor() {
    this.algorithm = 'aes-256-gcm';
    this.key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); // 32 bytes
  }

  /**
   * Encrypt sensitive data (credit cards, SSN, etc.)
   * Unlike passwords, this IS reversible (you need to decrypt later)
   */
  encrypt(text) {
    const iv = crypto.randomBytes(16); // Initialization vector
    const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);

    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    const authTag = cipher.getAuthTag();

    // Store: encrypted data + IV + auth tag
    return {
      encrypted,
      iv: iv.toString('hex'),
      authTag: authTag.toString('hex')
    };
  }

  /**
   * Decrypt sensitive data
   */
  decrypt(encryptedData) {
    const decipher = crypto.createDecipheriv(
      this.algorithm,
      this.key,
      Buffer.from(encryptedData.iv, 'hex')
    );

    decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));

    let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted;
  }
}

// Usage example
const encryptionService = new DataEncryptionService();

// Storing sensitive data
const user = {
  email: "user@example.com",
  password: hashedPassword, // Hashed (one-way)
  creditCard: encryptionService.encrypt("4532-1234-5678-9010"), // Encrypted (two-way)
  ssn: encryptionService.encrypt("123-45-6789") // Encrypted (two-way)
};

// Retrieving sensitive data
const decryptedCC = encryptionService.decrypt(user.creditCard);
Enter fullscreen mode Exit fullscreen mode

When to Hash vs When to Encrypt

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   HASH (one-way)                          β”‚
β”‚  βœ… Passwords                                             β”‚
β”‚  βœ… Security answers                                      β”‚
β”‚  βœ… API keys (sometimes)                                  β”‚
β”‚  βœ… Any data you NEVER need to retrieve                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                 ENCRYPT (two-way)                         β”‚
β”‚  βœ… Credit card numbers                                   β”‚
β”‚  βœ… Social security numbers                               β”‚
β”‚  βœ… Personal identification                               β”‚
β”‚  βœ… Any data you NEED to retrieve later                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

The Complete Security Checklist

Before you deploy:

  • [ ] Passwords hashed with bcrypt (12+ rounds)
  • [ ] Pepper stored in environment variables
  • [ ] Pepper NOT in version control
  • [ ] Salt automatically generated per password
  • [ ] HTTPS enabled (passwords encrypted in transit)
  • [ ] Rate limiting on login attempts
  • [ ] Password complexity requirements enforced
  • [ ] Timing attack prevention (constant-time comparison)
  • [ ] No password hints stored
  • [ ] No passwords in logs
  • [ ] No passwords in error messages
  • [ ] Password rehashing on algorithm updates
  • [ ] Sensitive data encrypted (not hashed)
  • [ ] Regular security audits scheduled

Testing Your Implementation

// password-service.test.js
const passwordService = require('./password-service');

describe('PasswordService', () => {
  test('should hash password successfully', async () => {
    const password = 'TestPassword123!';
    const hash = await passwordService.hash(password);

    expect(hash).toBeDefined();
    expect(hash).not.toBe(password);
    expect(hash.startsWith('$2b$')).toBe(true);
  });

  test('should verify correct password', async () => {
    const password = 'TestPassword123!';
    const hash = await passwordService.hash(password);

    const isValid = await passwordService.verify(password, hash);
    expect(isValid).toBe(true);
  });

  test('should reject incorrect password', async () => {
    const password = 'TestPassword123!';
    const wrongPassword = 'WrongPassword456!';
    const hash = await passwordService.hash(password);

    const isValid = await passwordService.verify(wrongPassword, hash);
    expect(isValid).toBe(false);
  });

  test('should create different hashes for same password', async () => {
    const password = 'TestPassword123!';
    const hash1 = await passwordService.hash(password);
    const hash2 = await passwordService.hash(password);

    expect(hash1).not.toBe(hash2);
  });

  test('should throw error for short password', async () => {
    const shortPassword = 'short';

    await expect(passwordService.hash(shortPassword))
      .rejects
      .toThrow('Password must be at least');
  });

  test('should detect if hash needs rehashing', async () => {
    const oldHash = '$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy';
    const needsRehash = passwordService.needsRehash(oldHash);

    expect(needsRehash).toBe(true); // Because we use 12 rounds, not 10
  });
});
Enter fullscreen mode Exit fullscreen mode

What Grandma Taught Me

Remember my grandmother's pasta sauce? The secret wasn't just one ingredientβ€”it was the combination and the technique:

  1. Quality ingredients (strong base algorithm: bcrypt)
  2. Proper seasoning (unique salt for each dish)
  3. Secret ingredient (pepper that stays in the family)
  4. Technique (proper key management and rotation)

Your users trust you with their passwords. That trust is sacred. Don't be the person who serves raw chicken because "it's faster." Take the time to season your security properly.

Quick Start: Copy-Paste Template

npm install bcrypt dotenv
Enter fullscreen mode Exit fullscreen mode
// password-utils.js - Copy this entire file
require('dotenv').config();
const crypto = require('crypto');
const bcrypt = require('bcrypt');

const PEPPER = process.env.PASSWORD_PEPPER;
const SALT_ROUNDS = 12;

function applyPepper(password) {
  return crypto
    .createHmac('sha256', PEPPER)
    .update(password)
    .digest('hex');
}

async function hashPassword(password) {
  const peppered = applyPepper(password);
  return await bcrypt.hash(peppered, SALT_ROUNDS);
}

async function verifyPassword(password, hash) {
  const peppered = applyPepper(password);
  return await bcrypt.compare(peppered, hash);
}

module.exports = { hashPassword, verifyPassword };
Enter fullscreen mode Exit fullscreen mode
# .env
PASSWORD_PEPPER=generate-a-random-64-character-string-here
Enter fullscreen mode Exit fullscreen mode

That's it. Three functions. Uncrackable passwords.

Top comments (0)