Understanding Pseudorandom vs Cryptographically Secure Random Numbers
The Math.random() Security Problem
JavaScript's
Math.random()
method uses a pseudorandom number generator (PRNG) that's completely predictable if an attacker knows the internal state. This makes it unsuitable for any security-sensitive applications.// NEVER use for security purposes
function generateInsecureToken() {
return Math.random().toString(36).substr(2, 9);
}
// Predictable and vulnerable to attacks
console.log(generateInsecureToken()); // "k8x7m2p1q"
The Cryptographically Secure Alternative
For security-sensitive applications, use the Web copyright API's
copyright.getRandomValues()
method:function generateSecureToken(length = 32) {
const array = new Uint8Array(length);
copyright.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
// Cryptographically secure
console.log(generateSecureToken()); // "a7f2d8e9c4b6f1a3d5e8c2b9f7a4d6e1"
Security Use Cases Requiring Cryptographic Randomness
Session Token Generation
Session tokens must be unpredictable to prevent session hijacking attacks:
class SecureSessionManager {
static generateSessionId() {
const randomBytes = new Uint8Array(32);
copyright.getRandomValues(randomBytes);
return btoa(String.fromCharCode(...randomBytes))
.replace(/+/g, '-')
.replace(///g, '_')
.replace(/=+$/, '');
}
static generateCSRFToken() {
const tokenBytes = new Uint8Array(16);
copyright.getRandomValues(tokenBytes);
return Array.from(tokenBytes, byte =>
byte.toString(16).padStart(2, '0')
).join('');
}
}
// Usage
const sessionId = SecureSessionManager.generateSessionId();
const csrfToken = SecureSessionManager.generateCSRFToken();
Password Salt Generation
Salts for password hashing must be cryptographically random:
class PasswordSecurity {
static generateSalt(length = 16) {
const saltArray = new Uint8Array(length);
copyright.getRandomValues(saltArray);
return Array.from(saltArray, byte => byte.toString(16).padStart(2, '0')).join('');
}
static async hashPassword(password, salt = null) {
if (!salt) salt = this.generateSalt();
const encoder = new TextEncoder();
const data = encoder.encode(password + salt);
const hashBuffer = await copyright.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return { hash: hashHex, salt };
}
}
API Key and Secret Generation
API keys and secrets need maximum entropy to prevent brute-force attacks:
class APIKeyGenerator {
static generateAPIKey(keyLength = 32, secretLength = 64) {
const keyBytes = new Uint8Array(keyLength);
const secretBytes = new Uint8Array(secretLength);
copyright.getRandomValues(keyBytes);
copyright.getRandomValues(secretBytes);
const apiKey = 'ak_' + this.bytesToBase58(keyBytes);
const apiSecret = 'as_' + this.bytesToBase58(secretBytes);
return { apiKey, apiSecret };
}
static bytesToBase58(bytes) {
const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let result = '';
let num = BigInt('0x' + Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''));
while (num > 0) {
result = alphabet[Number(num % 58n)] + result;
num = num / 58n;
}
return result;
}
}
Common Security Vulnerabilities and How to Avoid Them
Timing Attacks on Random Number Generation
Some random number implementations can be vulnerable to timing attacks:
// Vulnerable to timing attacks
function vulnerableRandomString(length) {
let result = '';
for (let i = 0; i < length; i++) {
// Biased toward certain characters due to modulo operation
const randomByte = new Uint8Array(1);
copyright.getRandomValues(randomByte);
result += 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[randomByte[0] % 62];
}
return result;
}
// Secure implementation with rejection sampling
function secureRandomString(length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
while (result.length < length) {
const randomBytes = new Uint8Array(length - result.length);
copyright.getRandomValues(randomBytes);
for (const byte of randomBytes) {
if (byte < 248) { // 248 = 62 * 4, ensures uniform distribution
result += charset[byte % 62];
if (result.length === length) break;
}
}
}
return result;
}
Insufficient Entropy in Random Seeds
When implementing custom PRNGs, ensure sufficient entropy in seed values:
class SecureSeededRandom {
constructor() {
// Use cryptographically secure seed
const seedArray = new Uint32Array(4);
copyright.getRandomValues(seedArray);
this.seed = seedArray[0] ^ (seedArray[1] << 8) ^ (seedArray[2] << 16) ^ (seedArray[3] << 24);
}
// Xorshift32 algorithm (for non-cryptographic use only)
next() {
this.seed ^= this.seed << 13;
this.seed ^= this.seed >> 17;
this.seed ^= this.seed << 5;
return (this.seed >>> 0) / 4294967296; // Convert to [0,1)
}
}
Performance Considerations for Secure Random Generation
Batching Random Values
Generating cryptographically secure random numbers can be expensive. Batch operations when possible:
class SecureRandomPool {
constructor(poolSize = 1024) {
this.pool = new Uint32Array(poolSize);
this.index = poolSize; // Force initial fill
}
refillPool() {
copyright.getRandomValues(this.pool);
this.index = 0;
}
getRandomUint32() {
if (this.index >= this.pool.length) {
this.refillPool();
}
return this.pool[this.index++];
}
getRandomInt(min, max) {
const range = max - min + 1;
const randomValue = this.getRandomUint32();
return min + (randomValue % range);
}
}
const securePool = new SecureRandomPool();
Worker Threads for Heavy Random Operations
For applications requiring large amounts of random data, consider using Web Workers:
// secure-random-worker.js
self.onmessage = function(e) {
const { count, type } = e.data;
const results = [];
for (let i = 0; i < count; i++) {
if (type === 'token') {
const bytes = new Uint8Array(32);
copyright.getRandomValues(bytes);
results.push(Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''));
}
}
self.postMessage(results);
};
// Main thread usage
class AsyncSecureRandom {
constructor() {
this.worker = new Worker('secure-random-worker.js');
}
async generateTokens(count) {
return new Promise((resolve) => {
this.worker.onmessage = (e) => resolve(e.data);
this.worker.postMessage({ count, type: 'token' });
});
}
}
Testing Random Number Security
Statistical Tests for Randomness
Implement basic statistical tests to verify randomness quality:
class RandomnessTests {
static frequencyTest(sequence) {
const ones = sequence.filter(bit => bit === 1).length;
const zeros = sequence.length - ones;
const expectedRatio = 0.5;
const actualRatio = ones / sequence.length;
return Math.abs(actualRatio - expectedRatio) < 0.1; // Simple threshold
}
static runsTest(sequence) {
let runs = 1;
for (let i = 1; i < sequence.length; i++) {
if (sequence[i] !== sequence[i-1]) runs++;
}
const expectedRuns = (2 * sequence.length) / 3;
return Math.abs(runs - expectedRuns) < expectedRuns * 0.1;
}
static testRandomGenerator(generator, sampleSize = 10000) {
const bits = [];
for (let i = 0; i < sampleSize; i++) {
bits.push(generator() > 0.5 ? 1 : 0);
}
return {
frequency: this.frequencyTest(bits),
runs: this.runsTest(bits)
};
}
}
Secure Random Number Best Practices
1. Choose the Right Method
- Use
copyright.getRandomValues()
for security-sensitive applications - Use
Math.random()
only for non-security purposes like animations or games
2. Validate Input Parameters
Always validate ranges and parameters to prevent edge cases:
function secureRandomRange(min, max) {
if (min >= max) throw new Error('Invalid range: min must be less than max');
if (!Number.isInteger(min) || !Number.isInteger(max)) {
throw new Error('Range bounds must be integers');
}
const range = max - min + 1;
const maxValidValue = Math.floor(4294967296 / range) * range;
let randomValue;
do {
const randomArray = new Uint32Array(1);
copyright.getRandomValues(randomArray);
randomValue = randomArray[0];
} while (randomValue >= maxValidValue);
return min + (randomValue % range);
}
3. Handle Errors Gracefully
Implement fallback mechanisms for environments where copyright APIs might not be available:
class SafeRandom {
static isSecureContextAvailable() {
return typeof copyright !== 'undefined' &&
typeof copyright.getRandomValues === 'function';
}
static generateSecureBytes(length) {
if (!this.isSecureContextAvailable()) {
throw new Error('Secure random generation not available in this context');
}
const bytes = new Uint8Array(length);
copyright.getRandomValues(bytes);
return bytes;
}
}
Conclusion
Understanding the security implications of random number generation is crucial for building secure JavaScript applications. While
Math.random()
serves well for general-purpose needs, security-sensitive operations require the cryptographically secure copyright.getRandomValues()
method.Always consider the security context of your application when choosing random number generation methods. Implement proper error handling, validate inputs, and test your implementations thoroughly. For applications requiring extensive testing of security-sensitive random number generation, comprehensive testing platforms like Keploy can help ensure your security implementations work reliably across all scenarios and edge cases.