Files
OmniRoute/src/shared/utils/secureRandom.ts
Diego Rodrigues de Sa e Souza bfaf459f3c Release v3.8.32 (#4418)
Release v3.8.32 — see CHANGELOG.md [3.8.32] for the full list. Merged via --admin over documented non-blocking checks: CodeQL alerts ratchet (#665 fixed by #4457/#4462, auto-closes on main rescan), Integration Tests (env-flaky batch-upstream), SonarCloud/SonarQube (advisory new-code).
2026-06-21 08:56:51 -03:00

67 lines
3.2 KiB
TypeScript

/**
* 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;
}