CrackedRuby CrackedRuby

Overview

Secure random generation creates values that cannot be predicted or reproduced by an attacker, even with knowledge of previous outputs. Standard random number generators prioritize speed and statistical distribution, making them deterministic and unsuitable for security contexts. Cryptographically secure random number generators (CSPRNGs) derive randomness from sources of entropy and apply cryptographic algorithms to produce outputs resistant to prediction.

The distinction matters in security-sensitive operations: session identifiers, password reset tokens, cryptographic keys, initialization vectors, and authentication challenges all require unpredictability. A predictable random value compromises the entire security mechanism. An attacker who can predict session tokens gains unauthorized access; predictable cryptographic keys expose encrypted data; predictable password reset tokens allow account takeover.

Operating systems provide entropy sources through hardware events: keyboard timing, mouse movements, disk I/O patterns, network packet arrival times, and hardware random number generators. These sources feed entropy pools that CSPRNGs tap to generate secure random values. The quality of randomness depends on entropy availability, particularly during system startup when entropy pools may not be fully populated.

# Insecure: Predictable random generation
token = Random.rand(1000000)
# An attacker can predict this sequence

# Secure: Cryptographically secure generation
require 'securerandom'
token = SecureRandom.hex(32)
# Output is unpredictable even with knowledge of previous tokens

Ruby's SecureRandom module provides interface to operating system CSPRNGs, wrapping platform-specific implementations: /dev/urandom on Unix-like systems, CryptGenRandom on Windows, and OpenSSL's random number generator as fallback. This abstraction handles platform differences while maintaining security guarantees.

Key Principles

Cryptographic randomness requires three fundamental properties: unpredictability, non-reproducibility, and uniform distribution. Unpredictability means an attacker cannot determine future outputs given all previous outputs. Non-reproducibility prevents regenerating the same sequence. Uniform distribution ensures each possible value has equal probability, preventing bias that attackers could exploit.

Entropy represents the uncertainty or randomness in a system, measured in bits. A 128-bit random value has 2^128 possible values; sufficient entropy ensures an attacker cannot feasibly guess or brute-force the value. Insufficient entropy leads to predictable outputs: if a CSPRNG has only 32 bits of actual entropy, an attacker can test all 4 billion possibilities regardless of output length.

CSPRNGs maintain internal state seeded from entropy sources. The state advances through cryptographic transformations that make the output sequence unpredictable without knowing the internal state. Forward secrecy ensures that compromising the current state doesn't reveal previous outputs. Backward secrecy ensures compromising the current state doesn't make future outputs predictable after reseeding.

# Entropy and output length
token_16 = SecureRandom.hex(16)  # 16 bytes = 128 bits entropy
token_32 = SecureRandom.hex(32)  # 32 bytes = 256 bits entropy

# Same principle applies to different formats
uuid = SecureRandom.uuid           # 122 bits entropy (UUID v4)
base64 = SecureRandom.base64(24)   # 24 bytes = 192 bits entropy
random_number = SecureRandom.random_number(100)  # Uniform in range [0, 100)

Timing attacks exploit non-constant-time operations. Comparing tokens character-by-character leaks information through execution time: matching prefixes take longer to reject. Constant-time comparison operations prevent this by always examining all characters regardless of when a mismatch occurs.

The birthday paradox affects collision probability: with 23 people, probability exceeds 50% that two share a birthday. For random tokens, collisions become probable much sooner than intuition suggests. A 64-bit token space experiences 50% collision probability after approximately 2^32 (4 billion) tokens. This mathematical reality requires sufficient token length for the token space size.

Reseeding refreshes internal state with new entropy. CSPRNGs typically reseed automatically after generating a threshold number of bytes or after a time interval. Manual reseeding may be necessary in specific contexts like fork operations where child processes inherit parent state.

Security Implications

Predictable randomness creates exploitable vulnerabilities across application layers. Session fixation attacks succeed when attackers can predict or force session identifiers. Account enumeration exploits predictable password reset tokens to identify valid accounts. Cryptographic weaknesses emerge from insufficient randomness in key generation, making encrypted data vulnerable to recovery.

An attacker with the capability to predict random values gains several attack vectors. Session hijacking becomes trivial when session tokens follow predictable patterns. Password reset token prediction allows unauthorized password changes. CSRF token prediction bypasses cross-site request forgery protections. Cryptographic nonce prediction enables replay attacks and breaks encryption schemes requiring unique initialization vectors.

# VULNERABLE: Predictable session token
session_id = "session_#{Time.now.to_i}_#{Process.pid}"
# Attacker can guess this from timestamp and process enumeration

# SECURE: Unpredictable session token
session_id = SecureRandom.urlsafe_base64(32)
# 32 bytes = 256 bits, computationally infeasible to predict

Insufficient token length creates collision vulnerabilities. A 32-bit token space (4 billion values) reaches 50% collision probability after 77,000 tokens, making collision attacks feasible. A 64-bit space reaches 50% collision after 5 billion tokens, still within attacker capability given sufficient resources. A 128-bit space requires 2^64 tokens for 50% collision probability, effectively eliminating collision attacks.

# VULNERABLE: Short token (32 bits = 4 bytes)
short_token = SecureRandom.hex(4)
# Collision-prone in systems with many tokens

# SECURE: Long token (128 bits = 16 bytes minimum)
secure_token = SecureRandom.hex(16)
# Collision probability negligible even with billions of tokens

# PREFERRED: Extra margin (256 bits = 32 bytes)
extra_secure_token = SecureRandom.hex(32)
# Provides future-proof security margin

Timing attacks extract secret information through execution time measurement. Token comparison using string equality operators short-circuits on first mismatch, revealing partial token knowledge through timing differences. Secure comparison requires constant-time algorithms that examine all characters.

# VULNERABLE: Timing attack susceptible
def verify_token_unsafe(provided, expected)
  provided == expected  # Short-circuits on mismatch
end

# SECURE: Constant-time comparison
def verify_token_safe(provided, expected)
  Rack::Utils.secure_compare(provided, expected)
end

# Alternative: ActiveSupport implementation
def verify_token_activesupport(provided, expected)
  ActiveSupport::SecurityUtils.secure_compare(provided, expected)
end

Token storage requires the same security considerations as passwords. Storing tokens in plain text exposes them to database compromises. Hashing tokens before storage protects against database breaches while allowing verification. Use cryptographic hash functions, not encryption, as decryption capability creates additional attack surface.

# Token generation and storage pattern
class APIKey
  def self.generate
    token = SecureRandom.hex(32)
    digest = Digest::SHA256.hexdigest(token)
    # Store digest in database, return token to user once
    [token, digest]
  end
  
  def self.verify(provided_token, stored_digest)
    provided_digest = Digest::SHA256.hexdigest(provided_token)
    Rack::Utils.secure_compare(provided_digest, stored_digest)
  end
end

Randomness failures in virtual environments stem from entropy starvation. Virtual machines lack direct hardware access for entropy gathering, particularly after clone operations or during early boot. Cloud instances may start with depleted entropy pools. Container environments inherit entropy from host systems but may face contention. Monitor entropy availability in production environments and ensure adequate entropy sources.

Ruby Implementation

Ruby's SecureRandom module provides multiple methods for generating cryptographically secure random values in different formats. The module abstracts platform-specific CSPRNG implementations, automatically selecting the most appropriate source: /dev/urandom on Unix-like systems, CryptGenRandom on Windows, or OpenSSL's random number generator as fallback.

The hex method generates hexadecimal strings containing twice as many characters as the byte count specified. Each byte produces two hexadecimal digits (0-9, a-f). This format works well for tokens, identifiers, and keys where human readability matters less than length and security.

require 'securerandom'

# Generate 16 bytes (128 bits) as 32 hex characters
token = SecureRandom.hex(16)
# => "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"

# Generate 32 bytes (256 bits) as 64 hex characters  
long_token = SecureRandom.hex(32)

# Default generates 16 bytes
default_token = SecureRandom.hex
# => 32 hex characters

The base64 method generates Base64-encoded strings, producing more compact representations than hexadecimal. Base64 encoding converts 3 bytes into 4 characters, resulting in approximately 33% overhead compared to 100% overhead for hexadecimal. Use Base64 for URL parameters, headers, or storage where space efficiency matters.

# Generate 24 bytes (192 bits) as Base64
token = SecureRandom.base64(24)
# => "3Kj5+8xP2mN/4qW9vL1sY7eR6tU0zA=="

# Note: Base64 includes +, /, and = which may need encoding for URLs

The urlsafe_base64 method generates Base64 strings suitable for URLs by replacing + with -, / with _, and removing padding =. This format works directly in URLs without requiring additional encoding, making it ideal for web tokens, API keys, and URL parameters.

# URL-safe Base64 without padding
token = SecureRandom.urlsafe_base64(32)
# => "xY3mK9pL2nQ7rS8tU4vW6zA1bC5dE0fG"

# Safe for URLs and filenames
url = "https://api.example.com/reset?token=#{token}"

The random_bytes method generates raw binary data, returning a string with ASCII-8BIT encoding. Use this for cryptographic operations requiring binary input, like key derivation or encryption initialization vectors. The output requires encoding for text-based transmission or storage.

# Generate 32 random bytes
raw_bytes = SecureRandom.random_bytes(32)
# => "\x8A\x2F\x9D\x4C..." (binary data)

# Convert to hex for display
raw_bytes.unpack1('H*')
# => "8a2f9d4c..."

# Convert to Base64 for transmission
Base64.strict_encode64(raw_bytes)

The uuid method generates version 4 UUIDs (Universally Unique Identifiers) following RFC 4122. These 128-bit identifiers use 122 random bits with 6 bits reserved for version and variant indicators. UUIDs provide standardized format recognized by databases, APIs, and systems expecting UUID identifiers.

# Generate version 4 UUID
id = SecureRandom.uuid
# => "f47ac10b-58cc-4372-a567-0e02b2c3d479"

# UUIDs have standard format: 8-4-4-4-12 hexadecimal digits
# Example usage in database primary keys
class User < ApplicationRecord
  before_create :generate_uuid
  
  private
  
  def generate_uuid
    self.id = SecureRandom.uuid
  end
end

The random_number method generates random integers or floats. Without arguments, it generates a float in range [0.0, 1.0). With an integer argument, it generates an integer in range [0, n). With a range argument, it generates a value within that range. This method maintains uniform distribution critical for unbiased selection.

# Random float [0.0, 1.0)
float = SecureRandom.random_number
# => 0.8374629472

# Random integer [0, 100)
int = SecureRandom.random_number(100)
# => 42

# Random integer in range [50, 100)
range_int = SecureRandom.random_number(50..99)
# => 73

# Random element from array
colors = ['red', 'green', 'blue']
random_color = colors[SecureRandom.random_number(colors.length)]

The alphanumeric method generates strings containing only letters (a-z, A-Z) and digits (0-9), producing human-friendly tokens without special characters. This format works well for user-facing tokens, temporary passwords, and verification codes where special characters cause confusion or transcription errors.

# Generate 20-character alphanumeric string
code = SecureRandom.alphanumeric(20)
# => "aB3dE9fGh2IjK4lMn7Op"

# Verification code (shorter, easier to type)
verification = SecureRandom.alphanumeric(6)
# => "X7kP2m"

Custom implementation patterns extend SecureRandom for specific requirements. Generating random elements from arrays, creating tokens with specific character sets, or implementing custom encoding schemes all build on SecureRandom primitives.

# Custom character set token
CUSTOM_CHARSET = ('a'..'z').to_a + ('0'..'9').to_a - ['o', '0', 'i', '1']

def generate_readable_token(length)
  length.times.map { CUSTOM_CHARSET[SecureRandom.random_number(CUSTOM_CHARSET.length)] }.join
end

token = generate_readable_token(12)
# => "k3p7m2q9r8x5" (excludes confusable characters)

# Random selection with secure randomness
def secure_sample(array, count = 1)
  result = []
  remaining = array.dup
  
  count.times do
    break if remaining.empty?
    index = SecureRandom.random_number(remaining.length)
    result << remaining.delete_at(index)
  end
  
  count == 1 ? result.first : result
end

winners = secure_sample(['Alice', 'Bob', 'Carol', 'Dave'], 2)
# => ["Carol", "Alice"]

Practical Examples

Session token generation requires sufficient entropy and appropriate format. Web applications typically use URL-safe Base64 tokens stored in cookies or headers. The token length must provide adequate security margin against both prediction and collision attacks.

class SessionManager
  TOKEN_BYTES = 32  # 256 bits
  
  def self.create_session(user_id)
    token = SecureRandom.urlsafe_base64(TOKEN_BYTES)
    digest = Digest::SHA256.hexdigest(token)
    
    Session.create!(
      user_id: user_id,
      token_digest: digest,
      created_at: Time.now,
      expires_at: Time.now + 24.hours
    )
    
    token  # Return to client, never store plain text
  end
  
  def self.find_session(token)
    digest = Digest::SHA256.hexdigest(token)
    session = Session.find_by(token_digest: digest)
    
    return nil unless session
    return nil if session.expired?
    
    session
  end
  
  def self.verify(token, stored_digest)
    provided_digest = Digest::SHA256.hexdigest(token)
    Rack::Utils.secure_compare(provided_digest, stored_digest)
  end
end

# Usage
token = SessionManager.create_session(user.id)
cookies.encrypted[:session_token] = {
  value: token,
  httponly: true,
  secure: true,
  same_site: :strict
}

Password reset tokens require single-use design and expiration. Tokens should expire after short periods (15-60 minutes) and invalidate after use. Store only hashed tokens to protect against database compromise. Include user identifier in token lookup to prevent enumeration attacks.

class PasswordReset
  TOKEN_BYTES = 32
  EXPIRATION_MINUTES = 30
  
  def self.create_for_user(user)
    token = SecureRandom.urlsafe_base64(TOKEN_BYTES)
    digest = Digest::SHA256.hexdigest(token)
    
    PasswordResetToken.create!(
      user_id: user.id,
      token_digest: digest,
      created_at: Time.now,
      expires_at: Time.now + EXPIRATION_MINUTES.minutes,
      used: false
    )
    
    # Send token via email, never log or store plaintext
    UserMailer.password_reset(user, token).deliver_later
    
    true
  end
  
  def self.verify_and_consume(token)
    digest = Digest::SHA256.hexdigest(token)
    reset = PasswordResetToken.find_by(
      token_digest: digest,
      used: false
    )
    
    return nil unless reset
    return nil if reset.expires_at < Time.now
    
    # Mark as used before returning
    reset.update!(used: true, used_at: Time.now)
    reset.user
  end
end

# Usage in controller
def create
  user = User.find_by(email: params[:email])
  # Always show success to prevent enumeration
  if user
    PasswordReset.create_for_user(user)
  end
  
  render json: { message: 'Password reset email sent if account exists' }
end

def update
  user = PasswordReset.verify_and_consume(params[:token])
  
  if user && user.update(password: params[:password])
    render json: { message: 'Password updated successfully' }
  else
    render json: { error: 'Invalid or expired token' }, status: :unauthorized
  end
end

API key generation for external service authentication requires collision resistance and revocation capability. Keys typically have longer lifespans than session tokens but need individual revocation without affecting other keys. Store key metadata (creation time, last use, permissions) alongside digest.

class APIKeyManager
  KEY_BYTES = 32
  PREFIX = 'sk_live_'  # Prefix identifies key type and environment
  
  def self.generate_for_account(account_id, name:, permissions: [])
    random_part = SecureRandom.urlsafe_base64(KEY_BYTES)
    key = "#{PREFIX}#{random_part}"
    digest = Digest::SHA256.hexdigest(key)
    
    api_key = APIKey.create!(
      account_id: account_id,
      name: name,
      key_digest: digest,
      permissions: permissions,
      created_at: Time.now,
      last_used_at: nil
    )
    
    # Return key once, store only digest
    { key: key, id: api_key.id }
  end
  
  def self.authenticate(key)
    # Reject keys without proper prefix
    return nil unless key.start_with?(PREFIX)
    
    digest = Digest::SHA256.hexdigest(key)
    api_key = APIKey.find_by(key_digest: digest, revoked: false)
    
    return nil unless api_key
    
    # Update last used timestamp
    api_key.update_column(:last_used_at, Time.now)
    api_key
  end
  
  def self.revoke(api_key_id)
    APIKey.find(api_key_id).update!(
      revoked: true,
      revoked_at: Time.now
    )
  end
end

# Usage in API authentication
class APIAuthenticator
  def authenticate_request(request)
    auth_header = request.headers['Authorization']
    return nil unless auth_header
    
    key = auth_header.sub(/^Bearer\s+/, '')
    api_key = APIKeyManager.authenticate(key)
    
    return nil unless api_key
    
    # Check permissions for requested action
    api_key if api_key.has_permission?(request.controller, request.action)
  end
end

Cryptographic key generation for encryption requires appropriate key lengths for the cipher. AES-256 requires 256-bit (32-byte) keys. Generate keys from secure random bytes, not from passwords or predictable sources. Derive keys from passwords using key derivation functions like PBKDF2 or Argon2, not by directly hashing passwords.

require 'openssl'

class EncryptionKeyManager
  KEY_SIZE = 32  # 256 bits for AES-256
  IV_SIZE = 16   # 128 bits for AES block size
  
  def self.generate_key
    SecureRandom.random_bytes(KEY_SIZE)
  end
  
  def self.generate_iv
    SecureRandom.random_bytes(IV_SIZE)
  end
  
  def self.encrypt(plaintext, key)
    cipher = OpenSSL::Cipher.new('AES-256-CBC')
    cipher.encrypt
    cipher.key = key
    iv = cipher.random_iv  # Uses secure random internally
    
    ciphertext = cipher.update(plaintext) + cipher.final
    
    # Return IV with ciphertext (IV doesn't need to be secret)
    { iv: iv, ciphertext: ciphertext }
  end
  
  def self.decrypt(ciphertext, key, iv)
    cipher = OpenSSL::Cipher.new('AES-256-CBC')
    cipher.decrypt
    cipher.key = key
    cipher.iv = iv
    
    cipher.update(ciphertext) + cipher.final
  end
end

# Usage
key = EncryptionKeyManager.generate_key
encrypted = EncryptionKeyManager.encrypt('sensitive data', key)
# Store encrypted[:iv] and encrypted[:ciphertext]

decrypted = EncryptionKeyManager.decrypt(
  encrypted[:ciphertext],
  key,
  encrypted[:iv]
)

Common Pitfalls

Using standard Random or rand for security purposes creates predictable outputs. These generators use deterministic algorithms suitable for simulations or games but completely inappropriate for security contexts. Attackers can seed identical generators and reproduce the entire sequence.

# WRONG: Predictable random generation
srand(Time.now.to_i)  # Seed from timestamp
session_id = rand(1_000_000_000)
# Only 1 billion possibilities, easily brute-forced

# WRONG: Using Random class
random = Random.new(12345)  # Deterministic seed
token = random.rand(1000000)
# Given seed, all outputs predictable

# CORRECT: Cryptographically secure
session_id = SecureRandom.hex(32)

Generating tokens from predictable components like timestamps, PIDs, or sequential counters exposes patterns. Combining multiple weak sources doesn't create strength; attackers can predict or narrow down each component independently.

# WRONG: Predictable token components
token = "#{Time.now.to_i}_#{Process.pid}_#{SecureRandom.hex(4)}"
# Timestamp and PID are predictable, only 8 hex chars provide security

# WRONG: Sequential token generation
@counter ||= 0
token = "user_#{@counter += 1}_#{Date.today}"
# Fully predictable sequence

# CORRECT: Purely random token
token = SecureRandom.urlsafe_base64(32)

Insufficient token length creates collision vulnerabilities and brute-force opportunities. Tokens shorter than 128 bits allow feasible collision attacks. Tokens shorter than 64 bits enable practical brute-force attacks.

# WRONG: Too short (32 bits)
short_token = SecureRandom.hex(4)  # Only 4 bytes
# 2^32 = 4.3 billion possibilities, feasibly searchable

# WRONG: Barely adequate (64 bits)
medium_token = SecureRandom.hex(8)  # Only 8 bytes
# Collision-prone at scale

# CORRECT: Adequate security (128+ bits)
secure_token = SecureRandom.hex(16)  # 16 bytes minimum

# PREFERRED: Future-proof (256 bits)
strong_token = SecureRandom.hex(32)  # 32 bytes

Using timing-unsafe comparison allows timing attacks. String equality operators short-circuit on first mismatch, revealing information through execution time differences. Attackers can iteratively guess tokens character-by-character.

# WRONG: Timing-vulnerable comparison
def verify_token(provided, expected)
  provided == expected  # Leaks timing information
end

# WRONG: Early return on length mismatch
def verify_token(provided, expected)
  return false if provided.length != expected.length
  provided == expected  # Still vulnerable
end

# CORRECT: Constant-time comparison
def verify_token(provided, expected)
  Rack::Utils.secure_compare(provided, expected)
end

Storing unhashed tokens exposes them in database compromises. Treat tokens like passwords: hash before storage, verify by hashing and comparing digests. Never store plaintext tokens.

# WRONG: Storing plaintext tokens
token = SecureRandom.hex(32)
Token.create!(value: token, user_id: user.id)
# Database breach exposes all tokens

# CORRECT: Storing hashed tokens
token = SecureRandom.hex(32)
digest = Digest::SHA256.hexdigest(token)
Token.create!(digest: digest, user_id: user.id)
# Return token to user once, store only digest

# Verification
def verify(provided)
  digest = Digest::SHA256.hexdigest(provided)
  Token.exists?(digest: digest)
end

Reusing nonces or initialization vectors breaks cryptographic schemes. Each encryption operation requires fresh random values. Never store or reuse IVs across sessions.

# WRONG: Reusing IV
FIXED_IV = SecureRandom.random_bytes(16)  # Generated once
cipher.iv = FIXED_IV  # Reused for every encryption
# Breaks security, allows attacks

# CORRECT: Fresh IV per encryption
cipher.iv = SecureRandom.random_bytes(16)  # New for each operation

Seeding CSPRNGs manually interferes with proper entropy collection. SecureRandom manages seeding internally from system entropy sources. Manual seeding may weaken security by introducing predictable elements.

# WRONG: Manual seeding attempt
SecureRandom.seed(Time.now.to_i)  # No such method, but demonstrates antipattern
# If this worked, would introduce predictability

# CORRECT: Let SecureRandom manage its own seeding
token = SecureRandom.hex(32)  # Uses system entropy automatically

Error Handling & Edge Cases

Entropy starvation occurs when system entropy pools deplete, blocking random generation. This primarily affects systems during early boot, in virtual machines, or after fork operations. Monitor entropy availability through /proc/sys/kernel/random/entropy_avail on Linux systems.

# Check entropy availability (Linux)
def check_entropy
  entropy = File.read('/proc/sys/kernel/random/entropy_avail').to_i
  warn "Low entropy: #{entropy} bits" if entropy < 100
  entropy
rescue Errno::ENOENT
  # Not on Linux, entropy checking unavailable
  nil
end

# Generate with entropy check
def generate_secure_token(bytes = 32)
  entropy = check_entropy
  if entropy && entropy < 100
    # Log warning but proceed - SecureRandom will block if necessary
    Rails.logger.warn("Generating token with low entropy: #{entropy} bits")
  end
  
  SecureRandom.hex(bytes)
end

Exception handling for random generation failures covers rare but possible scenarios. SecureRandom may raise NotImplementedError if no secure random source is available, though this occurs only on misconfigured or very unusual systems.

def generate_token_with_fallback
  SecureRandom.hex(32)
rescue NotImplementedError => e
  # No secure random source available
  Rails.logger.error("SecureRandom unavailable: #{e.message}")
  raise SecurityError, "Cannot generate secure random values on this system"
rescue => e
  Rails.logger.error("Token generation failed: #{e.message}")
  raise
end

Virtual machine cloning creates identical entropy states across clones. Child processes inherit parent state before diverging. This creates potential for duplicate tokens immediately after cloning. Detect virtualization and clone events, forcing reseeding after clone operations.

# Detect and handle fork events
def after_fork_reseed
  # Trigger new entropy gathering after fork
  SecureRandom.random_bytes(1)  # Forces reseed in some implementations
end

# In Rails, handle process forking
if defined?(PhusionPassenger)
  PhusionPassenger.on_event(:starting_worker_process) do |forked|
    after_fork_reseed if forked
  end
end

Length validation prevents truncation attacks and ensures minimum security levels. Validate token length before storage and comparison to prevent shortened tokens from matching longer stored values.

def validate_and_store_token(token, min_length: 32)
  # Validate length before processing
  raise ArgumentError, "Token too short" if token.length < min_length
  
  # Validate character set for URL-safe tokens
  unless token.match?(/\A[A-Za-z0-9_-]+\z/)
    raise ArgumentError, "Invalid token format"
  end
  
  digest = Digest::SHA256.hexdigest(token)
  Token.create!(digest: digest)
end

Handling token expiration and cleanup prevents accumulation of expired tokens. Implement periodic cleanup jobs and check expiration before verification.

class TokenCleanupJob
  def perform
    # Remove expired tokens
    Token.where('expires_at < ?', Time.now).delete_all
    
    # Remove old unused tokens (e.g., 90 days)
    Token.where('created_at < ? AND last_used_at IS NULL', 90.days.ago).delete_all
  end
end

# Verification with expiration check
def verify_with_expiration(token)
  digest = Digest::SHA256.hexdigest(token)
  record = Token.find_by(digest: digest)
  
  return nil unless record
  return nil if record.expires_at < Time.now
  
  record
end

Reference

SecureRandom Methods

Method Returns Description Use Case
hex(n) String 2n hexadecimal characters General tokens, IDs
base64(n) String Base64 encoded, ~4n/3 characters Compact tokens
urlsafe_base64(n) String URL-safe Base64, ~4n/3 characters URL parameters, filenames
random_bytes(n) String n bytes of binary data Cryptographic operations
uuid String UUID v4 format Database keys, identifiers
random_number Float Float in [0.0, 1.0) Probability, ratios
random_number(n) Integer Integer in [0, n) Array indexing, ranges
random_number(range) Integer Integer within range Bounded selection
alphanumeric(n) String n alphanumeric characters User-facing codes

Recommended Token Lengths

Use Case Minimum Bits Recommended Bytes Format
Session tokens 128 16 urlsafe_base64(16)
API keys 256 32 urlsafe_base64(32)
Password reset 256 32 urlsafe_base64(32)
CSRF tokens 128 16 urlsafe_base64(16)
Verification codes 64 8 alphanumeric(12)
UUIDs 122 16 uuid
Cryptographic keys 256 32 random_bytes(32)
IVs (AES) 128 16 random_bytes(16)
Nonces 96 12 random_bytes(12)

Security Comparison Operations

Method Location Use Case
Rack::Utils.secure_compare Rack gem General constant-time comparison
ActiveSupport::SecurityUtils.secure_compare Rails Rails applications
OpenSSL.fixed_length_secure_compare OpenSSL gem Fixed-length comparisons

Entropy Sources by Platform

Platform Primary Source Fallback Notes
Linux /dev/urandom OpenSSL RAND Non-blocking, cryptographically secure
macOS /dev/urandom OpenSSL RAND Integrated with system entropy
Windows CryptGenRandom OpenSSL RAND Uses CPU instructions when available
BSD /dev/urandom OpenSSL RAND May block on low entropy

Common Token Format Patterns

Pattern Example Format
Hex token a1b2c3d4e5f6... hex(n)
URL-safe xY3mK9pL2nQ7... urlsafe_base64(n)
UUID f47ac10b-58cc-4372-a567-0e02b2c3d479 uuid
Prefixed key sk_live_xY3mK9pL... PREFIX + urlsafe_base64(n)
Alphanumeric X7kP2mQ9R3... alphanumeric(n)

Collision Probability Approximations

Token Length Space Size 50% Collision After
32 bits 4.3 billion ~77,000 tokens
64 bits 18 quintillion ~5 billion tokens
96 bits 79 octillion ~10 trillion tokens
128 bits 340 undecillion ~10^19 tokens
256 bits ~10^77 Effectively never

Token Storage Best Practices

Aspect Requirement Implementation
Hashing SHA-256 minimum Digest::SHA256.hexdigest(token)
Comparison Constant-time Rack::Utils.secure_compare
Transmission HTTPS only Never transmit over HTTP
Storage Hash only Never store plaintext
Lifetime Expire appropriately Include expiration timestamp
Revocation Individual revocation Support per-token revocation
Logging Never log tokens Redact from logs