skip to content
logo
Table of Contents

Random Number Generation in JavaScript Apps

Random Number Generation (RNG) is critical in many applications, from gaming to cryptography. In this post, we’ll examine how provably fair RNG systems work and how to implement them in JavaScript.


The Challenge of True Randomness

Generating truly random numbers in software is fundamentally difficult. Computers execute deterministic operations, making true randomness hard to achieve through algorithms alone.

Standard JavaScript RNG

The built-in Math.random() function provides pseudorandom numbers between 0 and 1:

const randomValue = Math.random();
console.log(randomValue); // 0.3942399501622835 (example output)

However, it has limitations:

  1. It’s deterministic (based on a seed)
  2. It’s not cryptographically secure
  3. It’s not provably fair for applications requiring verification

Cryptographically Secure RNG

For applications requiring higher security, use the Web Crypto API instead:

const generateSecureRandomNumber = () => {
const array = new Uint32Array(1);
window.crypto.getRandomValues(array);
return array[0] / (0xffffffff + 1); // Normalize to [0, 1)
};
const secureRandom = generateSecureRandomNumber();
console.log(secureRandom);

This approach pulls entropy from the operating system, making it more suitable for security-critical applications.


Provably Fair Systems

Provably fair systems allow users to verify that the outcomes weren’t manipulated. Common in gambling platforms, they rely on three key components:

  1. Server Seed: Secret random value generated by the server
  2. Client Seed: Public value provided by or visible to the client
  3. Nonce: Counter that changes with each generation

Implementation Example

const crypto = require('crypto');
class ProvablyFairSystem {
constructor(serverSeed, clientSeed) {
this.serverSeed = serverSeed;
this.clientSeed = clientSeed;
this.nonce = 0;
}
generateHash() {
const message = `${this.clientSeed}:${this.nonce}`;
const hash = crypto.createHmac('sha256', this.serverSeed)
.update(message)
.digest('hex');
this.nonce++;
return hash;
}
generateRandomNumber(min = 0, max = 1) {
const hash = this.generateHash();
const decimal = parseInt(hash.substr(0, 8), 16) / 0xffffffff;
return min + decimal * (max - min);
}
// For verification after revealing server seed
verifyResult(serverSeed, clientSeed, nonce, expectedResult) {
const message = `${clientSeed}:${nonce}`;
const hash = crypto.createHmac('sha256', serverSeed)
.update(message)
.digest('hex');
const decimal = parseInt(hash.substr(0, 8), 16) / 0xffffffff;
const result = Math.floor(decimal * 100) / 100;
return result === expectedResult;
}
}

The Verification Process

Provably fair systems work by the following process:

  1. The server generates a server seed and provides a hash of it to the client
  2. The client provides or sees their client seed
  3. The system generates results using both seeds
  4. After the process completes, the server reveals the original server seed
  5. The client can verify all results using the disclosed server seed and original client seed

Client-Side Implementation

Here’s how to implement a verification system in the browser:

const verifyFairness = (serverSeed, clientSeed, nonce, result) => {
const message = `${clientSeed}:${nonce}`;
const hmac = CryptoJS.HmacSHA256(message, serverSeed);
const hash = hmac.toString(CryptoJS.enc.Hex);
// Use first 8 characters of hash as the random value source
const decimal = parseInt(hash.slice(0, 8), 16) / 0xffffffff;
const calculatedResult = Math.floor(decimal * 100) / 100;
return {
isValid: calculatedResult === result,
expected: calculatedResult,
received: result
};
};

Enhancing Entropy Sources

For increased randomness, combine multiple entropy sources:

const generateEnhancedSeed = () => {
const sources = [
Date.now().toString(),
performance.now().toString(),
navigator.userAgent,
screen.width + 'x' + screen.height,
new Uint32Array(1).map(() => window.crypto.getRandomValues(new Uint32Array(1))[0])
];
const combinedSource = sources.join('|');
return crypto.createHash('sha256').update(combinedSource).digest('hex');
};

Best Practices for RNG Implementation

  1. Never rely solely on client-side RNG for security-critical applications
  2. Use cryptographically secure RNG like Web Crypto API instead of Math.random()
  3. Implement server-side verification to prevent manipulation
  4. Provide transparency by clearly documenting your RNG methodology
  5. Allow result verification through provably fair mechanisms when appropriate

Conclusion

Implementing provably fair random number generation in JavaScript requires careful consideration of security, transparency, and verification processes. While standard RNG methods like Math.random() are convenient, applications requiring fairness verification demand more sophisticated approaches involving cryptographic techniques and proper seed management.

By implementing the patterns described in this article, you can create RNG systems that not only function properly but also build trust with users through verifiable fairness mechanisms.