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 |