Skip to content

Secure Token Generator — TypeScript 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 TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Secure token generation - pure, deterministic logic with an injectable RNG so
// generation is unit-testable without touching Web Crypto. The interactive
// island passes the default CSPRNG; tests pass a seeded generator for exact,
// reproducible outputs. No React, no DOM.

/** Output character sets. `custom` reads its symbols from `opts.customAlphabet`. */
export type Alphabet =
  | 'hex'
  | 'hex-upper'
  | 'base32'
  | 'base32-crockford'
  | 'base64'
  | 'base64url'
  | 'base62'
  | 'alphanumeric'
  | 'custom';

/** Convenience aliases that map 1:1 onto an `Alphabet` (the common encodings). */
export type Encoding =
  | 'hex'
  | 'base32'
  | 'base64'
  | 'base64url'
  | 'base62'
  | 'alphanumeric';

/** The fixed alphabet strings. `custom` is intentionally absent - it is caller-supplied. */
export const ALPHABETS: Record<Exclude<Alphabet, 'custom'>, string> = {
  hex: '0123456789abcdef',
  'hex-upper': '0123456789ABCDEF',
  base32: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', // RFC 4648
  'base32-crockford': '0123456789ABCDEFGHJKMNPQRSTVWXYZ', // Crockford (no I/L/O/U)
  base64: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
  base64url: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
  base62: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
  alphanumeric: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
};

const ENCODING_TO_ALPHABET: Record<Encoding, Alphabet> = {
  hex: 'hex',
  base32: 'base32',
  base64: 'base64',
  base64url: 'base64url',
  base62: 'base62',
  alphanumeric: 'alphanumeric',
};

export interface GenerateOptions {
  /** Output character set. Wins over `encoding` when both are given. */
  alphabet?: Alphabet;
  /** Used only when `alphabet === 'custom'`. Ignored otherwise. */
  customAlphabet?: string;
  /** Uniform float generator in [0, 1). Defaults to a crypto-backed CSPRNG. */
  rng?: () => number;
}

/** Default CSPRNG-backed [0, 1) float, used when no `rng` is injected. */
const defaultRng = (): number => {
  const buf = new Uint32Array(1);
  crypto.getRandomValues(buf);
  return buf[0] / 0x100000000;
};

/**
 * Resolve the effective alphabet string from options and an optional encoding.
 * Precedence: explicit `alphabet` → `encoding` → `hex`. A `custom` alphabet
 * with no/empty `customAlphabet` resolves to '' (an invalid, empty set).
 */
export function resolveAlphabet(opts?: GenerateOptions, encoding?: Encoding): string {
  if (opts?.alphabet === 'custom') return opts.customAlphabet ?? '';
  if (opts?.alphabet) return ALPHABETS[opts.alphabet];
  if (encoding) return ALPHABETS[ENCODING_TO_ALPHABET[encoding]];
  return ALPHABETS.hex;
}

/**
 * Build an `n`-character string from `alphabet`, selecting each symbol without
 * modulo bias via rejection sampling: a 32-bit draw is rejected if it falls in
 * the uneven remainder, so every symbol stays equally likely - important for
 * non-power-of-two alphabets like base62. The rejection cap (`guard`) keeps a
 * pathological `rng` from looping forever.
 */
export function constantTimeSelect(
  alphabet: string,
  n: number,
  rng: () => number = defaultRng,
): string {
  const size = alphabet.length;
  if (size < 1 || !Number.isFinite(n) || n < 1) return '';
  const limit = Math.floor(0xffffffff / size) * size; // largest multiple of size ≤ 2^32-1
  let out = '';
  for (let i = 0; i < n; i++) {
    let x = rng() * 0x100000000; // [0, 2^32)
    let guard = 0;
    while (x >= limit && guard < 64) {
      x = rng() * 0x100000000;
      guard++;
    }
    out += alphabet[Math.floor(x) % size];
  }
  return out;
}

/** Output characters needed to carry `bytes` bytes of entropy in a `size`-symbol alphabet. */
export function outputLength(bytes: number, alphabetSize: number): number {
  if (!Number.isFinite(bytes) || bytes < 1 || alphabetSize < 2) return 0;
  return Math.ceil((bytes * 8) / Math.log2(alphabetSize));
}

/**
 * Generate a token with `bytes` bytes of underlying entropy, rendered through
 * `opts.alphabet` (or `encoding`). Each character is sampled uniformly without
 * modulo bias, so the output is unbiased even for base62/alphanumeric. Returns
 * '' for invalid input (non-positive/non-finite bytes, empty alphabet).
 *
 * Example: `generateToken(16, { alphabet: 'hex' })` → 32 hex chars (128 bits).
 */
export function generateToken(
  bytes: number,
  opts?: GenerateOptions,
  encoding?: Encoding,
): string {
  if (!Number.isFinite(bytes) || bytes < 1) return '';
  const alphabet = resolveAlphabet(opts, encoding);
  if (alphabet.length < 2) return '';
  const n = outputLength(bytes, alphabet.length);
  const rng = opts?.rng ?? defaultRng;
  return constantTimeSelect(alphabet, n, rng);
}

/**
 * Entropy (in bits) of a token of `bytes` entropy in a `size`-symbol alphabet.
 * Equals `outputLength * log2(size)`, which is ≥ `bytes * 8` because the char
 * count is rounded up.
 */
export function estimateEntropy(bytes: number, alphabetSize: number): number {
  if (!Number.isFinite(bytes) || bytes < 1 || alphabetSize < 2) return 0;
  return outputLength(bytes, alphabetSize) * Math.log2(alphabetSize);
}

export type StrengthLabel = 'weak' | 'fair' | 'strong' | 'very strong';

/**
 * Bucket an entropy estimate (bits) into a human strength label.
 * Tiers: weak <64 · fair 64-127 · strong 128-255 · very strong ≥256.
 */
export function strengthLabel(entropyBits: number): StrengthLabel {
  if (!Number.isFinite(entropyBits) || entropyBits < 64) return 'weak';
  if (entropyBits < 128) return 'fair';
  if (entropyBits < 256) return 'strong';
  return 'very strong';
}

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 →