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,getrandomor a crypto crate; C/C++ callinggetentropyor 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.
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.
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. Usecrypto.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.
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.
Related
- Verifying Ed25519 signatures in Wasm — a module that needs no randomness.
- Encrypting files client-side with Wasm — keys and nonces in practice.
- Fixing crates that fail to compile for Wasm — the getrandom error among others.
- Writing constant-time code for Wasm — the other crypto pitfall.
← Back to Cryptography & Untrusted Code