Skip to content

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

// ─────────────────────────────────────────────────────────────────────────────
// Secure token generator - JavaScript polyglot showcase port.
// Language: JavaScript (ES module, ES2020+).
//
// 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. The live web tool
// runs the TypeScript lib; this file mirrors that logic for reference.
//
// Design:
//   - Pure generation logic with an *injectable* RNG, so the path is unit-
//     testable without touching Web Crypto. Production callers pass nothing
//     and get the CSPRNG default; tests pass a seeded generator.
//   - Each output symbol is selected without modulo bias via rejection
//     sampling (see `constantTimeSelect`), so even non-power-of-two
//     alphabets like base62 stay unbiased.
//   - The CSPRNG uses the Web Crypto API (`globalThis.crypto`), available in
//     browsers and in Node 19+ as a global. No external dependencies.
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The fixed alphabet strings. `custom` is intentionally absent here - its
 * symbols are caller-supplied via `GenerateOptions.customAlphabet`.
 */
export const ALPHABETS = Object.freeze({
  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',
});

/** Convenience encoding aliases that map 1:1 onto a fixed alphabet. */
const ENCODING_TO_ALPHABET = Object.freeze({
  hex: 'hex',
  base32: 'base32',
  base64: 'base64',
  base64url: 'base64url',
  base62: 'base62',
  alphanumeric: 'alphanumeric',
});

/**
 * GenerateOptions (plain object - JS has no interface syntax):
 *   alphabet        Output character set. Wins over `encoding` when both given.
 *                   Pass 'custom' to supply symbols via `customAlphabet`.
 *   customAlphabet  Used only when `alphabet === 'custom'`. Ignored otherwise.
 *   rng             Uniform float generator in [0, 1). Defaults to the CSPRNG.
 */

/**
 * Default CSPRNG-backed [0, 1) float, used when no `rng` is injected.
 *
 * Web Crypto fills a 32-bit unsigned integer with cryptographically secure
 * randomness; dividing by 2^32 maps all 4 294 967 296 outcomes uniformly into
 * the half-open unit interval - every value in [0, 1) is equally likely.
 */
function defaultRng() {
  const buf = new Uint32Array(1);
  crypto.getRandomValues(buf);
  return buf[0] / 0x100000000; // 2^32
}

/**
 * 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), which
 * downstream callers reject.
 */
export function resolveAlphabet(opts, encoding) {
  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.
 *
 * Naive `draw % size` is biased whenever `size` does not divide the draw
 * range: for base62 (size 62), the trailing symbols would be slightly
 * over-represented. Instead we reject any 32-bit draw that lands in the
 * uneven remainder (>= `limit`) and redraw - so every symbol is exactly
 * equally likely. The `guard` cap keeps a pathological/constant `rng` from
 * looping forever.
 */
export function constantTimeSelect(alphabet, n, rng = defaultRng) {
  const size = alphabet.length;
  if (size < 1 || !Number.isFinite(n) || n < 1) return '';
  // Largest multiple of `size` that still fits in [0, 2^32-1]. Draws at or
  // above this boundary map unevenly under `% size`, so we redraw.
  const limit = Math.floor(0xffffffff / size) * size;
  let out = '';
  for (let i = 0; i < n; i++) {
    let x = rng() * 0x100000000; // scale [0,1) up to [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 through an
 *  alphabet of `alphabetSize` symbols. */
export function outputLength(bytes, alphabetSize) {
  if (!Number.isFinite(bytes) || bytes < 1 || alphabetSize < 2) return 0;
  return Math.ceil((bytes * 8) / Math.log2(alphabetSize));
}

/**
 * Generate a token carrying `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 or
 * non-finite bytes, an alphabet of fewer than 2 symbols).
 *
 * Example: generateToken(16, { alphabet: 'hex' }) → 32 hex chars (128 bits).
 */
export function generateToken(bytes, opts, encoding) {
  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
 * character count is rounded up to the next whole symbol.
 */
export function estimateEntropy(bytes, alphabetSize) {
  if (!Number.isFinite(bytes) || bytes < 1 || alphabetSize < 2) return 0;
  return outputLength(bytes, alphabetSize) * Math.log2(alphabetSize);
}

/**
 * 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) {
  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 →