Secure Random Number Generation: Foundation of Strong Crypto
Secure random number generation is critical for encryption. Learn how crypto.getRandomValues works and why weak randomness breaks your entire security model.
Secure random number generation is the foundation every other piece of cryptography rests on. In JavaScript, crypto.getRandomValues(buffer) fills a TypedArray with cryptographically secure random bytes from the operating system's CSPRNG (/dev/urandom on Linux/macOS, BCryptGenRandom on Windows, SecRandomCopyBytes on iOS/macOS). Do not use Math.random() for anything touching security — it's a Mulberry32 or xorshift-style PRNG designed for speed, not unpredictability, and its output is predictable after observing a few samples. A weak RNG breaks AES keys, TLS handshakes, nonce uniqueness in GCM, token unguessability, and every other security primitive that depends on unpredictable bits.
The Difference Between Random and Cryptographic Random
A PRNG (pseudo-random number generator) produces a deterministic stream from a seed. Given the seed and algorithm, you can reproduce every output. Fine for games, simulations, and Monte Carlo methods. Catastrophic for cryptography.
A CSPRNG (cryptographically secure PRNG) is seeded from a true entropy source (thermal noise, interrupt timing, hardware RNG instructions like Intel's RDSEED) and its design ensures output is computationally indistinguishable from true randomness, and observing past output doesn't help predict future output.
JavaScript gives you both. Math.random() is a PRNG. crypto.getRandomValues() is a wrapper over the operating system's CSPRNG. One line of code difference, huge security difference.
The Canonical Correct Usage
// Generate a 256-bit AES key worth of random bytes
const keyBytes = crypto.getRandomValues(new Uint8Array(32));
// Generate a 96-bit GCM nonce
const nonce = crypto.getRandomValues(new Uint8Array(12));
// Generate a 128-bit salt
const salt = crypto.getRandomValues(new Uint8Array(16));
// Generate a URL-safe random token
const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
const token = btoa(String.fromCharCode(...tokenBytes))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
crypto.getRandomValues() is synchronous, fills the buffer in place, returns the buffer. Max request size is 65,536 bytes in a single call (a quota imposed by the spec to prevent blocking). For larger random material, call repeatedly.
Node.js Equivalents
const { randomBytes, randomFillSync, webcrypto } = require('crypto');
const keyBytes = randomBytes(32); // Returns a Buffer
// Or Web Crypto-compatible
const nonce = webcrypto.getRandomValues(new Uint8Array(12));
Node's randomBytes pulls from the same underlying CSPRNG as Web Crypto. Use whichever API style fits your code. For isomorphic code running in both environments, webcrypto.getRandomValues matches the browser exactly.
Why Math.random Fails
V8 (Chrome/Node), SpiderMonkey (Firefox), and JavaScriptCore (Safari) all implement Math.random() as a fast PRNG with no cryptographic guarantees. V8 uses an xorshift128+ variant. Researchers have demonstrated that after observing ~5 outputs, an attacker can recover the internal state and predict all future outputs. In 2015, Mike Pound and colleagues reversed V8's Math.random state in real-world bug bounties.
If you use Math.random() to generate session tokens, password reset links, encryption nonces, or share IDs, attackers who observe a few of them can predict the rest. This is not theoretical — it's a common bug class in audits.
Common Misuses
Seeding a library PRNG with Math.random():
// BAD
const seed = Math.floor(Math.random() * 2**32);
Nothing downstream can be more random than the seed. Use crypto.getRandomValues(new Uint32Array(1))[0] instead.
Using Date.now() as entropy: Time is guessable within tight windows. Even combined with a tiny random factor, timestamps leak enough bits for attackers.
Rolling your own by XORing sources: Don't. Operating system CSPRNGs already mix every useful entropy source. Adding your own stir typically reduces entropy rather than increasing it.
Modulo bias when generating ranges: randomBytes[0] % 10 isn't uniformly distributed over 0-9 because 256 isn't a multiple of 10. For uniform random integers in a range, use rejection sampling:
function randomInt(max) {
const range = new Uint32Array(1);
const threshold = 2**32 - (2**32 % max);
do {
crypto.getRandomValues(range);
} while (range[0] >= threshold);
return range[0] % max;
}
Entropy Sources and Boot-Time Concerns
On Linux, /dev/urandom is always safe after early boot. During the first few seconds of boot on systems without hardware RNG, the kernel pool may be under-seeded. This was exploited in the 2008 Debian OpenSSL bug where a patch removed the entropy mixing and left only process ID as the seed. Keys generated in this window had only 2^15 possible values — enumerated in seconds.
Modern systems seed the kernel CSPRNG from: RDSEED on x86-64 (when available), ARMv8.5-A RNG instructions, thermal noise from various peripherals, interrupt timing, keyboard/mouse if interactive. On servers using hardware like Intel Ice Lake or AMD Zen 3+, the CSPRNG is seeded within microseconds of boot.
For Docker containers: the host's /dev/urandom is passed through by default. No action needed. For serverless (AWS Lambda, Cloudflare Workers), the runtime handles entropy seeding per invocation.
Session Tokens and Share IDs
For a file transfer service, you generate random identifiers for:
- File IDs in URLs (attackers should not be able to guess valid IDs)
- Share tokens for password-protected links
- CSRF tokens
- Encryption keys (per-file AES keys)
- Nonces for GCM
Minimum length: 128 bits (16 bytes) for collision and unguessability, 256 bits (32 bytes) for keys. URL-safe encoding via base64url adds ~33% length; hex adds 100%.
A 32-byte base64url-encoded token is 43 characters and essentially collision-free at 2^256.
Testing for Weak RNG
Signs your RNG is broken or weak:
- Identical tokens generated by different requests (collision in what should be huge space)
- Output passes visual tests but fails
dieharderorPractRandstatistical batteries - Seed reuse after process restart — each deploy uses the same initial state
- Generated keys fall into patterns (e.g., first 4 bytes vary but last 28 are identical)
In production, you probably won't see these unless something's catastrophically wrong. The failure mode is usually silent: attacks just become practical on what should be a 2^256 search space.
Audit: every call to Math.random() in a codebase should be reviewed. A grep for Math.random over your source tree is a good weekly hygiene check. Converting any security-relevant callsite to crypto.getRandomValues takes minutes and prevents real vulnerabilities.
Random Strings and UUIDs
For human-facing identifiers, crypto.randomUUID() returns a v4 UUID (122 bits of randomness) in a standard format:
const id = crypto.randomUUID();
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"
Supported in Chrome 92+, Firefox 95+, Safari 15.4+, Node 14.17+. Good for database primary keys, API request IDs, and non-security-critical unique identifiers. Use explicit getRandomValues for anything requiring custom formats or higher entropy.
On Servers: Avoid Custom RNG Pools
Some server frameworks offer their own random pools that claim to "mix" the system CSPRNG with application-level entropy. Treat this with suspicion. Custom mixing rarely improves on the kernel's output and can silently reduce entropy if bugged.
If you're on Node or a major runtime, the builtin crypto.randomBytes is correct and fast. Don't replace it with third-party mixers.
The Bottom Line
Every piece of cryptography in a file transfer app depends on unpredictable random bytes. AES keys, GCM nonces, PBKDF2 salts, share tokens, anti-CSRF tokens, session IDs — all need the same primitive: crypto.getRandomValues() in browsers, crypto.randomBytes() or webcrypto.getRandomValues in Node. Use those. Never Math.random(). Never timestamps. Never your own mixer.
HexaTransfer derives every per-file AES key, nonce, and URL identifier from crypto.getRandomValues() on the client. Server-side share IDs come from crypto.randomBytes. One API, consistent behavior, no way to accidentally leak predictable bits into the system.
The primitive is boring precisely because it needs to be. Boring, correct, and available everywhere — exactly what crypto foundations should be.
Try it at hexatransfer.com — free, no account, 10 GB max.
Send large files securely with end-to-end encryption
Transfer files up to 10 GB for free with end-to-end encryption. No account required. Your files are encrypted in your browser before upload — no one else can read them.
Send a file