How does rand::distributions::Alphanumeric generate secure random strings for tokens?

rand::distributions::Alphanumeric is a distribution that samples uniformly from the characters a-z, A-Z, and 0-9 (62 characters total), providing a convenient way to generate random strings suitable for tokens, identifiers, and other use cases where alphanumeric characters are required. When combined with a cryptographic random number generator like rand::rngs::ThreadRng or OsRng, it produces strings that are suitable for security-sensitive applications. The distribution ensures each character has equal probability of being selected, making the output suitable for tokens where predictability would be a security concern.

Basic Usage for Random Strings

use rand::Rng;
use rand::distributions::Alphanumeric;
 
fn main() {
    let mut rng = rand::thread_rng();
    
    // Generate a random string of 16 characters
    let random_string: String = (0..16)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    
    println!("Random string: {}", random_string);
    // Example output: aB3xK9mP2qR7sT1n
}

Alphanumeric samples from 62 characters: 26 lowercase, 26 uppercase, and 10 digits.

The Alphanumeric Character Set

use rand::distributions::Alphanumeric;
use std::char;
 
fn main() {
    // Alphanumeric includes:
    // - a-z (26 characters)
    // - A-Z (26 characters)
    // - 0-9 (10 characters)
    // Total: 62 characters
    
    let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        .chars()
        .collect();
    
    println!("Character set: {:?}", chars);
    println!("Total characters: {}", chars.len());
    
    // Each character has equal probability: 1/62 ≈ 1.61%
    // This provides uniform entropy per character
}

The uniform distribution ensures each character contributes equally to randomness.

Generating Secure Tokens

use rand::Rng;
use rand::distributions::Alphanumeric;
 
fn generate_token(length: usize) -> String {
    let mut rng = rand::thread_rng();
    (0..length)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect()
}
 
fn main() {
    let api_key = generate_token(32);
    let session_id = generate_token(24);
    let reset_code = generate_token(8);
    
    println!("API key: {}", api_key);
    println!("Session ID: {}", session_id);
    println!("Reset code: {}", reset_code);
}

Thread-local RNG is cryptographically secure by default in the rand crate.

Cryptographic Security Properties

use rand::Rng;
use rand::rngs::OsRng;
use rand::distributions::Alphanumeric;
 
fn main() {
    // thread_rng() uses OsRng internally, which is cryptographically secure
    let mut rng = rand::thread_rng();
    
    // For explicit cryptographic security:
    let mut os_rng = OsRng;
    
    let token_crypto: String = (0..32)
        .map(|_| os_rng.sample(Alphanumeric) as char)
        .collect();
    
    println!("Cryptographically secure token: {}", token_crypto);
    
    // Security properties:
    // - Each character is uniformly random
    // - Characters are independent
    // - No predictable patterns
    // - Suitable for security tokens
}

OsRng provides cryptographically secure randomness from the operating system.

Entropy Calculation

use rand::Rng;
use rand::distributions::Alphanumeric;
 
fn calculate_entropy(length: usize) -> f64 {
    // Alphanumeric has 62 characters
    // Entropy per character = log2(62) ≈ 5.95 bits
    let entropy_per_char = (62_f64).log2();
    length as f64 * entropy_per_char
}
 
fn main() {
    let lengths = [8, 16, 24, 32, 64];
    
    for len in lengths {
        let entropy = calculate_entropy(len);
        println!("{} chars = {:.2} bits of entropy", len, entropy);
        
        // Example output:
        // 8 chars = 47.63 bits
        // 16 chars = 95.26 bits
        // 24 chars = 142.89 bits
        // 32 chars = 190.52 bits
        // 64 chars = 381.05 bits
    }
    
    // For context:
    // - 128 bits is considered secure for most applications
    // - 22 alphanumeric characters ≈ 128 bits
}

Each character contributes ~5.95 bits of entropy (log₂(62)).

URL-Safe Token Generation

use rand::Rng;
use rand::distributions::Alphanumeric;
 
fn generate_url_safe_token(length: usize) -> String {
    // Alphanumeric characters are URL-safe
    // No special characters that need encoding
    let mut rng = rand::thread_rng();
    (0..length)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect()
}
 
fn main() {
    let token = generate_url_safe_token(32);
    
    // Safe to use in:
    // - URLs: https://example.com/verify/{token}
    // - Query params: ?token={token}
    // - Headers: Authorization: Bearer {token}
    // - File names (no special characters)
    
    println!("URL-safe token: {}", token);
    println!("Safe for URL: https://example.com/reset/{}", token);
}

Alphanumeric tokens don't require URL encoding.

Comparison with Other Distributions

use rand::Rng;
use rand::distributions::{Alphanumeric, DistString, Standard};
 
fn main() {
    let mut rng = rand::thread_rng();
    
    // Alphanumeric: a-z, A-Z, 0-9 (62 characters)
    let alphanumeric: String = Alphanumeric.sample_string(&mut rng, 16);
    println!("Alphanumeric: {}", alphanumeric);
    
    // For comparison, other common distributions:
    
    // Hexadecimal: 0-9, a-f (16 characters)
    // - Less entropy per character: log2(16) = 4 bits
    // - Needs more characters for same security
    
    // Base64: a-z, A-Z, 0-9, +, / (64 characters)
    // - Similar entropy: log2(64) = 6 bits
    // - Includes + and / which aren't URL-safe
    
    // Standard printable: all printable ASCII
    // - More characters but includes special chars
}

Alphanumeric balances entropy density with URL safety.

Using DistString Trait

use rand::distributions::{Alphanumeric, DistString};
use rand::rngs::OsRng;
 
fn main() {
    let mut rng = OsRng;
    
    // DistString provides a convenient method
    let token = Alphanumeric.sample_string(&mut rng, 32);
    
    println!("Token: {}", token);
    
    // This is equivalent to:
    let token2: String = (0..32)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    
    // sample_string is more readable and potentially optimized
}

DistString::sample_string provides a convenient shorthand.

Token Uniqueness and Collision Probability

use rand::Rng;
use rand::distributions::Alphanumeric;
 
fn collision_probability(token_length: usize, num_tokens: u64) -> f64 {
    // Birthday problem approximation
    // 62 characters, each position has 62 possibilities
    let space = 62_f64.powi(token_length as i32);
    let n = num_tokens as f64;
    
    // Approximate collision probability
    // P ≈ n² / (2 * space)
    let prob = (n * n) / (2.0 * space);
    prob.min(1.0)
}
 
fn main() {
    // For 16-character tokens
    let length = 16;
    
    for count in [1_000, 10_000, 100_000, 1_000_000] {
        let prob = collision_probability(length, count);
        println!("{} tokens of {} chars: collision prob = {:.2e}", 
                 count, length, prob);
    }
    
    // With 62^16 ≈ 4.8 × 10^28 possible tokens
    // Collision probability is extremely low for practical numbers
}

Longer tokens reduce collision probability exponentially.

Performance Considerations

use rand::Rng;
use rand::distributions::Alphanumeric;
use std::time::Instant;
 
fn main() {
    let mut rng = rand::thread_rng();
    
    // Generate many tokens and measure
    let start = Instant::now();
    let iterations = 100_000;
    
    for _ in 0..iterations {
        let _token: String = (0..32)
            .map(|_| rng.sample(Alphanumeric) as char)
            .collect();
    }
    
    let duration = start.elapsed();
    let per_token = duration / iterations;
    
    println!("Time per 32-char token: {:?}", per_token);
    
    // Alphanumeric sampling is efficient:
    // - Simple modulo operation for uniform sampling
    // - No rejection sampling needed (62 divides evenly)
    // - Character mapping is trivial
}

Alphanumeric sampling is efficient with uniform distribution.

Secure Token Generation Function

use rand::Rng;
use rand::rngs::OsRng;
use rand::distributions::Alphanumeric;
 
#[derive(Debug)]
pub struct SecureToken {
    value: String,
}
 
impl SecureToken {
    pub fn new(length: usize) -> Self {
        let mut rng = OsRng;
        let value = (0..length)
            .map(|_| rng.sample(Alphanumeric) as char)
            .collect();
        Self { value }
    }
    
    pub fn as_str(&self) -> &str {
        &self.value
    }
    
    pub fn into_string(self) -> String {
        self.value
    }
    
    pub fn entropy_bits(&self) -> f64 {
        self.value.len() as f64 * (62_f64).log2()
    }
}
 
fn main() {
    let token = SecureToken::new(32);
    
    println!("Token: {}", token.as_str());
    println!("Length: {} characters", token.as_str().len());
    println!("Entropy: {:.2} bits", token.entropy_bits());
}

Encapsulate token generation in a type for consistent security practices.

Handling Token Requirements

use rand::Rng;
use rand::distributions::Alphanumeric;
 
struct TokenConfig {
    length: usize,
    prefix: Option<String>,
}
 
fn generate_token_with_config(config: TokenConfig) -> String {
    let mut rng = rand::thread_rng();
    
    let random_part: String = (0..config.length)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    
    match config.prefix {
        Some(ref prefix) => format!("{}_{}", prefix, random_part),
        None => random_part,
    }
}
 
fn main() {
    // API key with prefix
    let api_key = generate_token_with_config(TokenConfig {
        length: 24,
        prefix: Some("sk".to_string()),
    });
    
    // Session ID without prefix
    let session = generate_token_with_config(TokenConfig {
        length: 32,
        prefix: None,
    });
    
    println!("API key: {}", api_key);
    println!("Session: {}", session);
}

Add prefixes to identify token types while keeping the random portion.

Non-Cryptographic RNG Warning

use rand::Rng;
use rand::rngs::SmallRng;
use rand::distributions::Alphanumeric;
use rand::SeedableRng;
 
fn main() {
    // SmallRng is FAST but NOT cryptographically secure
    // Only use for non-security purposes
    
    let mut small_rng = SmallRng::seed_from_u64(42);
    
    let predictable_token: String = (0..16)
        .map(|_| small_rng.sample(Alphanumeric) as char)
        .collect();
    
    println!("Predictable token (seeded): {}", predictable_token);
    
    // With same seed, produces same token:
    let mut rng2 = SmallRng::seed_from_u64(42);
    let same_token: String = (0..16)
        .map(|_| rng2.sample(Alphanumeric) as char)
        .collect();
    
    println!("Same seed = same token: {}", same_token);
    
    // WARNING: Do NOT use SmallRng for:
    // - Password reset tokens
    // - Session IDs
    // - API keys
    // - Nonce values
    // - Any security-sensitive tokens
}

Use SmallRng only for testing or non-security applications.

Token Storage and Comparison

use rand::Rng;
use rand::distributions::Alphanumeric;
use std::collections::HashSet;
 
fn main() {
    let mut rng = rand::thread_rng();
    
    // Store tokens securely
    let mut issued_tokens: HashSet<String> = HashSet::new();
    
    // Issue tokens
    for _ in 0..10 {
        let token: String = (0..16)
            .map(|_| rng.sample(Alphanumeric) as char)
            .collect();
        
        issued_tokens.insert(token);
    }
    
    println!("Issued {} tokens", issued_tokens.len());
    
    // Verify token (constant-time comparison would be better)
    let some_token = issued_tokens.iter().next().unwrap();
    let is_valid = issued_tokens.contains(some_token);
    println!("Token valid: {}", is_valid);
    
    // For production:
    // - Use constant-time comparison
    // - Consider hashing tokens before storage
    // - Implement expiration
}

Consider hashing tokens before storage for additional security.

Comparison: Alphanumeric vs Hex

use rand::Rng;
use rand::distributions::Alphanumeric;
 
fn main() {
    let mut rng = rand::thread_rng();
    
    // Alphanumeric: 62 characters, ~5.95 bits/char
    let alphanum: String = (0..22)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    
    // For 128-bit entropy:
    // - Alphanumeric: ceil(128/5.95) = 22 characters
    // - Hex: 128/4 = 32 characters
    
    println!("Alphanumeric (22 chars): {}", alphanum);
    
    // Alphanumeric advantages:
    // - More entropy per character
    // - Shorter tokens for same security
    // - Still URL-safe
    
    // Hex advantages:
    // - Simpler character set
    // - Easy to parse back to bytes
    // - More widely supported
}

Alphanumeric is more compact than hex for equivalent entropy.

Custom Character Sets

use rand::Rng;
use rand::distributions::DistString;
use rand::seq::SliceRandom;
 
fn generate_custom_token(length: usize) -> String {
    let mut rng = rand::thread_rng();
    
    // Custom character set
    let chars = "abcdefghijkmnpqrstuvwxyz23456789";
    // Removed: l, 1, I, 0, O (avoid confusion)
    
    (0..length)
        .map(|_| chars.chars().choose(&mut rng).unwrap())
        .collect()
}
 
fn main() {
    let token = generate_custom_token(16);
    println!("Custom charset token: {}", token);
    
    // Use Alphanumeric when you want all characters
    // Use custom sets when you need to exclude similar characters
}

Custom character sets can exclude visually similar characters.

Token Expiration and Management

use rand::Rng;
use rand::distributions::Alphanumeric;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
 
struct TimedToken {
    value: String,
    expires_at: Instant,
}
 
impl TimedToken {
    fn new(length: usize, ttl: Duration) -> Self {
        let mut rng = rand::thread_rng();
        let value = (0..length)
            .map(|_| rng.sample(Alphanumeric) as char)
            .collect();
        
        Self {
            value,
            expires_at: Instant::now() + ttl,
        }
    }
    
    fn is_valid(&self) -> bool {
        Instant::now() < self.expires_at
    }
    
    fn value(&self) -> &str {
        &self.value
    }
}
 
fn main() {
    // Token valid for 1 hour
    let token = TimedToken::new(32, Duration::from_secs(3600));
    
    println!("Token: {}", token.value());
    println!("Valid: {}", token.is_valid());
}

Add expiration to tokens for time-limited validity.

Validation and Use Cases

use rand::Rng;
use rand::distributions::Alphanumeric;
 
fn is_valid_alphanumeric(s: &str) -> bool {
    s.chars().all(|c| c.is_ascii_alphanumeric())
}
 
fn main() {
    let mut rng = rand::thread_rng();
    
    // Generate and validate
    let token: String = (0..24)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    
    assert!(is_valid_alphanumeric(&token));
    
    // Use cases suitable for Alphanumeric tokens:
    
    // 1. Password reset tokens
    let reset_token: String = (0..32)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    
    // 2. Email verification codes
    let verify_code: String = (0..16)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    
    // 3. Session identifiers
    let session_id: String = (0..24)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    
    // 4. API keys
    let api_key: String = (0..48)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    
    // 5. Unique identifiers
    let unique_id: String = (0..12)
        .map(|_| rng.sample(Alphanumeric) as char)
        .collect();
    
    println!("Reset token: {}", reset_token);
    println!("Verify code: {}", verify_code);
    println!("Session ID: {}", session_id);
}

Alphanumeric tokens suit many authentication and identification use cases.

Synthesis

Alphanumeric characteristics:

Property Value
Character set a-z, A-Z, 0-9 (62 chars)
Entropy per char ~5.95 bits (log₂(62))
URL-safe Yes
File-name safe Yes
Case-sensitive Yes

Security properties with cryptographic RNG:

Property Detail
Uniform distribution Each char has equal probability
Independence Each character is independent
Unpredictability No patterns or correlations
Collision resistance Very low probability for sufficient length

Token length recommendations:

Use case Recommended length Entropy
Short-lived codes 8-12 chars ~48-72 bits
Session IDs 16-24 chars ~95-143 bits
Password reset 32 chars ~190 bits
API keys 32-64 chars ~190-381 bits

RNG selection:

RNG Use case
thread_rng() General security-sensitive
OsRng Explicit cryptographic security
SmallRng Testing, non-security only

Key insight: rand::distributions::Alphanumeric provides a uniform distribution over 62 characters (a-z, A-Z, 0-9), making it ideal for generating URL-safe tokens with good entropy density. When combined with a cryptographically secure random number generator like thread_rng() or OsRng, it produces tokens suitable for security-sensitive applications. Each character contributes approximately 5.95 bits of entropy (log₂(62)), so a 22-character token provides roughly 128 bits of entropy—suitable for most security applications. The alphanumeric character set is URL-safe without requiring encoding, making these tokens convenient for use in URLs, headers, and file names. For security-critical tokens, always use thread_rng() or OsRng rather than predictable generators like SmallRng. The distribution's uniform sampling ensures each character has equal probability, eliminating biases that could make tokens predictable.