Cryptographic vs Pseudo-Random Numbers: When Security Matters

May 28, 20267 min read

Two Kinds of Random

Computers cannot produce true randomness — they are deterministic machines. Instead, they use algorithms to generate sequences that look random. These are called pseudo-random number generators (PRNGs).

For most applications, pseudo-randomness is fine. Shuffling a playlist, picking a background color, or generating test data — the output just needs to be unpredictable, not unbreakable.

But when security depends on the randomness — passwords, encryption keys, session tokens — pseudo-randomness is dangerous. An attacker who understands the PRNG state can predict future outputs.

That is where cryptographically secure pseudo-random number generators (CSPRNGs) come in.

How PRNGs Work

A PRNG starts from a seed value and applies a deterministic algorithm to produce a sequence. The same seed always produces the same sequence.

// Simple PRNG: Linear Congruential Generator (illustrative only)
let seed = 12345;
function lcg() {
  seed = (seed * 1103515245 + 12345) & 0x7fffffff;
  return seed / 0x7fffffff;
}

lcg(); // 0.5823...
lcg(); // 0.1267...

If you know the seed and the algorithm, you can reproduce every number — past and future. This predictability is the fundamental weakness.

JavaScript's Math.random() is a PRNG. The exact algorithm varies by browser (V8 uses xorshift128+), but all share the same property: not cryptographically secure.

How CSPRNGs Differ

A CSPRNG adds three properties on top of statistical randomness:

  1. Next-bit unpredictability: Given all previous outputs, the next bit cannot be predicted with probability better than 50%
  2. Backtracking resistance: Even if the internal state is compromised, previous outputs cannot be reconstructed
  3. Entropy harvesting: The generator collects randomness from hardware or OS events (timing, thermal noise, mouse movements) to seed itself

In JavaScript, the CSPRNG is crypto.getRandomValues():

// Cryptographically secure random bytes
const array = new Uint32Array(1);
crypto.getRandomValues(array);
console.log(array[0]); // Unpredictable, even to an attacker

In Node.js:

const crypto = require('crypto');

// Secure random bytes
const bytes = crypto.randomBytes(32);
console.log(bytes.toString('hex'));

// Secure random integer in range
const n = crypto.randomInt(1, 101); // 1 to 100 inclusive

When to Use Each

Use CaseGeneratorWhy
UI animations, delaysPRNGNo security impact
Shuffling playlistPRNGNo attacker motivation
Dice rolls in casual gamesPRNGLow stakes
Test data generationPRNGSpeed matters more than security
Password generationCSPRNGPredictable passwords get cracked
Session tokensCSPRNGPredictable tokens enable hijacking
Encryption keysCSPRNGKeys must be unguessable
Nonces in protocolsCSPRNGReplay attacks if predictable
Lottery/prize drawsCSPRNGFinancial stakes
Online gamblingCSPRNGRegulatory requirement

Rule of thumb: If knowing the random value would let an attacker cause harm, use a CSPRNG.

Practical Vulnerability Examples

Predictable Session Tokens

// ❌ Vulnerable: predictable session token
const token = Math.random().toString(36).slice(2);

// ✅ Secure: unpredictable session token
const token = Array.from(crypto.getRandomValues(new Uint8Array(32)))
  .map(b => b.toString(16).padStart(2, '0'))
  .join('');

If an attacker observes a few Math.random() outputs, they can reconstruct the internal state and predict all future tokens — including tokens assigned to other users.

// ❌ Vulnerable: guessable reset code
const code = Math.floor(Math.random() * 1000000);

// ✅ Secure: unguessable reset code
const code = crypto.randomInt(1000000);

Race Condition in Async Random

// ❌ Problematic: two calls may get related values
const a = await crypto.getRandomValues(new Uint8Array(16));
const b = await crypto.getRandomValues(new Uint8Array(16));

// ✅ Better: get all randomness in one call
const both = crypto.getRandomValues(new Uint8Array(32));
const a = both.slice(0, 16);
const b = both.slice(16);

Performance Comparison

CSPRNGs are slower because they must read from the operating system's entropy pool:

// Benchmark: 1 million random numbers
// Math.random():              ~50ms
// crypto.getRandomValues():   ~200ms

The 4–5x slowdown is the cost of security. For most applications, this is negligible — you are not generating a million tokens per second.

Key Takeaways

  • PRNGs are deterministic algorithms — same seed produces same sequence
  • Math.random() is a PRNG and is never safe for security-critical randomness
  • CSPRNGs harvest hardware entropy and resist prediction even if state is compromised
  • Use CSPRNGs for passwords, tokens, keys, nonces, and anything with financial stakes
  • Use PRNGs for animations, shuffling, test data, and non-security contexts
  • The performance cost of CSPRNGs is negligible for typical web application loads

Try It Yourself

Generate secure random numbers with customizable ranges and precision using our Random Number Generator. Choose between integer, decimal, and unique-value modes for any use case.