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 →