File size: 3,228 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
 * Cryptographically-secure RNG helpers for load-balancing / routing selection.
 *
 * OmniRoute's combo target selection (weighted / random / power-of-two-choices), the
 * credential-deck rotation, and shadow-routing sampling pick among upstream
 * providers/connections. CodeQL's `js/insecure-randomness` flags `Math.random()` in these
 * paths as "randomness in a security context" β€” a false positive (provider load-balancing
 * is not a secret, token, nonce or session id). Routing these few, non-hot-path selections
 * through `node:crypto` removes the finding at negligible cost. These are drop-in
 * replacements with identical ranges and semantics:
 *
 *   secureRandomInt(n)   === Math.floor(Math.random() * n)   // integer in [0, n)
 *   secureRandomFloat()  === Math.random()                   // float   in [0, 1)
 *
 * The integer helper uses `crypto.randomInt` (unbiased rejection sampling) rather than
 * `Math.floor(cryptoFloat() * n)` β€” dividing/rounding a crypto value introduces modulo bias
 * (CodeQL `js/biased-cryptographic-random`).
 */
import { randomBytes, randomInt } from "node:crypto";

/** Default source: uniform float in [0, 1) from 48 bits of crypto entropy. */
function cryptoRandomFloat(): number {
  const buf = randomBytes(6);
  let value = 0;
  for (let i = 0; i < buf.length; i++) {
    value = value * 256 + buf[i];
  }
  return value / 2 ** 48;
}

// Test-only deterministic float source β€” `null` in production (the crypto sources above are
// used directly). Tests inject a fixed sequence via _setSecureRandomFloatSource (mirrors the
// `_resetAllDecks` test-only export). When injected, secureRandomInt derives the index from
// the same float so the deterministic selection tests keep identical assertions; production
// never takes that path, so no crypto value is divided/rounded into a biased integer.
let testFloatSource: (() => number) | null = null;

/** Uniform float in [0, 1) β€” drop-in for `Math.random()`. */
export function secureRandomFloat(): number {
  return testFloatSource ? testFloatSource() : cryptoRandomFloat();
}

/**
 * Uniform integer in [0, maxExclusive) β€” drop-in for `Math.floor(Math.random() * maxExclusive)`.
 * Returns 0 for any `maxExclusive <= 1` (matching `Math.floor(Math.random() * {0,1})`), so
 * single-element / empty selections behave exactly as before. Production uses the unbiased
 * `crypto.randomInt`; only the test path (deterministic injected source) scales a float.
 */
export function secureRandomInt(maxExclusive: number): number {
  if (!Number.isFinite(maxExclusive) || maxExclusive <= 1) return 0;
  const max = Math.floor(maxExclusive);
  if (testFloatSource) {
    // Deterministic test path β€” clamp defends the probability-0 case of a source returning ~1.
    return Math.min(max - 1, Math.floor(testFloatSource() * max));
  }
  return randomInt(max);
}

/**
 * TEST ONLY β€” replace the underlying RNG with a deterministic source so selection logic can
 * be asserted; pass `null` to restore the crypto source. Mirrors the `_resetAllDecks`
 * test-only export convention in shuffleDeck.ts.
 */
export function _setSecureRandomFloatSource(source: (() => number) | null): void {
  testFloatSource = source ?? null;
}