Skip to content

Secure Token Generator — Rust source

Generate cryptographically-secure random tokens in your browser. Pick the entropy size and format - hex, base32, base64, base62, or alphanumeric - and see the real strength in bits. Runs entirely client-side.

This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

// ─────────────────────────────────────────────────────────────────────────────
// Secure token generator — Rust polyglot showcase port.
// Language: Rust (edition 2021, standard library only — no external crates).
//
// CosmoDev polyglot showcase port of `token-generator`, ported from the
// canonical TypeScript logic in src/lib/token-generator.ts.
//
// This is display source — part of CosmoDev's polyglot tool pages, where each
// tool's pure logic is shown side-by-side in many languages.
//
// Design:
//   - Pure logic with an *injectable* RNG, so generation is unit-testable
//     without touching the secure RNG. Callers pass None for the CSPRNG
//     default; tests pass a seeded generator for exact, reproducible output.
//   - Each output symbol is selected without modulo bias via rejection
//     sampling (see `constant_time_select`), so even non-power-of-two
//     alphabets like base62 are unbiased.
//
// CSPRNG note: Rust's stdlib has no secure RNG. This showcase stays
// dependency-free by reading 4 bytes from `/dev/urandom` — the kernel's
// cryptographically secure RNG on modern Unix (the same source the `getrandom`
// syscall and the `getrandom`/`rand` crates consume). Production Rust would
// use the `getrandom` or `rand` crates (`OsRng`) for a portable, audited
// wrapper over the same primitive.
// ─────────────────────────────────────────────────────────────────────────────

use std::fs::File;
use std::io::Read;

/// Scaling constants for the 32-bit draw space.
const MAX_U32_F: f64 = 4_294_967_295.0; // 0xFFFFFFFF
const TWO_POW_32: f64 = 4_294_967_296.0; // 0x1_0000_0000

/// An injectable uniform float generator in `[0, 1)`. A bare function pointer
/// keeps the API simple and zero-allocation; closures that capture state would
/// use `Box<dyn Fn() -> f64>` instead.
pub type Rng = fn() -> f64;

/// Output character set. `Custom` reads its symbols from `GenerateOptions`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Alphabet {
    Hex,
    HexUpper,
    Base32,        // RFC 4648
    Base32Crockford, // no I/L/O/U
    Base64,
    Base64Url,
    Base62,
    Alphanumeric,
    Custom,
}

impl Alphabet {
    /// Returns the fixed symbol string, or `None` for `Custom` (caller-supplied).
    fn symbols(self) -> Option<&'static str> {
        match self {
            Alphabet::Hex => Some("0123456789abcdef"),
            Alphabet::HexUpper => Some("0123456789ABCDEF"),
            Alphabet::Base32 => Some("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"),
            Alphabet::Base32Crockford => Some("0123456789ABCDEFGHJKMNPQRSTVWXYZ"),
            Alphabet::Base64 => Some(
                "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
            ),
            Alphabet::Base64Url => Some(
                "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
            ),
            Alphabet::Base62 => Some(
                "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
            ),
            Alphabet::Alphanumeric => Some(
                "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
            ),
            Alphabet::Custom => None,
        }
    }
}

/// Convenience encoding alias mapping 1:1 onto a fixed `Alphabet`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Encoding {
    Hex,
    Base32,
    Base64,
    Base64Url,
    Base62,
    Alphanumeric,
}

impl Encoding {
    fn to_alphabet(self) -> Alphabet {
        match self {
            Encoding::Hex => Alphabet::Hex,
            Encoding::Base32 => Alphabet::Base32,
            Encoding::Base64 => Alphabet::Base64,
            Encoding::Base64Url => Alphabet::Base64Url,
            Encoding::Base62 => Alphabet::Base62,
            Encoding::Alphanumeric => Alphabet::Alphanumeric,
        }
    }
}

/// Controls token generation. All fields optional; `Default::default()` means
/// "use the defaults" (hex alphabet, CSPRNG).
#[derive(Clone, Debug, Default)]
pub struct GenerateOptions {
    /// Output character set. Wins over `encoding` when set.
    pub alphabet: Option<Alphabet>,
    /// Used only when `alphabet == Alphabet::Custom`.
    pub custom_alphabet: Option<String>,
    /// Fallback used when `alphabet` is `None`.
    pub encoding: Option<Encoding>,
    /// Overrides the default CSPRNG. `None` for `/dev/urandom`.
    pub rng: Option<Rng>,
}

/// Draws a uniform float in `[0, 1)` from the kernel CSPRNG.
///
/// Reads four bytes from `/dev/urandom` and scales the resulting 32-bit
/// unsigned integer into the unit interval, so every one of the 2^32 outcomes
/// is equally likely. Panics if the RNG device is unavailable — secure output
/// is impossible without a working entropy source.
fn default_rng() -> f64 {
    let mut buf = [0u8; 4];
    let mut f = File::open("/dev/urandom")
        .expect("tokengenerator: failed to open /dev/urandom");
    f.read_exact(&mut buf)
        .expect("tokengenerator: failed to read /dev/urandom");
    let u = u32::from_be_bytes(buf);
    (u as f64) / TWO_POW_32
}

/// Resolves the effective symbol string from options and an optional encoding.
///
/// Precedence: explicit `alphabet` → `encoding` → `Hex`. `Alphabet::Custom`
/// with no `custom_alphabet` resolves to an empty `String` (an invalid set).
pub fn resolve_alphabet(opts: Option<&GenerateOptions>, encoding: Option<Encoding>) -> String {
    if let Some(o) = opts {
        match o.alphabet {
            Some(Alphabet::Custom) => return o.custom_alphabet.clone().unwrap_or_default(),
            Some(a) => {
                if let Some(s) = a.symbols() {
                    return s.to_string();
                }
            }
            None => {}
        }
    }
    let enc = encoding.or_else(|| opts.and_then(|o| o.encoding));
    if let Some(e) = enc {
        if let Some(s) = e.to_alphabet().symbols() {
            return s.to_string();
        }
    }
    Alphabet::Hex.symbols().unwrap().to_string()
}

/// Builds an `n`-character `String` from `alphabet`, selecting each symbol
/// WITHOUT modulo bias via rejection sampling.
///
/// Naive `draw % size` is biased whenever `size` does not divide the draw
/// range: for base62 the trailing symbols would be slightly over-represented.
/// Instead we reject any 32-bit draw that lands in the uneven remainder
/// (`>= limit`) and redraw — keeping every symbol exactly equally likely. The
/// `guard` cap stops a pathological/constant RNG from looping forever.
pub fn constant_time_select(alphabet: &str, n: usize, rng: Option<Rng>) -> String {
    let r: Rng = rng.unwrap_or(default_rng);
    // All alphabets are ASCII, so byte-length == symbol-length and we may
    // index bytes directly.
    let symbols = alphabet.as_bytes();
    let size = symbols.len();
    if size < 1 || n == 0 {
        return String::new();
    }
    let limit = (MAX_U32_F / size as f64).floor() * size as f64; // largest multiple of size ≤ 2^32-1
    let mut out = String::with_capacity(n);
    for _ in 0..n {
        let mut x = r() * TWO_POW_32; // [0, 2^32)
        let mut guard = 0;
        while x >= limit && guard < 64 {
            x = r() * TWO_POW_32;
            guard += 1;
        }
        let idx = (x.floor() as usize) % size;
        out.push(symbols[idx] as char);
    }
    out
}

/// Characters needed to carry `bytes` bytes of entropy through an alphabet of
/// `alphabet_size` symbols.
pub fn output_length(bytes: usize, alphabet_size: usize) -> usize {
    if bytes == 0 || alphabet_size < 2 {
        return 0;
    }
    let bits = (bytes as f64) * 8.0;
    (bits / (alphabet_size as f64).log2()).ceil() as usize
}

/// Generates a token carrying `bytes` bytes of underlying entropy, rendered
/// through `opts.alphabet` (or `encoding`). Each character is sampled
/// uniformly without modulo bias, so output is unbiased even for base62.
/// Returns an empty `String` for invalid input (zero bytes, an alphabet under
/// 2 symbols).
///
/// Example: `generate_token(16, Some(&GenerateOptions { alphabet: Some(Alphabet::Hex), ..Default::default() }), None)`
/// → 32 hex chars (128 bits).
pub fn generate_token(bytes: usize, opts: Option<&GenerateOptions>, encoding: Option<Encoding>) -> String {
    if bytes == 0 {
        return String::new();
    }
    let alphabet = resolve_alphabet(opts, encoding);
    let size = alphabet.chars().count();
    if size < 2 {
        return String::new();
    }
    let n = output_length(bytes, size);
    let rng = opts.and_then(|o| o.rng);
    constant_time_select(&alphabet, n, rng)
}

/// Entropy (in bits) of a token of `bytes` entropy in a `size`-symbol alphabet.
///
/// Equals `output_length * log2(size)`, which is `>= bytes * 8` because the
/// character count is rounded up to the next whole symbol.
pub fn estimate_entropy(bytes: usize, alphabet_size: usize) -> f64 {
    if bytes == 0 || alphabet_size < 2 {
        return 0.0;
    }
    output_length(bytes, alphabet_size) as f64 * (alphabet_size as f64).log2()
}

/// Human strength label for an entropy estimate. Tiers: weak <64 · fair
/// 64–127 · strong 128–255 · very strong ≥256.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Strength {
    Weak,
    Fair,
    Strong,
    VeryStrong,
}

impl Strength {
    /// Lowercase label matching the TypeScript union literals.
    pub fn as_str(self) -> &'static str {
        match self {
            Strength::Weak => "weak",
            Strength::Fair => "fair",
            Strength::Strong => "strong",
            Strength::VeryStrong => "very strong",
        }
    }
}

/// Buckets an entropy estimate (bits) into a human `Strength` label.
/// Tiers: weak <64 · fair 64–127 · strong 128–255 · very strong ≥256.
pub fn strength_label(entropy_bits: f64) -> Strength {
    if entropy_bits.is_nan() || entropy_bits.is_infinite() || entropy_bits < 64.0 {
        Strength::Weak
    } else if entropy_bits < 128.0 {
        Strength::Fair
    } else if entropy_bits < 256.0 {
        Strength::Strong
    } else {
        Strength::VeryStrong
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →