Generating Secure Random Numbers with Web Crypto API

This article explores how modern web applications generate cryptographically secure pseudo-random numbers using the Web Cryptography API. It breaks down the mechanics of the crypto.getRandomValues() method, details how modern browsers bridge JavaScript with underlying operating system entropy sources, contrasts secure generation with standard pseudo-random functions like Math.random(), and demonstrates proper implementation patterns for sensitive web security tasks.

The Limitation of Math.random()

Standard JavaScript provides Math.random() for basic randomness. However, Math.random() relies on deterministic algorithms—most commonly variations of xorshift128+—designed for speed and uniform distribution rather than unpredictability. Because its internal state can be deduced after observing a short sequence of outputs, it is entirely unsuitable for security-sensitive contexts like session tokens, cryptographic nonces, password salts, or key generation.

The Web Cryptography API Solution

To address this limitation, the W3C defined the Web Cryptography API, exposed globally in modern browsers via window.crypto (or crypto in secure contexts and Web Workers). Within this interface, the crypto.getRandomValues() method produces cryptographically secure pseudo-random numbers (CSPRNG).

Unlike basic pseudo-random number generators (PRNGs), a CSPRNG satisfies two core security properties:

  1. Next-Bit Unpredictability: Given the first k bits of a random sequence, an attacker with polynomial computing power cannot predict the (k+1)-th bit with a probability significantly greater than 50%.
  2. State Compromise Resistance: If the internal state of the generator is compromised, an attacker cannot reconstruct prior random outputs (forward secrecy) or predict future outputs if new entropy is continuously injected.

Underlying Operating System Entropy Sources

The browser itself does not independently generate cryptographic randomness from scratch. Instead, the Web Cryptography API acts as an abstraction layer that interfaces directly with the host operating system's kernel CSPRNG.

When crypto.getRandomValues() is invoked, the browser queries the OS-level entropy pool, which continuously collects physical noise from hardware sources (such as thermal sensor variations, keystroke timings, disk I/O interrupt jitter, and network packet arrivals). Depending on the platform, the browser taps into specific system APIs:

  • Linux / Android: Reads from /dev/urandom or uses the getrandom() system call.
  • Windows: Calls the CryptoAPI or Cryptography API: Next Generation (CNG) via BCryptGenRandom or CryptGenRandom.
  • macOS / iOS: Interfaces with the Security framework via SecRandomCopyBytes or the kernel-level getentropy().

These underlying operating system routines feed collected environmental entropy through robust cryptographic algorithms (such as AES-CTR-DRBG, HMAC-DRBG, or ChaCha20) to ensure continuous, non-blocking, and statistically sound randomness.

Implementation with crypto.getRandomValues()

The crypto.getRandomValues() method populates an existing JavaScript ArrayBufferView (such as a typed integer array) in place with cryptographically secure random bytes.

// Generate a 16-byte cryptographically secure token
const randomBytes = new Uint8Array(16);
window.crypto.getRandomValues(randomBytes);

// Convert to a hexadecimal string representation
const secureToken = Array.from(randomBytes, byte => 
  byte.toString(16).padStart(2, '0')
).join('');

console.log(secureToken);

Supported Typed Arrays

The method accepts instances of integer-based typed arrays:

  • Int8Array, Uint8Array, Uint8ClampedArray
  • Int16Array, Uint16Array
  • Int32Array, Uint32Array
  • BigInt64Array, BigUint64Array

It does not accept floating-point arrays such as Float32Array or Float64Array. Floating-point representations introduce rounding and bit-pattern complexities that undermine uniform entropy distribution.

Technical Constraints and Best Practices

  • Quotas and Size Limits: The crypto.getRandomValues() method enforces a strict 65,536-byte (64 KiB) limit per execution call. Attempting to fill a buffer larger than this limit throws a QuotaExceededError DOMException to prevent thread blocking and entropy starvation. Applications requiring more data must make sequential calls with appropriately sized buffers.
  • Context Restrictions: While crypto.getRandomValues() is available in most contexts, the broader Web Cryptography API (crypto.subtle) is strictly restricted to secure contexts (HTTPS or localhost).
  • Range Normalization: To generate random integers within a specific range without introducing modulo bias, developers should combine crypto.getRandomValues() with rejection sampling algorithms rather than using the modulo operator directly across raw random bytes.