How to verify cryptographic security in a client-side random number generator for fair game draws

The "Math.random() Is Good Enough" Trap

You built a raffle game. Players pick numbers, a draw happens, someone wins. To keep things transparent, you run the random number generator right in the browser using Math.random(). You even display the result on screen so players can see it happen in real time. Feels fair, right?

Here's the problem. Math.random() is not cryptographically secure. It's a pseudo-random number generator (PRNG) that uses a deterministic algorithm internally. If someone can figure out the internal state, and they can, they can predict every future number your game produces. That means a savvy player could reverse-engineer your draw mechanism, figure out which numbers will win, and clean you out before you even realize what happened.

Worse, running an unverified RNG client-side doesn't prove fairness. It only proves that *something* random-looking happened on screen. Without cryptographic verification, there's no way for your players to know you didn't quietly bias the outcome.

If you're an indie game developer or small platform operator running prize draws, raffles, or loot mechanics in the browser, this guide walks you through how to verify cryptographic security in a client-side random number generator so your draws are provably fair and tamper-resistant.

Why Standard PRNGs Fail Fairness Tests

The Determinism Problem

Think of a standard PRNG like a recipe. You start with an ingredient (the seed), follow steps in order, and get a result. Follow the same steps with the same ingredient, and you get the exact same cake every time. That's determinism. It's great for testing and simulations. It's terrible for fairness.

Most browser-based PRNGs, including the V8 engine's implementation of Math.random(), use algorithms like xorshift128+. These are fast and statistically decent for casual use. But they were never designed to resist reverse-engineering. Researchers have demonstrated that observing as few as 5 consecutive outputs from xorshift128+ can be enough to reconstruct the internal state and predict all future outputs.

For a game draw where real prizes are on the line, that's a fatal flaw.

What Cryptographic Security Actually Means Here

A cryptographically secure pseudo-random number generator (CSPRNG) solves this by pulling entropy from sources that are practically impossible to predict: hardware noise, timing variations, operating system-level entropy pools. Even if an attacker observes thousands of previous outputs, they cannot feasibly determine the next one.

In the browser, the Web Crypto API gives you access to this through crypto.getRandomValues(). This is your foundation. But using it is only step one. You also need to verify that it's being used correctly and that the entire draw flow resists manipulation.

Step-by-Step: Building and Verifying a Provably Fair Client-Side Draw

Step 1: Replace Math.random() With the Web Crypto API

This is the single most important change. Swap out every instance of Math.random() in your draw logic with crypto.getRandomValues().

Here's what that looks like in practice:

// Generate a random integer between 0 and 9999
const buffer = new Uint16Array(1);
crypto.getRandomValues(buffer);
const result = buffer[0] % 10000;

The Web Crypto API draws from the operating system's CSPRNG, which collects entropy from hardware events, keystroke timing, and other unpredictable sources. This gives you a number that is computationally infeasible to predict.

But don't stop here. Using a CSPRNG is necessary but not sufficient for provable fairness.

Step 2: Implement a Commit-Reveal Scheme

Here's where most developers get tripped up. Even with a CSPRNG, if your server or client generates the winning number and immediately shows it, players have no way to verify that the number wasn't chosen *after* they placed their bets. You could peek at their entries and then generate a number that avoids their picks.

A commit-reveal scheme fixes this. It works like sealing an envelope.

The flow:

1. Commit phase: Before the draw, you generate a random seed and publish its cryptographic hash. This is the sealed envelope. Players can see the hash but cannot derive the seed from it. 2. Entry phase: Players submit their entries. All entries are recorded. 3. Reveal phase: After entries close, you reveal the original seed. Anyone can hash it and compare against the committed hash to confirm it hasn't changed. 4. Draw phase: The winning number is derived from the revealed seed using a transparent, deterministic algorithm. Players can reproduce the exact same result themselves.

This proves the outcome was locked in before any entries were submitted. No peeking. No post-hoc manipulation.

Step 3: Use a Transparent Derivation Function

Once the seed is revealed, the winning number must be derived using a method that anyone can reproduce independently. This means no secret logic, no hidden modifiers, no "extra randomness" layered on top.

A common approach is to use a cryptographic hash function like SHA-256 as a deterministic mapping:

// Derive a draw result from the revealed seed
async function deriveResult(seed, maxRange) {
  const encoder = new TextEncoder();
  const data = encoder.encode(seed);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  // Take first 4 bytes and convert to integer
  const num = hashArray[0] * 16777216 + hashArray[1] * 65536 + hashArray[2] * 256 + hashArray[3];
  return num % maxRange;
}

Let's say your seed is the string "draw-7349-abc123" and your max range is 10,000. The SHA-256 hash of that string produces a 256-bit output. You take the first 32 bits (4 bytes), convert them to a decimal integer, and apply modulo 10,000 to get your winning number. Every player who runs the same computation gets the same result. That's verifiable fairness.

Step 4: Add a Client-Side Seed for Combined Entropy

To make the draw even more robust, let players contribute their own randomness. This is sometimes called a "client seed" or "player seed."

Here's how it works:

1. The server (or your client-side commit mechanism) generates a server seed and commits its hash. 2. Each player provides a client seed, which could be a string they type in or a value generated by their own browser's CSPRNG. 3. The final draw seed is a combination: SHA-256(serverSeed + clientSeed + drawId)

This way, neither you nor the player can unilaterally control the outcome. Both parties would need to collude to bias the result, which defeats the purpose for everyone involved.

Step 5: Publish a Verification Tool

The last piece is giving players a way to verify the draw themselves. This is what separates a *claim* of fairness from *provable* fairness.

Build a simple verification page where players can:

- Enter the revealed server seed - Enter their client seed - Enter the draw ID - Click a button and see the computed result

If the number they get matches the announced winning number, the draw was fair. No trust required. The math speaks for itself.

This tool should run entirely client-side using the same derivation function from Step 3. Open-source the code if you can. Let players inspect it, run it locally, even modify it to test edge cases. Transparency is your strongest asset here.

Common Verification Pitfalls to Avoid

Using a Weak Hash Function

SHA-1 and MD5 are broken. Don't use them for commit-reveal schemes. An attacker could find a hash collision and reveal a different seed than the one originally committed. Stick with SHA-256 or SHA-3. Both are available in the Web Crypto API through crypto.subtle.digest().

Reusing Seeds Across Draws

Every draw needs a fresh seed. If you reuse a seed, even with a different draw ID, you're reducing the effective entropy and creating patterns that statistical analysis could expose. Generate a new server seed for every single draw, commit it, and never recycle.

Skipping the Commit Phase

Some developers implement a CSPRNG and a transparent derivation function but skip the commit-reveal step entirely. They generate the seed, run the draw, and publish everything at once. This is better than using Math.random(), but it doesn't prove the seed was chosen before entries were collected. The commit phase is what locks the timeline. Without it, you're asking players to trust that you didn't peek.

Ignoring Entropy Quality on the Server Side

If your client-side commit mechanism pulls a server seed, that server needs to use a proper CSPRNG too. On Linux, that means reading from /dev/urandom or using a library like Node's crypto.randomBytes(). On Windows, use BCryptGenRandom. Never seed a CSPRNG with Date.now() or Math.random(). The entire chain is only as strong as its weakest link.

What a Verified Draw Looks Like End to End

Let's walk through a complete example with real numbers.

1. Server seed generated: f3a9c1e7b2d4... (256-bit hex string) 2. SHA-256 hash committed publicly: a7e2b9f1c4d8... 3. Player submits client seed: myLuck42 4. Draw ID: draw-1024 5. Entries close. Server seed is revealed. 6. Players verify: SHA-256 of the revealed seed matches the committed hash. Confirmed unmodified. 7. Derivation: SHA-256("f3a9c1e7b2d4..." + "myLuck42" + "draw-1024") produces a hash. First 4 bytes convert to 2,847,193. Modulo 10,000 = 7193. 8. Winning number: 7193. Every player who runs the same computation gets 7193. Provably fair.

No black boxes. No "trust us." Just verifiable math running in every player's browser.

The Bottom Line for Indie Game Developers

Cryptographic security in a client-side random number generator isn't about writing more complex code. It's about building a system where fairness can be independently verified by anyone. Swap Math.random() for the Web Crypto API. Implement a commit-reveal scheme so the timeline is locked. Use a transparent derivation function so results are reproducible. Add client-side seeds so no single party controls the outcome. And publish a verification tool so players can check your work.

Do all five, and your game draws aren't just fair. They're provably fair. That's the difference between asking players to trust you and giving them the tools to verify you.

Frequently Asked Questions

What makes a random number generator cryptographically secure?

A cryptographically secure random number generator (CSPRNG) uses high-entropy sources and mathematical algorithms that make it impossible for attackers to predict future outputs. It ensures that even if part of the internal state is known, the remaining sequence remains entirely unpredictable. This is essential for maintaining fairness in game draws.

How do you ensure a random number generator is fair?

To ensure fairness, a random number generator must use a transparent, verifiable algorithm combined with a publicly known seed and a secret server seed. By revealing the server seed after the draw, users can independently verify that the outcome was not manipulated. This process is widely known as provably fair technology.

What is a client-side random number generator?

A client-side random number generator executes directly in the user's web browser rather than on a centralized server. This approach gives users more control over the generation process and reduces the risk of server-side manipulation. It often leverages browser APIs like the Web Crypto API to access system-level entropy.

How can I verify cryptographic security in an RNG?

You can verify cryptographic security by checking if the RNG uses industry-standard algorithms like SHA-256 and sources its entropy from unpredictable hardware events. Additionally, proper implementations will allow you to hash the initial seed and compare it against the final output. Reviewing the open-source code of the client-side script is also highly recommended.

How does a provably fair algorithm work?

A provably fair algorithm uses cryptographic hashing to allow players to verify the fairness of each game draw before and after it happens. The system provides a hashed server seed before the draw, and the unhashed seed afterward, proving the outcome was predetermined. Players can then use these seeds alongside their own client seed to recreate and verify the exact result.

What is the difference between server-side and client-side RNG?

Server-side RNG generates random numbers on the host's servers, requiring players to trust the operator completely. Client-side RNG shifts the generation process to the user's browser, allowing them to contribute their own seeds and verify the process. This decentralization significantly boosts transparency and trust in fair game draws.

Can a client-side random number generator be hacked?

While no system is entirely immune, a properly implemented client-side CSPRNG is extremely difficult to hack because it relies on unpredictable system entropy. However, if the code uses weak pseudo-random number generators like Math.random() instead of secure APIs, attackers could predict the sequence. Always ensure the system uses cryptographically secure methods like window.crypto.getRandomValues.

What cryptographic algorithms are used for secure random number generation?

Secure random number generation typically relies on algorithms like SHA-256 for hashing seeds and Yarrow or Fortuna for managing entropy pools. In web browsers, the Web Crypto API securely handles this under the hood using native system entropy. These algorithms ensure that the generated numbers cannot be reverse-engineered or predicted.

How do I use a seed to verify a random draw?

To verify a draw, you need the client seed, the server seed, and the nonce used during the generation process. After the draw concludes, the server reveals its unhashed seed, allowing you to run the same cryptographic hash function on the combined inputs. If the resulting number matches the draw outcome, the game is verified as fair.

Why use client-side RNG for game draws?

Using client-side RNG for game draws builds immediate trust between the platform and the user by eliminating the need to rely on a black-box server. It empowers players to audit the randomness themselves, ensuring operators cannot tamper with the outcome. This level of transparency is crucial for maintaining credibility in competitive gaming and online gambling.