CrackedRuby CrackedRuby

Technical documentation covering key derivation functions for password hashing and cryptographic key generation, including Ruby implementations, security considerations, and algorithm selection criteria.

Overview

Key Derivation Functions (KDFs) transform passwords or other low-entropy secrets into cryptographic keys suitable for encryption, authentication, or secure storage. Unlike general-purpose hash functions designed for speed, KDFs intentionally slow down computation to resist brute-force and dictionary attacks against stored credentials.

The primary application of KDFs is password hashing for authentication systems. When users create accounts, systems apply KDFs to passwords before storage, making password databases useless to attackers even after data breaches. The computational cost of testing each password guess becomes prohibitively expensive when proper KDFs protect credentials.

KDFs serve a second purpose in cryptographic systems: deriving encryption keys from passwords. Users remember passwords but encryption algorithms require high-entropy binary keys. KDFs bridge this gap by expanding password entropy and producing keys that meet cryptographic requirements.

Modern KDFs incorporate three critical elements: computational cost parameters that control processing time, random salts that prevent pre-computation attacks, and well-studied algorithms resistant to specialized hardware attacks. The relationship between these elements determines the security level against different attack vectors.

require 'bcrypt'

# Password hashing for authentication
password = BCrypt::Password.create('user_password_123')
# => "$2a$12$K1j2k3l4m5n6o7p8q9r0st..."

# Verification during login
password == 'user_password_123'  # => true
password == 'wrong_password'      # => false

Key Principles

Key derivation functions operate on a fundamentally different design philosophy than cryptographic hash functions. While SHA-256 and similar algorithms optimize for speed to process large data volumes quickly, KDFs deliberately consume significant computational resources. This intentional slowness creates asymmetric cost: legitimate authentication systems hash passwords rarely (during registration and login), but attackers must hash billions of guesses when attempting to crack stolen databases.

The work factor or cost parameter controls computational requirements. In PBKDF2, this parameter specifies iteration counts, requiring the internal hash function to execute thousands or millions of times. Modern standards recommend at least 600,000 iterations for PBKDF2-HMAC-SHA256. Bcrypt uses a cost parameter representing logarithmic work (cost 12 means 2^12 = 4,096 iterations of the Blowfish key schedule). Argon2 expands this concept with separate time and memory parameters, creating memory-hard operations resistant to GPU and ASIC attacks.

Salts provide cryptographic uniqueness. A salt is random data combined with the password before derivation, ensuring that identical passwords produce different outputs. Without salts, attackers build rainbow tables—precomputed hash databases covering millions of common passwords. A single rainbow table would compromise all accounts using popular passwords. Random salts force attackers to compute hashes for each individual account, eliminating the economies of scale that make rainbow tables effective.

Salt generation requires cryptographic-quality randomness. Operating system random number generators (like /dev/urandom on Unix systems or CryptGenRandom on Windows) provide suitable entropy. Salt length varies by algorithm: PBKDF2 and bcrypt commonly use 16-byte salts, while Argon2 recommendations range from 16 to 32 bytes. The salt stores alongside the derived key—secrecy is unnecessary since the salt serves to prevent pre-computation, not to add confidentiality.

Output key length depends on the application. Password verification systems typically generate 32 to 64 bytes. Encryption key derivation produces keys matching the cipher requirements: 32 bytes for AES-256, 24 bytes for AES-192, or 16 bytes for AES-128. Some KDFs produce variable-length output through extension mechanisms, while others generate fixed lengths determined by the underlying hash function.

The composition of KDF operations involves several cryptographic primitives. PBKDF2 combines HMAC (Hash-based Message Authentication Code) with a cryptographic hash function, iterating to amplify computational cost. Bcrypt builds on the Blowfish cipher, repeatedly re-keying the cipher with password-derived data—a process expensive to implement in hardware. Scrypt chains multiple operations including PBKDF2 for initial key derivation followed by memory-intensive sequential operations. Argon2 implements parallel and sequential memory access patterns, with variants optimizing for different attack scenarios.

require 'openssl'

# PBKDF2 with explicit parameters
password = 'secure_password'
salt = OpenSSL::Random.random_bytes(16)
iterations = 600_000
key_length = 32

derived_key = OpenSSL::PKCS5.pbkdf2_hmac(
  password,
  salt,
  iterations,
  key_length,
  OpenSSL::Digest::SHA256.new
)
# => 32 bytes of derived key material

# The same password with different salt produces different output
salt2 = OpenSSL::Random.random_bytes(16)
derived_key2 = OpenSSL::PKCS5.pbkdf2_hmac(
  password,
  salt,
  iterations,
  key_length,
  OpenSSL::Digest::SHA256.new
)

derived_key == derived_key2  # => false (different salts)

Ruby Implementation

Ruby provides KDF implementations through the standard library and specialized gems. The OpenSSL module includes PBKDF2 through the PKCS5 namespace. For bcrypt, the bcrypt gem offers the standard Ruby interface. Argon2 requires the argon2 gem, which wraps the reference C implementation. Each approach balances portability, performance, and security features.

The OpenSSL::PKCS5 module exposes the pbkdf2_hmac method, accepting a password string, salt bytes, iteration count, desired key length, and digest algorithm. This method returns raw binary data requiring encoding for storage. Common patterns convert output to hexadecimal or Base64 for database storage.

require 'openssl'
require 'base64'

# Generate salt and derive key
salt = OpenSSL::Random.random_bytes(16)
iterations = 600_000
derived_key = OpenSSL::PKCS5.pbkdf2_hmac(
  'password',
  salt,
  iterations,
  32,
  OpenSSL::Digest::SHA256.new
)

# Encode for storage
encoded_key = Base64.strict_encode64(derived_key)
encoded_salt = Base64.strict_encode64(salt)

# Store: "#{algorithm}$#{iterations}$#{encoded_salt}$#{encoded_key}"
stored_hash = "pbkdf2-sha256$#{iterations}$#{encoded_salt}$#{encoded_key}"

# Verification
def verify_password(password, stored_hash)
  algorithm, iterations, encoded_salt, encoded_key = stored_hash.split('$')
  salt = Base64.strict_decode64(encoded_salt)
  stored_key = Base64.strict_decode64(encoded_key)
  
  derived_key = OpenSSL::PKCS5.pbkdf2_hmac(
    password,
    salt,
    iterations.to_i,
    stored_key.length,
    OpenSSL::Digest::SHA256.new
  )
  
  # Constant-time comparison
  derived_key.bytes == stored_key.bytes
end

The bcrypt gem provides an object-oriented interface abstracting salt generation and format handling. The BCrypt::Password.create method generates salts automatically and returns an object containing the complete hash string. This string includes algorithm version, cost parameter, salt, and derived key in a standardized format.

require 'bcrypt'

# Hash password with default cost (12)
password = BCrypt::Password.create('user_password')
# => "$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW"

# Hash with custom cost
password = BCrypt::Password.create('user_password', cost: 14)

# Verification through comparison operator
password == 'user_password'     # => true
password == 'wrong_password'    # => false

# Access components
password.version   # => "2a"
password.cost      # => 14
password.salt      # => "$2a$14$R9h/cIPz0gi.URNNX3kh2O"
password.checksum  # => "PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW"

# Load existing hash from database
stored_hash = "$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW"
password = BCrypt::Password.new(stored_hash)
password == 'user_password'  # => true

Argon2 through the argon2 gem supports multiple variants. Argon2i optimizes resistance to side-channel attacks, Argon2d maximizes GPU resistance through data-dependent memory access, and Argon2id combines both approaches. The gem provides separate parameters for time cost (iterations), memory cost (kilobytes), and parallelism (threads).

require 'argon2'

# Default Argon2id with recommended parameters
hasher = Argon2::Password.new(
  t_cost: 2,      # Time cost (iterations)
  m_cost: 16,     # Memory cost (2^16 = 64 MB)
  p_cost: 1       # Parallelism (threads)
)

password = hasher.create('user_password')
# => "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$hash_data"

# Verification
hasher.verify_password('user_password', password)  # => true
hasher.verify_password('wrong_password', password) # => false

# Custom configuration for high-security applications
secure_hasher = Argon2::Password.new(
  t_cost: 4,
  m_cost: 18,     # 2^18 = 256 MB
  p_cost: 2,
  salt_len: 32,
  output_len: 64
)

password = secure_hasher.create('sensitive_password')

For applications requiring scrypt, the scrypt gem wraps the algorithm in a similar interface. Scrypt combines CPU and memory hardness, though Argon2 generally supersedes it in modern implementations.

require 'scrypt'

# Create password hash
password = SCrypt::Password.create('user_password', cost: 16384)

# Verification
password == 'user_password'  # => true

# Access parameters
password.cost        # => "16384$8$1"
password.salt        # => base64-encoded salt

Practical Examples

Password registration and authentication flows demonstrate typical KDF usage. During registration, the system generates a salt, applies the KDF, and stores the result. Authentication retrieves the stored hash and derives a new key from the submitted password for comparison.

require 'bcrypt'

class UserAccount
  attr_reader :username, :password_hash
  
  def initialize(username, password)
    @username = username
    @password_hash = BCrypt::Password.create(password, cost: 12)
  end
  
  def authenticate(password_attempt)
    BCrypt::Password.new(@password_hash) == password_attempt
  end
  
  def update_password(old_password, new_password)
    return false unless authenticate(old_password)
    @password_hash = BCrypt::Password.create(new_password, cost: 12)
    true
  end
end

# User registration
user = UserAccount.new('alice', 'initial_password_123')

# Login attempt
user.authenticate('initial_password_123')  # => true
user.authenticate('wrong_guess')           # => false

# Password change
user.update_password('initial_password_123', 'new_secure_password')
user.authenticate('new_secure_password')   # => true

Encryption key derivation from passwords requires deterministic output for the same password-salt combination. The derived key initializes encryption algorithms, while the salt stores alongside encrypted data for later key reconstruction during decryption.

require 'openssl'

class PasswordEncryption
  ITERATIONS = 600_000
  KEY_LENGTH = 32
  
  def self.encrypt(plaintext, password)
    # Generate random salt and IV
    salt = OpenSSL::Random.random_bytes(16)
    iv = OpenSSL::Random.random_bytes(16)
    
    # Derive encryption key from password
    key = OpenSSL::PKCS5.pbkdf2_hmac(
      password,
      salt,
      ITERATIONS,
      KEY_LENGTH,
      OpenSSL::Digest::SHA256.new
    )
    
    # Encrypt data
    cipher = OpenSSL::Cipher.new('AES-256-CBC')
    cipher.encrypt
    cipher.key = key
    cipher.iv = iv
    
    encrypted = cipher.update(plaintext) + cipher.final
    
    # Return salt + iv + ciphertext
    {
      salt: salt,
      iv: iv,
      ciphertext: encrypted
    }
  end
  
  def self.decrypt(encrypted_data, password)
    # Derive same encryption key
    key = OpenSSL::PKCS5.pbkdf2_hmac(
      password,
      encrypted_data[:salt],
      ITERATIONS,
      KEY_LENGTH,
      OpenSSL::Digest::SHA256.new
    )
    
    # Decrypt data
    decipher = OpenSSL::Cipher.new('AES-256-CBC')
    decipher.decrypt
    decipher.key = key
    decipher.iv = encrypted_data[:iv]
    
    decipher.update(encrypted_data[:ciphertext]) + decipher.final
  end
end

# Usage
plaintext = 'Sensitive information'
password = 'user_master_password'

encrypted = PasswordEncryption.encrypt(plaintext, password)
decrypted = PasswordEncryption.decrypt(encrypted, password)

plaintext == decrypted  # => true

Multi-factor authentication tokens benefit from KDF-derived keys. Hardware security keys or authenticator apps derive signing keys from master secrets, with KDFs ensuring compromise of stored secrets requires substantial computational effort.

require 'openssl'
require 'base64'

class TOTPGenerator
  def initialize(user_secret, username)
    @user_secret = user_secret
    @username = username
  end
  
  # Derive user-specific key from master secret
  def derive_key
    salt = "totp-#{@username}"
    OpenSSL::PKCS5.pbkdf2_hmac(
      @user_secret,
      salt,
      100_000,
      32,
      OpenSSL::Digest::SHA256.new
    )
  end
  
  def generate_token(timestamp = Time.now.to_i)
    key = derive_key
    counter = timestamp / 30  # 30-second window
    
    # HMAC-based OTP generation
    counter_bytes = [counter].pack('Q>')
    hmac = OpenSSL::HMAC.digest(
      OpenSSL::Digest::SHA256.new,
      key,
      counter_bytes
    )
    
    # Extract dynamic binary code
    offset = hmac[-1].ord & 0x0f
    code = hmac[offset, 4].unpack1('N') & 0x7fffffff
    
    # Generate 6-digit code
    (code % 1_000_000).to_s.rjust(6, '0')
  end
end

# Master secret (stored securely server-side)
master_secret = OpenSSL::Random.random_bytes(32)

# Per-user token generation
alice_totp = TOTPGenerator.new(master_secret, 'alice@example.com')
bob_totp = TOTPGenerator.new(master_secret, 'bob@example.com')

alice_totp.generate_token  # => "123456"
bob_totp.generate_token    # => "789012" (different due to username)

Migrating between KDF algorithms requires maintaining multiple hash formats during transition. Systems verify passwords against old hashes and upgrade to stronger algorithms upon successful authentication.

require 'bcrypt'
require 'digest'

class PasswordMigration
  def self.verify_and_upgrade(username, password, stored_hash)
    format = detect_format(stored_hash)
    
    case format
    when :legacy_sha256
      # Old insecure format
      if verify_legacy(password, stored_hash)
        new_hash = BCrypt::Password.create(password, cost: 12)
        update_database(username, new_hash, :bcrypt)
        return true
      end
    when :bcrypt
      bcrypt_hash = BCrypt::Password.new(stored_hash)
      if bcrypt_hash == password
        # Check if cost needs upgrading
        if bcrypt_hash.cost < 12
          new_hash = BCrypt::Password.create(password, cost: 12)
          update_database(username, new_hash, :bcrypt)
        end
        return true
      end
    end
    
    false
  end
  
  def self.detect_format(hash)
    if hash.start_with?('$2a$', '$2b$', '$2y$')
      :bcrypt
    elsif hash.match?(/\A[a-f0-9]{64}\z/)
      :legacy_sha256
    else
      :unknown
    end
  end
  
  def self.verify_legacy(password, hash)
    Digest::SHA256.hexdigest(password) == hash
  end
  
  def self.update_database(username, new_hash, format)
    # Database update logic
    puts "Upgraded #{username} to #{format}"
  end
end

# Migration scenario
stored_hash = 'ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f'
PasswordMigration.verify_and_upgrade('alice', 'password123', stored_hash)
# => Upgraded alice to bcrypt

Security Implications

The fundamental security property of KDFs is computational cost asymmetry. Authentication systems hash passwords once per user action, but attackers attempting to crack stolen databases must hash billions of password guesses. Proper KDF configuration makes each guess expensive, rendering brute-force attacks impractical even with dedicated hardware.

Algorithm selection significantly impacts security. PBKDF2 provides baseline security through iteration counts but remains vulnerable to GPU and ASIC acceleration since it operates on simple hash functions optimized for parallel execution. GPU clusters crack PBKDF2-protected passwords orders of magnitude faster than bcrypt. Bcrypt's memory-hard operations resist GPU optimization, but specialized FPGAs still achieve concerning speeds against it.

Argon2 represents current best practice for new systems. Its memory-hard design and configurable parallelism make both GPU and ASIC attacks expensive. The Argon2id variant combines protection against side-channel attacks (from Argon2i) with GPU resistance (from Argon2d). Systems without specific constraints should default to Argon2id with time cost 2, memory cost 2^16 (64 MB), and parallelism matching CPU cores.

Work factor configuration determines attack resistance. Insufficient work factors negate KDF benefits—attackers crack weak KDF parameters as easily as unsalted fast hashes. The target is computational cost that users tolerate during authentication (typically 100-500ms) but that multiplies into prohibitive totals when testing millions of passwords. Work factors require periodic increases as hardware improves, just as cryptographic key lengths grow over time.

For PBKDF2-HMAC-SHA256, current recommendations specify 600,000 iterations minimum. OWASP updates this guidance periodically as computing power increases. Applications should configure iteration counts in external configuration, allowing adjustment without code changes. Some systems implement adaptive work factors, measuring hash time during application startup and calculating iterations to achieve target latency.

require 'openssl'
require 'benchmark'

# Benchmark to determine appropriate iterations
def calibrate_pbkdf2(target_ms = 250)
  password = 'test_password'
  salt = OpenSSL::Random.random_bytes(16)
  test_iterations = 100_000
  
  time = Benchmark.measure do
    OpenSSL::PKCS5.pbkdf2_hmac(
      password,
      salt,
      test_iterations,
      32,
      OpenSSL::Digest::SHA256.new
    )
  end
  
  # Calculate iterations for target time
  actual_ms = time.real * 1000
  (test_iterations * target_ms / actual_ms).to_i
end

recommended_iterations = calibrate_pbkdf2
# => ~600000 on modern hardware

Salt security requires cryptographic randomness and adequate length. Weak random number generators produce predictable salts, enabling attackers to reduce the effective search space. Cryptographic RNGs like OpenSSL::Random use operating system entropy pools seeded from hardware sources, ensuring unpredictability. Salt length of 16 bytes provides 128 bits of entropy, making collision probability negligible even across billions of accounts.

Timing attacks against password verification allow attackers to learn password characteristics through response time measurements. Naive comparison code using early-exit logic reveals information through variable execution time. Constant-time comparison prevents timing leakage by examining every byte regardless of match status.

# Vulnerable to timing attacks
def insecure_verify(derived, stored)
  return false if derived.length != stored.length
  derived.bytes.zip(stored.bytes).all? { |a, b| a == b }
  # Early exit when mismatch found
end

# Constant-time comparison
def secure_verify(derived, stored)
  return false if derived.length != stored.length
  
  difference = 0
  derived.bytes.zip(stored.bytes).each do |a, b|
    difference |= a ^ b  # Accumulates differences without branching
  end
  
  difference.zero?
end

Storage format security involves proper encoding and format versioning. Storing algorithm parameters with hashes enables future algorithm migration and work factor increases. Format strings typically concatenate algorithm identifier, cost parameters, salt, and hash using delimiters. Base64 encoding handles binary data in text storage systems.

Pepper values add defense in depth by incorporating server-side secrets into KDF inputs. Unlike salts stored with password hashes, peppers remain in application configuration or hardware security modules. Compromising the database without accessing pepper values leaves hashes effectively uncrackable. Implementations apply peppers through HMAC with the KDF output or by concatenating with passwords before derivation.

require 'openssl'

class PepperedKDF
  # Pepper stored in environment, not database
  PEPPER = ENV['PASSWORD_PEPPER'] || raise('PEPPER not configured')
  
  def self.hash_password(password)
    salt = OpenSSL::Random.random_bytes(16)
    
    # Concatenate password and pepper
    peppered_password = password + PEPPER
    
    derived_key = OpenSSL::PKCS5.pbkdf2_hmac(
      peppered_password,
      salt,
      600_000,
      32,
      OpenSSL::Digest::SHA256.new
    )
    
    # Store only salt and hash, not pepper
    {
      salt: Base64.strict_encode64(salt),
      hash: Base64.strict_encode64(derived_key)
    }
  end
  
  def self.verify_password(password, stored_data)
    salt = Base64.strict_decode64(stored_data[:salt])
    stored_hash = Base64.strict_decode64(stored_data[:hash])
    
    peppered_password = password + PEPPER
    
    derived_key = OpenSSL::PKCS5.pbkdf2_hmac(
      peppered_password,
      salt,
      600_000,
      32,
      OpenSSL::Digest::SHA256.new
    )
    
    secure_verify(derived_key, stored_hash)
  end
  
  def self.secure_verify(a, b)
    return false if a.bytesize != b.bytesize
    diff = 0
    a.bytes.zip(b.bytes).each { |x, y| diff |= x ^ y }
    diff.zero?
  end
end

Performance Considerations

KDF performance characteristics differ fundamentally from optimization goals in most software. While typical algorithms minimize execution time, KDFs intentionally maximize computational cost within user experience constraints. The design challenge balances security (slower is stronger) against usability (users tolerate limited delays).

Authentication flow latency determines acceptable KDF cost. User registration and login tolerate 250-500ms delays on modern web applications. Mobile applications with lower-power processors might target 100-250ms. Background operations like password changes accept higher latency since users expect security-sensitive operations to take time. Work factor configuration must account for slowest supported client platforms—server-side hashing uses server CPU capabilities, while client-side hashing must accommodate mobile devices.

Algorithm selection impacts hardware requirements. PBKDF2's CPU-intensive operations perform well on general-purpose processors but allow parallel acceleration. A single CPU core might hash 1,000 PBKDF2 attempts per second, but a modern GPU exceeds 100,000 attempts per second. Bcrypt's memory dependencies reduce GPU efficiency to approximately 10x improvement over CPUs. Argon2's configurable memory cost enables tuning to available RAM—larger memory requirements decrease the CPU-to-GPU advantage further.

Benchmarking across algorithms reveals performance characteristics:

require 'benchmark'
require 'bcrypt'
require 'openssl'

def benchmark_kdfs
  password = 'test_password_123'
  iterations = 10
  
  puts "Benchmarking KDF performance (#{iterations} iterations):\n\n"
  
  Benchmark.bm(20) do |x|
    x.report("PBKDF2 (100k):") do
      iterations.times do
        salt = OpenSSL::Random.random_bytes(16)
        OpenSSL::PKCS5.pbkdf2_hmac(
          password, salt, 100_000, 32, OpenSSL::Digest::SHA256.new
        )
      end
    end
    
    x.report("PBKDF2 (600k):") do
      iterations.times do
        salt = OpenSSL::Random.random_bytes(16)
        OpenSSL::PKCS5.pbkdf2_hmac(
          password, salt, 600_000, 32, OpenSSL::Digest::SHA256.new
        )
      end
    end
    
    x.report("bcrypt (cost 10):") do
      iterations.times do
        BCrypt::Password.create(password, cost: 10)
      end
    end
    
    x.report("bcrypt (cost 12):") do
      iterations.times do
        BCrypt::Password.create(password, cost: 12)
      end
    end
  end
end

# Sample output on modern CPU:
# PBKDF2 (100k):         0.45s
# PBKDF2 (600k):         2.7s
# bcrypt (cost 10):      0.65s
# bcrypt (cost 12):      2.6s

Denial of service risks emerge from expensive KDF operations. Attackers submitting authentication requests with long passwords force servers to perform costly hash computations. Mitigation strategies include rate limiting authentication attempts, password length limits (often 72 bytes for bcrypt due to algorithm constraints, 128-256 bytes for others), and adaptive work factors that decrease under high load.

class RateLimitedAuth
  def initialize
    @attempts = Hash.new { |h, k| h[k] = [] }
    @max_attempts = 5
    @window = 300  # 5 minutes
  end
  
  def authenticate(username, password, password_hash)
    # Check rate limit
    now = Time.now.to_i
    @attempts[username].reject! { |t| t < now - @window }
    
    if @attempts[username].length >= @max_attempts
      return { success: false, reason: 'rate_limited' }
    end
    
    # Limit password length to prevent DoS
    if password.bytesize > 128
      return { success: false, reason: 'password_too_long' }
    end
    
    # Record attempt
    @attempts[username] << now
    
    # Perform verification
    result = BCrypt::Password.new(password_hash) == password
    { success: result, reason: result ? nil : 'invalid_credentials' }
  end
end

Memory requirements scale differently across algorithms. PBKDF2 uses minimal memory regardless of iterations—the hash function state plus small buffers for intermediate values. Bcrypt requires 4KB for the Blowfish state array. Scrypt and Argon2 use configurable amounts ranging from megabytes to gigabytes. Memory-hard algorithms trade memory for GPU resistance, but servers must provision RAM accordingly.

Argon2 memory configuration involves trade-offs:

# Low memory (16 MB) - suitable for constrained environments
low_memory = Argon2::Password.new(
  t_cost: 4,      # More time iterations
  m_cost: 14,     # 2^14 = 16 MB
  p_cost: 1
)

# Balanced (64 MB) - recommended for most applications
balanced = Argon2::Password.new(
  t_cost: 2,
  m_cost: 16,     # 2^16 = 64 MB
  p_cost: 1
)

# High memory (256 MB) - maximum security
high_memory = Argon2::Password.new(
  t_cost: 2,
  m_cost: 18,     # 2^18 = 256 MB
  p_cost: 2
)

Parallel hashing for bulk operations requires careful resource management. Background jobs processing password migrations or generating tokens for many users must limit concurrent KDF operations to prevent memory exhaustion or CPU saturation. Thread pools or worker queues throttle parallelism:

require 'concurrent'

class BulkPasswordHash
  def initialize(max_threads: 4)
    @pool = Concurrent::FixedThreadPool.new(max_threads)
  end
  
  def hash_passwords(users_and_passwords)
    promises = users_and_passwords.map do |user, password|
      Concurrent::Promise.execute(executor: @pool) do
        {
          user: user,
          hash: BCrypt::Password.create(password, cost: 12)
        }
      end
    end
    
    promises.map(&:value)
  ensure
    @pool.shutdown
    @pool.wait_for_termination
  end
end

# Process 100 password hashes with controlled parallelism
hasher = BulkPasswordHash.new(max_threads: 4)
results = hasher.hash_passwords(user_password_pairs)

Common Pitfalls

Using fast cryptographic hash functions for password storage remains the most prevalent error. SHA-256, SHA-512, and other general-purpose hash functions execute in microseconds, allowing attackers to test billions of passwords per second with commodity hardware. A password database protected only by SHA-256 falls to brute-force attacks in hours or days rather than years or decades. The misconception that cryptographic hashes provide security stems from confusion between data integrity (the design goal of SHA-family functions) and password protection (requiring computational expense).

# INSECURE - Never do this
require 'digest'
password_hash = Digest::SHA256.hexdigest('user_password')

# SECURE - Use proper KDF
require 'bcrypt'
password_hash = BCrypt::Password.create('user_password')

Missing or weak salts eliminate the computational cost multiplier that makes KDFs effective. Without salts, attackers generate rainbow tables once and test all accounts simultaneously. A single database of pre-computed hashes compromises millions of accounts. Using the same salt for all users provides minimal protection since attackers generate one rainbow table for that specific salt. Each user requires a unique, randomly-generated salt.

# INSECURE - Same salt for all users
GLOBAL_SALT = 'application_salt_123'
hash = OpenSSL::PKCS5.pbkdf2_hmac(password, GLOBAL_SALT, 600_000, 32, ...)

# SECURE - Random salt per user
salt = OpenSSL::Random.random_bytes(16)
hash = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, 600_000, 32, ...)

Insufficient work factors negate KDF benefits. Developers sometimes choose low iteration counts or bcrypt cost values to improve perceived application performance, inadvertently making passwords vulnerable. PBKDF2 with 1,000 iterations or bcrypt cost 4 provides negligible security over unsalted hashes. Work factor selection must align with current hardware capabilities and recommended standards.

Storing passwords in plaintext or reversible encryption represents catastrophic security failure but occurs more frequently than expected. Regulations increasingly mandate secure password storage, imposing penalties for plaintext storage. Encrypted passwords seem secure but face a key management problem—the encryption key must be available to the application, and an attacker compromising the application accesses the key along with encrypted passwords.

# INSECURE - Reversible encryption
cipher = OpenSSL::Cipher.new('AES-256-CBC')
encrypted_password = cipher.update(password) + cipher.final

# SECURE - One-way KDF
password_hash = BCrypt::Password.create(password)

Incorrect password comparison using string equality operators introduces timing attacks. Ruby's == operator for strings exits immediately upon finding mismatched characters. Measuring response times across many requests reveals password characteristics, narrowing the search space for attackers. Constant-time comparison examines every byte regardless of match or mismatch.

Re-hashing already hashed passwords compounds security rather than improving it. When migrating between systems or upgrading KDFs, developers sometimes apply the new KDF to existing hashes rather than waiting for user login to capture plaintext passwords. This creates nested KDFs that verify by double-hashing attacker guesses, but the stored value remains the old hash. Attackers crack the outer hash layer immediately and work on the inner hash as before.

# INSECURE - Wrapping old hash with new KDF
old_hash = stored_hash_from_database
new_hash = BCrypt::Password.create(old_hash)  # Wraps hash, not password

# SECURE - Upgrade on login when plaintext available
def upgrade_on_login(username, password, old_hash_format)
  if verify_old_format(password, old_hash_format)
    new_hash = BCrypt::Password.create(password)
    update_database(username, new_hash)
    return true
  end
  false
end

Exposing work factors or algorithm details in API responses leaks security configuration to attackers. While algorithm and iteration counts store with hashes for verification, public APIs should not disclose these details before authentication. Error messages distinguishing between invalid usernames and incorrect passwords enable username enumeration. Responses should remain identical for all authentication failures.

Client-side password hashing transfers security burden to untrusted code. JavaScript implementations of KDFs execute in browsers where attackers inspect and modify code. Proper password security requires server-side hashing with secure random salt generation. Client-side hashing provides minimal value while introducing complexity and potential vulnerabilities.

Ignoring password length limits allows denial of service attacks through memory exhaustion. Bcrypt truncates passwords beyond 72 bytes due to algorithm design, but applications should enforce limits before hashing. Extremely long passwords consume memory during hashing and enable attackers to degrade service performance.

class SecurePasswordHandler
  MAX_PASSWORD_LENGTH = 128
  
  def hash_password(password)
    if password.bytesize > MAX_PASSWORD_LENGTH
      raise ArgumentError, 'Password exceeds maximum length'
    end
    
    BCrypt::Password.create(password, cost: 12)
  end
end

Reference

Algorithm Comparison

Algorithm Best Use Case Primary Defense Memory Use GPU Resistance
PBKDF2 Legacy systems, compliance requirements Iteration count Minimal (few KB) Low
bcrypt General password hashing Memory-hard Blowfish 4 KB Medium
scrypt High-security applications Memory and CPU cost Configurable (MB to GB) High
Argon2id New systems, maximum security Configurable memory and time Configurable (MB to GB) Very High
Argon2i Side-channel protection Data-independent access Configurable (MB to GB) High
Argon2d GPU resistance priority Data-dependent access Configurable (MB to GB) Very High

Recommended Work Factors

Algorithm Parameter Minimum Value Recommended Value High Security Value
PBKDF2-HMAC-SHA256 Iterations 600,000 600,000 1,200,000
PBKDF2-HMAC-SHA512 Iterations 210,000 210,000 420,000
bcrypt Cost 12 12 14
scrypt N (CPU/memory cost) 32768 (2^15) 32768 65536 (2^16)
scrypt r (block size) 8 8 8
scrypt p (parallelization) 1 1 1
Argon2id Time cost 2 2 4
Argon2id Memory cost (power of 2) 15 (32 MB) 16 (64 MB) 18 (256 MB)
Argon2id Parallelism 1 1 2

Ruby Method Signatures

Method Parameters Returns Notes
OpenSSL::PKCS5.pbkdf2_hmac password, salt, iterations, key_len, digest Binary string Raw binary output requires encoding
BCrypt::Password.create password, cost: 12 BCrypt::Password object Includes formatted hash string
BCrypt::Password.new stored_hash_string BCrypt::Password object Loads existing hash for verification
Argon2::Password.new t_cost, m_cost, p_cost, salt_len, output_len Argon2::Password instance Configure before hashing
Argon2::Password#create password Formatted hash string Generates salt automatically
Argon2::Password#verify_password password, hash Boolean Constant-time comparison

Security Configuration Checklist

Requirement Specification Validation
Salt length Minimum 16 bytes Check entropy source
Salt randomness Cryptographic PRNG Use OpenSSL::Random
Work factor Meets current standards Benchmark 250-500ms
Algorithm version Argon2id or bcrypt Avoid PBKDF2 for new systems
Storage format Include algorithm and parameters Version for future migration
Comparison method Constant-time No early exit on mismatch
Password length limit 72-128 bytes Prevent DoS attacks
Rate limiting 5 attempts per 5 minutes Block brute force
Pepper (optional) 32+ bytes in secure config Separate from database

Migration Path Matrix

Current State Target State Migration Strategy Code Changes Required
Plain SHA-256 bcrypt Verify and upgrade on login Add legacy verification, bcrypt hashing
MD5 Argon2id Immediate forced reset Remove MD5, implement Argon2id
bcrypt cost 10 bcrypt cost 12 Upgrade on authentication Check cost, re-hash if below threshold
PBKDF2 100k PBKDF2 600k Upgrade on authentication Check iterations, re-hash if below threshold
bcrypt Argon2id Gradual migration on login Detect format, maintain both verifiers

Hash Storage Format Examples

PBKDF2 (custom format):
algorithm$iterations$base64_salt$base64_hash
pbkdf2-sha256$600000$LhPJ9XbZG...=$x8H9k3J...=

bcrypt (standard format):
$version$cost$salt_and_hash
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW

Argon2 (standard format):
$algorithm$version$params$salt$hash
$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$hash_data

scrypt (standard format):
$algorithm$params$salt$hash
$scrypt$ln=15,r=8,p=1$aM15713r3Xsvxbi31lqr1Q$nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD+iCs5E