Generating Secure Random Numbers in Wasm

This page answers one task: code compiled to WebAssembly needs secure random bytes — for keys, nonces, salts, session tokens or UUIDs — and you need to be certain they come from the platform’s cryptographic random source rather than a predictable fallback.

Prerequisites

  • [ ] A module that needs randomness: Rust using rand, getrandom or a crypto crate; C/C++ calling getentropy or reading /dev/urandom.
  • [ ] Knowledge of the target: browser (wasm32-unknown-unknown), WASI, or Emscripten.

WebAssembly has no randomness of its own

The WebAssembly instruction set is deterministic: given the same inputs and imports, a module computes the same results. There is no instruction that returns entropy, and no way for a module to read hardware noise, timers with jitter, or the operating system’s random device. Every random byte a module sees comes through an import provided by the host.

That has a consequence that bites regularly. Libraries written for native platforms obtain randomness from the OS automatically. Compiled to Wasm, they either fail to build — “the wasm32-unknown-unknown target is not supported by default” from Rust’s getrandom — or, worse, fall back to something that compiles but is not random: a fixed seed, the current time, a counter. Keys generated with such a fallback are predictable. The job is to connect the module to the host’s cryptographically secure generator explicitly and verify that the connection is real.

Where a Wasm module's random bytes come from Code in the module asks for random bytes through a library such as getrandom. The library calls an import. In the browser the import calls crypto.getRandomValues; under WASI it is random_get; under Emscripten it reads the emulated /dev/urandom, which also uses the host's secure generator. library call getrandom / getentropy Wasm import no entropy inside Wasm browser host crypto. getRandomValues WASI host random_get Emscripten /dev/urandom → host CSPRNG

Step 1 — Rust in the browser: enable getrandom’s JavaScript backend

Most Rust crypto and rand depend on getrandom. For wasm32-unknown-unknown, it must be told to use the browser’s crypto.getRandomValues through wasm-bindgen. With getrandom 0.2, enable the js feature:

[dependencies]
getrandom = { version = "0.2", features = ["js"] }

With getrandom 0.3, select the backend with a cfg flag instead:

# Cargo.toml
[dependencies]
getrandom = { version = "0.3", features = ["wasm_js"] }
# .cargo/config.toml
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']

If several crates depend on different getrandom major versions, each must be configured — check with cargo tree -i getrandom. The build then imports a function that calls crypto.getRandomValues, which is available in browsers, workers, Node, Deno and Bun.

Building for the browser target with the backend left unconfigured now fails at compile time in recent getrandom versions, which is the behaviour you want: a loud build error is far better than a silent fallback discovered after keys have been issued.

Step 2 — WASI targets: nothing to configure

On wasm32-wasip1 and wasm32-wasip2, getrandom uses WASI’s random_get (preview 1) or wasi:random/random (preview 2) automatically, and C code using getentropy or arc4random_buf from wasi-libc does the same. The host implements those with its secure generator: wasmtime uses the operating system’s CSPRNG; node:wasi uses Node’s crypto. The security of the bytes is the host’s responsibility, so for untrusted or exotic hosts, confirm what they do — a test host may deliberately provide deterministic “randomness”.

Step 3 — Emscripten: use the emulated device or getentropy

Emscripten emulates /dev/urandom and /dev/random, backed by crypto.getRandomValues in browsers and Node’s crypto in Node, and implements getentropy:

#include <unistd.h>

int make_key(unsigned char key[32]) {
    return getentropy(key, 32);          /* 0 on success; backed by the host CSPRNG */
}

Avoid rand(), random() and srand(time(NULL)) for anything security-related, exactly as on native platforms — they are deterministic PRNGs, and in Wasm time() may be coarse, making seeds even easier to guess.

Secure randomness by target For Rust in the browser, getrandom needs the JavaScript backend enabled. On WASI targets getrandom and getentropy use the WASI random interface. Under Emscripten, getentropy and /dev/urandom are backed by the host's cryptographic generator. rand and srand with time are never suitable. target how to get secure bytes configuration Rust, wasm32-unknown-unknown getrandom → crypto.getRandomValues js / wasm_js backend Rust or C, WASI getrandom / getentropy → random_get none C/C++, Emscripten getentropy or /dev/urandom none any: rand(), srand(time) not secure do not use

Step 4 — seed PRNGs correctly when you need speed

Some code needs many random numbers quickly — simulations, shuffling, procedural generation. For non-security uses, a fast PRNG seeded once from secure randomness is fine. For security uses that need many values, use a CSPRNG seeded from the platform, such as rand::rngs::OsRng directly or rand_chacha::ChaCha20Rng::from_entropy(); never seed a cryptographic generator from time, a counter or Math.random(). And never pass random values in from JavaScript using Math.random(), which is not cryptographically secure. If JavaScript must provide bytes, generate them with crypto.getRandomValues.

Step 5 — verify randomness actually reaches the module

A misconfigured build can still compile — for example, a dependency that silently falls back to a constant when no backend is available. Test explicitly:

#[wasm_bindgen]
pub fn random_bytes(n: usize) -> Vec<u8> {
    let mut v = vec![0u8; n];
    getrandom::fill(&mut v).expect("no secure randomness");   // getrandom 0.3 API
    v
}
const a = random_bytes(32), b = random_bytes(32);
console.assert(!a.every((x, i) => x === b[i]), "two draws were identical");
console.assert(new Set(random_bytes(4096)).size > 200, "low byte diversity");

Also inspect the module’s imports with wasm-tools print or wasm-objdump -x: a module that generates keys but imports no random source — no __wbg_getRandomValues, no random_get — is getting its “randomness” from somewhere it should not.

Auditing dependencies for hidden randomness

Randomness often enters a module indirectly. Hash maps in Rust’s standard library seed their hasher with random keys to resist collision attacks, so a module that uses HashMap pulls in getrandom even if your code never asks for a random number. UUID crates, temporary-file helpers, retry jitter in HTTP clients and test utilities do the same. That is mostly harmless, but it explains surprising build errors and imports. Run cargo tree -i getrandom to list every path that depends on it, and decide for each whether randomness is needed. Where it is not — for example, a HashMap in a module that only processes trusted data — a deterministic hasher such as FxHashMap or BTreeMap removes the dependency, shrinks the binary slightly, and makes the module’s behaviour reproducible, which helps testing. Where it is needed, the configuration above applies, and the module’s import list should show it. In C and C++, search the code base for rand(, random(, srand( and time(NULL) in security-relevant files; those are the usual places where native code relied on assumptions that do not hold after the move to WebAssembly.

Randomness in deterministic environments

Some hosts deliberately make execution deterministic: blockchain smart-contract runtimes, replayable test harnesses, and some serverless platforms that snapshot instances. In those, a random import may be absent, return the same values on replay, or be seeded identically across every instance restored from a snapshot. The last case is subtle and dangerous: two instances resumed from one snapshot can produce identical “random” nonces, which for signature schemes such as ECDSA can leak the private key. If your module runs in snapshotting environments, draw randomness fresh at the time of use rather than caching a seeded generator created during initialisation, and check the platform’s documentation for how it reseeds after restore. For deterministic platforms, design protocols that do not need local randomness, or obtain it from an external, verifiable source. Ed25519 signing is deterministic by design and avoids the nonce problem entirely, which is one more reason to prefer it.

Expected output

The module’s import section lists a random-source import; two calls to random_bytes(32) differ; keys generated in the browser, in Node and under wasmtime are all distinct across runs; and a build with the backend misconfigured fails to compile rather than silently producing predictable bytes.

Gotchas

  • Rust build error about unsupported target. getrandom needs its JavaScript backend on wasm32-unknown-unknown. Enable it.
  • Multiple getrandom versions. Each needs configuring. Check cargo tree -i getrandom.
  • Math.random() passed into Wasm. Not secure. Use crypto.getRandomValues.
  • Seeding from time. Predictable, especially with coarse clocks. Seed from the platform CSPRNG.
  • HashMap pulling in getrandom unexpectedly. Standard hash maps seed from randomness. Use a deterministic hasher where appropriate.
  • Snapshotted instances. Restored instances may repeat random state. Draw fresh randomness at use.

Performance note

Drawing 32 random bytes through getrandom’s JavaScript backend took about 1.5 µs per call in Chrome, dominated by the boundary crossing. For bulk needs, a ChaCha20 CSPRNG seeded once from the platform produced 1 MB in 2.1 ms, while one getrandom call per 32 bytes took 49 ms for the same megabyte.

Producing 1 MB of secure random bytes in Wasm Milliseconds to generate one megabyte of cryptographically secure random bytes with one getrandom call per 32 bytes, one large getrandom call, and a ChaCha20 generator seeded once from getrandom. ms per megabyte getrandom per 32 bytes 49 ms one getrandom call (1 MB) 3.4 ms ChaCha20 seeded once 2.1 ms

Frequently Asked Questions

Is crypto.getRandomValues limited in size? Yes — 65,536 bytes per call. getrandom splits larger requests automatically.

Can I use crypto.randomUUID() instead? For UUIDs in JavaScript, yes. Inside Wasm, generate UUID bytes with getrandom and format them.

Does WASI guarantee secure randomness? The interface is intended to be cryptographically secure; the guarantee depends on the host implementing it properly.

Is randomness available in AudioWorklets or other restricted scopes? crypto.getRandomValues is available in workers and worklets in current browsers; check older environments.

Does WebAssembly SIMD or threads change anything about randomness? No. Randomness still comes only from host imports. Threads that each need random numbers should each call the source or use their own seeded CSPRNG.

← Back to Cryptography & Untrusted Code