feat: Refactor random number generation to use a secure method from utils

This commit is contained in:
Morax
2025-07-29 12:19:42 +08:00
parent 755fbb7076
commit d5309f03c3
3 changed files with 21 additions and 44 deletions

View File

@@ -2,9 +2,7 @@ import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { TarotCard, CardOrientation, CardCategory } from './types.js'; import { TarotCard, CardOrientation, CardCategory } from './types.js';
import { getSecureRandom } from './utils.js';
// Use global crypto in Node.js >= 18 and browsers
declare const crypto: any;
// Helper to get __dirname in ES modules // Helper to get __dirname in ES modules
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -206,19 +204,6 @@ export class TarotCardManager {
return undefined; return undefined;
} }
/**
* Generate a cryptographically secure random number between 0 and 1.
*/
private getSecureRandom(): number {
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
const array = new Uint32Array(1);
crypto.getRandomValues(array);
return array[0] / (0xffffffff + 1);
}
// Fallback for older Node.js or unexpected environments
console.warn('crypto.getRandomValues not available, falling back to Math.random().');
return Math.random();
}
/** /**
* Fisher-Yates shuffle algorithm for true randomness. * Fisher-Yates shuffle algorithm for true randomness.
@@ -226,7 +211,7 @@ export class TarotCardManager {
private fisherYatesShuffle<T>(array: readonly T[]): T[] { private fisherYatesShuffle<T>(array: readonly T[]): T[] {
const shuffled = [...array]; const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) { for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(this.getSecureRandom() * (i + 1)); const j = Math.floor(getSecureRandom() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
} }
return shuffled; return shuffled;
@@ -236,7 +221,7 @@ export class TarotCardManager {
* Get a random card from the deck. * Get a random card from the deck.
*/ */
public getRandomCard(): TarotCard { public getRandomCard(): TarotCard {
const randomIndex = Math.floor(this.getSecureRandom() * this.allCards.length); const randomIndex = Math.floor(getSecureRandom() * this.allCards.length);
return this.allCards[randomIndex]; return this.allCards[randomIndex];
} }

View File

@@ -2,6 +2,7 @@ import { TarotCardManager } from "./card-manager.js";
import { TarotSessionManager } from "./session-manager.js"; import { TarotSessionManager } from "./session-manager.js";
import { TarotReading, DrawnCard, CardOrientation, TarotCard } from "./types.js"; import { TarotReading, DrawnCard, CardOrientation, TarotCard } from "./types.js";
import { TAROT_SPREADS, getAllSpreads, getSpread, isValidSpreadType } from "./spreads.js"; import { TAROT_SPREADS, getAllSpreads, getSpread, isValidSpreadType } from "./spreads.js";
import { getSecureRandom } from "./utils.js";
/** /**
* Manages tarot readings and interpretations * Manages tarot readings and interpretations
@@ -1115,41 +1116,17 @@ export class TarotReadingManager {
*/ */
private getSecureRandomOrientation(): CardOrientation { private getSecureRandomOrientation(): CardOrientation {
// Use the same secure random method as card manager // Use the same secure random method as card manager
const random = this.getSecureRandom(); const random = getSecureRandom();
return random < 0.5 ? "upright" : "reversed"; // 50% chance upright, 50% reversed return random < 0.5 ? "upright" : "reversed"; // 50% chance upright, 50% reversed
} }
/**
* Generate cryptographically secure random number
*/
private getSecureRandom(): number {
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
// Browser environment with Web Crypto API
const array = new Uint32Array(1);
crypto.getRandomValues(array);
return array[0] / (0xffffffff + 1);
} else if (typeof require !== 'undefined') {
// Node.js environment
try {
const crypto = require('crypto');
return crypto.randomBytes(4).readUInt32BE(0) / (0xffffffff + 1);
} catch (e) {
// Fallback to Math.random if crypto is not available
console.warn('Crypto module not available, falling back to Math.random()');
return Math.random();
}
} else {
// Fallback to Math.random
return Math.random();
}
}
/** /**
* Generate a unique reading ID with secure randomness * Generate a unique reading ID with secure randomness
*/ */
private generateReadingId(): string { private generateReadingId(): string {
const timestamp = Date.now(); const timestamp = Date.now();
const randomPart = Math.floor(this.getSecureRandom() * 1000000000).toString(36); const randomPart = Math.floor(getSecureRandom() * 1000000000).toString(36);
return `reading_${timestamp}_${randomPart}`; return `reading_${timestamp}_${randomPart}`;
} }
} }

15
src/tarot/utils.ts Normal file
View File

@@ -0,0 +1,15 @@
// Use global crypto in Node.js >= 18 and browsers
declare const crypto: any;
/**
* Generate a cryptographically secure random number between 0 and 1.
* @throws {Error} If crypto.getRandomValues is not available.
*/
export function getSecureRandom(): number {
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
const array = new Uint32Array(1);
crypto.getRandomValues(array);
return array[0] / (0xffffffff + 1);
}
throw new Error('A secure random number generator (crypto.getRandomValues) is not available in this environment.');
}