CrackedRuby CrackedRuby

Overview

Salt and pepper represent two distinct approaches to protecting password hashes from precomputation attacks. A salt adds a random value to each password before hashing, ensuring identical passwords produce different hash outputs. A pepper adds a secret value to passwords before hashing, providing an additional layer of protection even if the password database is compromised.

Rainbow tables and dictionary attacks pose significant threats to password databases. Without additional protections, attackers who obtain password hashes can use precomputed tables mapping common passwords to their hash values. Salt defeats these attacks by introducing uniqueness per password. Pepper provides defense in depth by introducing a secret that must be obtained separately from the database.

The fundamental difference between salt and pepper lies in storage and purpose. Salt values store alongside password hashes in the database, visible to anyone who gains database access. Each user requires a unique salt. Pepper values remain separate from the database, typically stored in application configuration or environment variables. The entire system shares a single pepper value.

# Without salt or pepper - vulnerable
hash = Digest::SHA256.hexdigest("password123")
# => "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"

# With salt - same password, different hash
salt = SecureRandom.hex(16)
hash_with_salt = Digest::SHA256.hexdigest(salt + "password123")
# => Result varies due to random salt

# With pepper - secret value added
pepper = ENV['PASSWORD_PEPPER']
hash_with_pepper = Digest::SHA256.hexdigest("password123" + pepper)
# => Result depends on secret pepper value

Modern password hashing requires salt as a mandatory component. The bcrypt, scrypt, and Argon2 algorithms automatically generate and manage salts. Pepper remains optional but provides additional protection in high-security environments.

Key Principles

Salt operates as a cryptographic nonce - a value used once per password that prevents identical passwords from generating identical hashes. The salt generation process uses cryptographically secure random number generators to ensure unpredictability. Minimum recommended salt length is 16 bytes (128 bits), though 32 bytes provides additional margin.

The salt concatenates with the password before the hashing operation. Three common concatenation patterns exist: prepending the salt, appending the salt, or interleaving salt and password. Prepending represents the most common approach due to its simplicity and compatibility with existing implementations.

# Salt prepending
salted_password = salt + password
hash = hash_function(salted_password)

# Salt appending
salted_password = password + salt
hash = hash_function(salted_password)

Salt storage occurs directly in the password hash record. Most password hashing formats encode both salt and hash into a single string. The bcrypt format exemplifies this approach:

$2b$12$EXAMPLESALTxxxxxxxxxxOeKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
└─┘ └┘ └────────────────────┘└──────────────────────────┘
│   │         Salt                    Hash value
│   Cost factor
Algorithm identifier

Pepper functions as a secret key mixed into password hashes. Unlike salt, which differs per user, a single pepper value applies across the entire system. The pepper never stores in the database. Common storage locations include environment variables, configuration management systems, hardware security modules, or key management services.

Pepper concatenation follows similar patterns to salt but typically occurs after salt addition. The pattern becomes: hash(salt + password + pepper). This ordering provides optimal security characteristics because attackers who obtain both password and salt still lack the pepper.

The cryptographic strength of pepper depends on its entropy and secrecy. Minimum pepper length should match the hash output size - 256 bits for SHA-256, 512 bits for SHA-512. Pepper generation requires cryptographically secure random number generators, not standard random functions.

Key rotation poses a significant challenge for pepper values. Changing the pepper invalidates all existing password hashes. Systems supporting pepper rotation maintain multiple pepper values identified by version numbers, rehashing passwords opportunistically during user login.

# Pepper rotation structure
peppers = {
  1 => "original_pepper_value_here",
  2 => "new_pepper_value_here",
  3 => "current_pepper_value_here"
}

# Hash storage includes pepper version
hash_record = {
  hash: computed_hash,
  salt: salt_value,
  pepper_version: 3
}

# Verification uses the correct pepper version
verification_pepper = peppers[hash_record[:pepper_version]]

Security Implications

Salt and pepper address different attack vectors in password security. Understanding the specific threats each technique mitigates enables appropriate security architecture decisions.

Rainbow table attacks precompute hash values for common passwords. An attacker generates hashes for millions of passwords, stores them in a lookup table, then compares stolen password hashes against the table. Salt defeats rainbow tables by introducing uniqueness per password. An attacker must generate a separate rainbow table for each possible salt value - computationally infeasible when salts contain sufficient entropy.

Dictionary attacks operate similarly but compute hashes on demand rather than precomputing. The attacker iterates through password dictionaries, hashing each candidate password and comparing to the target hash. Salt increases the computational cost linearly with the number of salted passwords. An attack against one password does not benefit attacks against other passwords.

Database compromise represents a critical failure mode. Attackers who gain read access to the password database obtain all password hashes and salts. Without pepper, attackers can immediately begin offline attacks against the hashes. Pepper requires attackers to also compromise the application server or configuration system storing the pepper value.

The security benefit of pepper depends on separation of concerns. The database server and application server must maintain distinct security boundaries. If a single compromise exposes both database and application configuration, pepper provides no additional protection. Organizations with strong operational security can maintain this separation effectively.

Timing attacks attempt to extract information by measuring operation duration. Password verification timing can leak information about password correctness. Constant-time comparison functions prevent timing attacks by ensuring all comparisons take identical time regardless of where differences occur.

# Vulnerable to timing attacks
def verify_password_unsafe(input, stored_hash)
  computed_hash = hash_password(input)
  computed_hash == stored_hash  # Comparison short-circuits on first difference
end

# Protected against timing attacks
def verify_password_safe(input, stored_hash)
  computed_hash = hash_password(input)
  Rack::Utils.secure_compare(computed_hash, stored_hash)
end

Side-channel attacks exploit information leakage through physical properties like power consumption or electromagnetic radiation. Hardware security modules protect pepper values against side-channel attacks by performing cryptographic operations within tamper-resistant hardware. Applications handling highly sensitive authentication should consider HSM storage for pepper values.

Pepper rotation introduces temporary vulnerability windows. During rotation, some passwords use old pepper values while new passwords use updated values. The system must maintain both pepper versions until all passwords have been rehashed. A compromise during this transition period exposes passwords hashed with different pepper values to different attack scenarios.

Backup and disaster recovery procedures must account for pepper values. Database backups contain salted password hashes but lack pepper values. Restoring a database backup without the corresponding pepper value renders all password hashes unverifiable. Pepper values require separate secure backup and synchronization with database backups.

Key management complexity increases with pepper adoption. Organizations must implement secure key generation, distribution, rotation, and destruction procedures. Cloud key management services provide these capabilities but introduce dependencies on external services. The cost-benefit analysis weighs increased security against operational complexity.

Ruby Implementation

Ruby provides multiple approaches for implementing salt and pepper in password hashing systems. The bcrypt-ruby gem offers the most common implementation, handling salt generation and management automatically.

require 'bcrypt'

# BCrypt automatically generates and manages salt
class User
  attr_accessor :password_hash
  
  def password=(new_password)
    # Cost factor 12 provides good security/performance balance
    @password_hash = BCrypt::Password.create(new_password, cost: 12)
  end
  
  def authenticate(password)
    BCrypt::Password.new(@password_hash) == password
  end
end

user = User.new
user.password = "secret_password"
# password_hash contains: $2a$12$[22-character-salt][31-character-hash]

user.authenticate("secret_password")    # => true
user.authenticate("wrong_password")     # => false

The BCrypt::Password class encapsulates both salt and hash in a single string. The salt generates automatically using SecureRandom under the hood. Manual salt specification remains possible but should be avoided in production code.

Implementing pepper with BCrypt requires mixing the pepper value before passing the password to BCrypt. This approach maintains compatibility with standard BCrypt while adding the pepper layer.

require 'bcrypt'

class User
  PEPPER = ENV.fetch('PASSWORD_PEPPER')
  
  attr_accessor :password_hash
  
  def password=(new_password)
    peppered_password = new_password + PEPPER
    @password_hash = BCrypt::Password.create(peppered_password, cost: 12)
  end
  
  def authenticate(password)
    peppered_password = password + PEPPER
    BCrypt::Password.new(@password_hash) == peppered_password
  end
end

Manual salt implementation using Ruby's digest libraries demonstrates the underlying mechanics. This approach provides complete control but requires careful attention to security details.

require 'securerandom'
require 'digest'

class PasswordHasher
  HASH_ALGORITHM = Digest::SHA256
  SALT_LENGTH = 32  # bytes
  
  def self.hash_password(password)
    salt = SecureRandom.bytes(SALT_LENGTH)
    hash = HASH_ALGORITHM.digest(salt + password)
    
    # Encode salt and hash for storage
    {
      salt: Base64.strict_encode64(salt),
      hash: Base64.strict_encode64(hash)
    }
  end
  
  def self.verify_password(password, salt_b64, hash_b64)
    salt = Base64.strict_decode64(salt_b64)
    stored_hash = Base64.strict_decode64(hash_b64)
    
    computed_hash = HASH_ALGORITHM.digest(salt + password)
    
    # Constant-time comparison
    Rack::Utils.secure_compare(computed_hash, stored_hash)
  end
end

# Usage
credentials = PasswordHasher.hash_password("user_password")
# => {salt: "base64_encoded_salt", hash: "base64_encoded_hash"}

PasswordHasher.verify_password("user_password", 
                               credentials[:salt], 
                               credentials[:hash])
# => true

Advanced implementations combine salt and pepper with key derivation functions. The PBKDF2 algorithm applies a pseudorandom function repeatedly to derive keys from passwords.

require 'openssl'
require 'securerandom'

class SecurePasswordHasher
  ITERATIONS = 100_000
  HASH_LENGTH = 32  # bytes
  SALT_LENGTH = 32  # bytes
  PEPPER = ENV.fetch('PASSWORD_PEPPER')
  
  def self.hash_password(password)
    salt = SecureRandom.bytes(SALT_LENGTH)
    peppered_password = password + PEPPER
    
    hash = OpenSSL::PKCS5.pbkdf2_hmac(
      peppered_password,
      salt,
      ITERATIONS,
      HASH_LENGTH,
      OpenSSL::Digest::SHA256.new
    )
    
    {
      salt: Base64.strict_encode64(salt),
      hash: Base64.strict_encode64(hash),
      iterations: ITERATIONS
    }
  end
  
  def self.verify_password(password, salt_b64, hash_b64, iterations)
    salt = Base64.strict_decode64(salt_b64)
    stored_hash = Base64.strict_decode64(hash_b64)
    peppered_password = password + PEPPER
    
    computed_hash = OpenSSL::PKCS5.pbkdf2_hmac(
      peppered_password,
      salt,
      iterations,
      HASH_LENGTH,
      OpenSSL::Digest::SHA256.new
    )
    
    Rack::Utils.secure_compare(computed_hash, stored_hash)
  end
end

Pepper rotation requires maintaining multiple pepper versions and tracking which version hashed each password. The implementation stores a pepper version identifier alongside each password hash.

class MultiPepperHasher
  PEPPERS = {
    1 => ENV.fetch('PASSWORD_PEPPER_V1'),
    2 => ENV.fetch('PASSWORD_PEPPER_V2'),
    3 => ENV.fetch('PASSWORD_PEPPER_V3')
  }
  
  CURRENT_PEPPER_VERSION = 3
  
  def self.hash_password(password)
    pepper = PEPPERS[CURRENT_PEPPER_VERSION]
    peppered_password = password + pepper
    
    {
      hash: BCrypt::Password.create(peppered_password, cost: 12),
      pepper_version: CURRENT_PEPPER_VERSION
    }
  end
  
  def self.verify_password(password, hash_data)
    pepper = PEPPERS[hash_data[:pepper_version]]
    
    unless pepper
      raise "Unknown pepper version: #{hash_data[:pepper_version]}"
    end
    
    peppered_password = password + pepper
    BCrypt::Password.new(hash_data[:hash]) == peppered_password
  end
  
  def self.needs_rehash?(hash_data)
    hash_data[:pepper_version] != CURRENT_PEPPER_VERSION
  end
  
  def self.rehash_password(password)
    hash_password(password)
  end
end

# Usage with opportunistic rehashing
def authenticate_user(username, password)
  user = find_user(username)
  return false unless user
  
  if MultiPepperHasher.verify_password(password, user.password_data)
    # Rehash with current pepper if needed
    if MultiPepperHasher.needs_rehash?(user.password_data)
      user.password_data = MultiPepperHasher.rehash_password(password)
      user.save
    end
    return true
  end
  
  false
end

The argon2 gem provides the most modern password hashing algorithm, winner of the Password Hashing Competition. Argon2 includes built-in salt management and memory-hard properties that resist GPU-based attacks.

require 'argon2'

class Argon2Hasher
  PEPPER = ENV.fetch('PASSWORD_PEPPER')
  
  # Configure Argon2 parameters
  HASHER = Argon2::Password.new(
    t_cost: 2,        # Number of iterations
    m_cost: 16,       # Memory in mebibytes (2^16 KiB)
    p_cost: 1,        # Parallelism factor
    salt_len: 16      # Salt length in bytes
  )
  
  def self.hash_password(password)
    peppered_password = password + PEPPER
    HASHER.create(peppered_password)
  end
  
  def self.verify_password(password, hash)
    peppered_password = password + PEPPER
    HASHER.verify_password(peppered_password, hash)
  end
end

Common Pitfalls

Using insufficient salt length compromises security. Salts shorter than 16 bytes reduce the search space for attackers attempting to generate rainbow tables per salt value. A 64-bit salt requires only 2^64 precomputed tables - feasible for well-funded attackers. Always use minimum 128-bit (16-byte) salts.

# Insufficient salt length - vulnerable
short_salt = SecureRandom.hex(4)  # Only 8 bytes

# Proper salt length
proper_salt = SecureRandom.hex(16)  # 32 bytes

Storing pepper values in the database defeats the purpose of pepper. Pepper must remain separate from password hashes to provide defense in depth. Developers occasionally store pepper in a database configuration table, eliminating the security benefit.

Reusing the same salt across multiple passwords creates the same vulnerability as no salt. Each password must receive a unique salt value. Global salts or per-application salts provide no protection against precomputation attacks.

# Wrong - reusing salt
GLOBAL_SALT = SecureRandom.hex(16)
hash1 = Digest::SHA256.hexdigest(GLOBAL_SALT + "password1")
hash2 = Digest::SHA256.hexdigest(GLOBAL_SALT + "password2")

# Correct - unique salt per password
salt1 = SecureRandom.hex(16)
hash1 = Digest::SHA256.hexdigest(salt1 + "password1")

salt2 = SecureRandom.hex(16)
hash2 = Digest::SHA256.hexdigest(salt2 + "password2")

Using weak hash functions undermines salt and pepper benefits. MD5 and SHA-1 compute too quickly, allowing attackers to test billions of passwords per second. Modern password hashing requires computationally expensive algorithms like bcrypt, scrypt, or Argon2.

Concatenating salt and password incorrectly can introduce vulnerabilities. Certain concatenation patterns enable length extension attacks against some hash functions. The standard pattern places salt first, followed by password: hash(salt || password).

Failing to use cryptographically secure random number generators for salt generation introduces predictability. Ruby's Random class provides adequate randomness for general purposes but SecureRandom must be used for cryptographic operations.

# Wrong - predictable salt
require 'digest'
salt = Random.new.bytes(16)  # Not cryptographically secure

# Correct - unpredictable salt
require 'securerandom'
salt = SecureRandom.bytes(16)  # Cryptographically secure

Hardcoding pepper values in source code exposes them through version control systems. Source code repositories often replicate across multiple systems and backup locations. Pepper values should load from environment variables or secure configuration management.

Missing constant-time comparison during password verification enables timing attacks. The standard equality operator (==) short-circuits on the first byte difference, leaking information about hash correctness through timing differences.

Implementing custom password hashing instead of using established libraries introduces numerous security risks. Password hashing requires careful attention to subtle security properties. Developers should use bcrypt, scrypt, or Argon2 through established libraries rather than implementing their own schemes.

Storing salt and hash in separate database fields without proper encoding can cause parsing issues. Binary salt and hash values require base64 or hex encoding before storage in text fields. Improper encoding leads to data corruption and authentication failures.

Forgetting to include pepper during password verification creates authentication failures. The verification code must apply the exact same transformations as the hashing code. A common mistake involves hashing with pepper but verifying without pepper.

# Incorrect - hashing with pepper but verifying without
def set_password(password)
  peppered = password + PEPPER
  @hash = BCrypt::Password.create(peppered)
end

def authenticate(password)
  # Missing pepper - will always fail
  BCrypt::Password.new(@hash) == password
end

# Correct - consistent pepper application
def authenticate(password)
  peppered = password + PEPPER
  BCrypt::Password.new(@hash) == peppered
end

Practical Examples

A complete user authentication system demonstrates salt and pepper integration with a web application. This example uses BCrypt with pepper and implements secure registration and login flows.

require 'bcrypt'
require 'securerandom'

class User
  PEPPER = ENV.fetch('PASSWORD_PEPPER', 'development-pepper-value')
  BCRYPT_COST = ENV.fetch('BCRYPT_COST', 12).to_i
  
  attr_accessor :id, :username, :password_hash, :created_at
  
  def self.register(username, password)
    return nil if password.length < 8
    
    user = User.new
    user.id = SecureRandom.uuid
    user.username = username
    user.password = password
    user.created_at = Time.now
    
    user
  end
  
  def password=(new_password)
    peppered_password = apply_pepper(new_password)
    @password_hash = BCrypt::Password.create(peppered_password, cost: BCRYPT_COST)
  end
  
  def authenticate(password_attempt)
    peppered_attempt = apply_pepper(password_attempt)
    BCrypt::Password.new(@password_hash) == peppered_attempt
  rescue BCrypt::Errors::InvalidHash
    false
  end
  
  private
  
  def apply_pepper(password)
    password + PEPPER
  end
end

# Registration flow
user = User.register("alice", "secure_password_123")
# user.password_hash => "$2a$12$[salt-and-hash-data]"

# Authentication flow
if user.authenticate("secure_password_123")
  puts "Authentication successful"
else
  puts "Authentication failed"
end

Migrating from unsalted hashes to salted hashes requires careful planning. This example shows opportunistic migration during user login without forcing password resets.

class UserMigration
  attr_accessor :username, :old_hash, :new_hash, :salt, :uses_salt
  
  def self.migrate_password(username, password)
    user = find_user(username)
    
    # Check if already migrated
    return false if user.uses_salt
    
    # Verify using old unsalted hash
    old_hash = Digest::SHA256.hexdigest(password)
    return false unless user.old_hash == old_hash
    
    # Generate new salted hash
    salt = SecureRandom.hex(16)
    new_hash = Digest::SHA256.hexdigest(salt + password)
    
    # Update database
    user.salt = salt
    user.new_hash = new_hash
    user.uses_salt = true
    user.save
    
    true
  end
  
  def authenticate(password)
    if uses_salt
      computed_hash = Digest::SHA256.hexdigest(salt + password)
      Rack::Utils.secure_compare(computed_hash, new_hash)
    else
      # Try migration on successful authentication
      computed_hash = Digest::SHA256.hexdigest(password)
      if Rack::Utils.secure_compare(computed_hash, old_hash)
        # Migrate to salted hash
        self.class.migrate_password(username, password)
        true
      else
        false
      end
    end
  end
end

A production-ready password system combines salt, pepper, and adaptive cost factors. This implementation adjusts bcrypt cost based on server performance and includes monitoring.

require 'bcrypt'
require 'benchmark'

class AdaptivePasswordHasher
  PEPPER = ENV.fetch('PASSWORD_PEPPER')
  TARGET_HASH_TIME = 0.5  # Target 500ms per hash
  MIN_COST = 10
  MAX_COST = 16
  
  # Determine appropriate cost factor
  def self.calibrate_cost
    cost = MIN_COST
    
    while cost <= MAX_COST
      time = Benchmark.realtime do
        BCrypt::Password.create("test_password", cost: cost)
      end
      
      return cost if time >= TARGET_HASH_TIME
      cost += 1
    end
    
    MAX_COST
  end
  
  def self.hash_password(password, cost: nil)
    cost ||= calibrate_cost
    peppered = password + PEPPER
    
    {
      hash: BCrypt::Password.create(peppered, cost: cost),
      cost: cost,
      created_at: Time.now
    }
  end
  
  def self.verify_password(password, password_data)
    peppered = password + PEPPER
    
    result = BCrypt::Password.new(password_data[:hash]) == peppered
    
    # Log verification timing for monitoring
    log_verification(password_data[:cost], result)
    
    result
  end
  
  def self.needs_rehash?(password_data)
    current_cost = calibrate_cost
    password_data[:cost] < current_cost
  end
  
  private
  
  def self.log_verification(cost, success)
    # Monitor authentication patterns
    puts "Password verification: cost=#{cost} success=#{success}"
  end
end

Handling pepper rotation in a production system requires careful coordination. This example implements versioned pepper with gradual migration.

class PepperRotationSystem
  PEPPERS = {
    1 => ENV.fetch('PASSWORD_PEPPER_V1'),
    2 => ENV.fetch('PASSWORD_PEPPER_V2'),
    3 => ENV.fetch('PASSWORD_PEPPER_V3')
  }
  
  CURRENT_VERSION = 3
  MINIMUM_VERSION = 1  # Versions below this are invalid
  
  def self.hash_password(password)
    pepper = PEPPERS[CURRENT_VERSION]
    peppered = password + pepper
    
    {
      hash: BCrypt::Password.create(peppered, cost: 12),
      pepper_version: CURRENT_VERSION,
      updated_at: Time.now
    }
  end
  
  def self.verify_and_upgrade(password, password_data)
    version = password_data[:pepper_version]
    
    # Reject outdated pepper versions
    return nil if version < MINIMUM_VERSION
    
    # Get pepper for this version
    pepper = PEPPERS[version]
    return nil unless pepper
    
    # Verify with original pepper
    peppered = password + pepper
    is_valid = BCrypt::Password.new(password_data[:hash]) == peppered
    
    return nil unless is_valid
    
    # Opportunistic upgrade to current version
    if version < CURRENT_VERSION
      new_password_data = hash_password(password)
      return { success: true, upgrade: new_password_data }
    end
    
    { success: true, upgrade: nil }
  end
  
  def self.force_upgrade_user(user_id, password)
    # Force upgrade during password change or reset
    user = find_user(user_id)
    
    # Verify current password with any valid pepper version
    (MINIMUM_VERSION..CURRENT_VERSION).each do |version|
      pepper = PEPPERS[version]
      next unless pepper
      
      peppered = password + pepper
      if BCrypt::Password.new(user.password_hash) == peppered
        # Upgrade to current version
        user.password_data = hash_password(password)
        user.save
        return true
      end
    end
    
    false
  end
  
  def self.rotation_status
    users_per_version = {}
    
    User.find_each do |user|
      version = user.password_data[:pepper_version] || 1
      users_per_version[version] ||= 0
      users_per_version[version] += 1
    end
    
    {
      current_version: CURRENT_VERSION,
      distribution: users_per_version,
      outdated_count: users_per_version.select { |v, _| v < CURRENT_VERSION }
                                       .values.sum
    }
  end
end

# Usage during authentication
def authenticate_user(username, password)
  user = find_user(username)
  return false unless user
  
  result = PepperRotationSystem.verify_and_upgrade(password, user.password_data)
  return false unless result && result[:success]
  
  # Apply upgrade if available
  if result[:upgrade]
    user.password_data = result[:upgrade]
    user.save
    puts "Upgraded user #{username} to pepper version #{PepperRotationSystem::CURRENT_VERSION}"
  end
  
  true
end

Reference

Salt and Pepper Characteristics

Property Salt Pepper
Storage Location Database with hash Environment variable or key store
Uniqueness Unique per password Shared across system
Visibility Public within database Secret, not in database
Length Requirement Minimum 16 bytes Matches hash output size
Purpose Prevent rainbow tables Defense in depth against database compromise
Rotation Frequency Never (generated once) Occasional (requires rehashing)
Generation Method Cryptographically secure random Cryptographically secure random

Hash Algorithm Selection

Algorithm Work Factor Memory Usage Security Status Ruby Gem
bcrypt High (tunable) Low Recommended bcrypt
scrypt High (tunable) High (tunable) Recommended scrypt
Argon2 High (tunable) High (tunable) Most secure argon2
PBKDF2 Medium (tunable) Low Acceptable OpenSSL
SHA-256 None Low Not recommended Digest
MD5 None Low Broken Digest

Common Implementation Patterns

Pattern Salt Placement Pepper Placement Use Case
Basic Salt Prepend to password None Minimum security requirement
Salt with Pepper Prepend to password Append after password High security applications
Versioned Pepper Prepend to password Append with version Systems requiring pepper rotation
HSM Pepper Prepend to password Applied in HSM Maximum security environments

Security Checklist

Check Requirement Status
Salt length Minimum 16 bytes Required
Salt uniqueness Unique per password Required
Salt generation SecureRandom or equivalent Required
Hash algorithm bcrypt, scrypt, or Argon2 Required
Pepper length Matches hash output size Optional
Pepper storage Separate from database Required if used
Constant-time comparison Rack::Utils.secure_compare Required
Password minimum length 8 characters minimum Recommended
Work factor 50-500ms hash time Recommended

BCrypt Configuration

Parameter Default Recommended Description
cost 10 12-14 Number of iterations (2^cost)
salt_len 16 bytes 16 bytes Salt length in bytes
hash_len 23 bytes 23 bytes Output hash length

Argon2 Configuration

Parameter Type Recommended Description
t_cost Iterations 2-4 Number of iterations
m_cost Memory 65536 KiB Memory usage in KiB
p_cost Parallelism 1-4 Parallelism degree
salt_len Bytes 16 Salt length in bytes
hash_len Bytes 32 Output hash length

Error Messages

Error Cause Resolution
BCrypt::Errors::InvalidHash Corrupted hash format Regenerate password hash
ArgumentError: invalid base64 Incorrect salt encoding Use Base64.strict_encode64
Timing attack detected Non-constant-time comparison Use Rack::Utils.secure_compare
Pepper version not found Missing pepper configuration Add pepper to environment
Unknown pepper version Outdated pepper version Implement version migration