From 568d37a45caabdcd2fc2e119d6d537a15263271a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:54:42 +0000 Subject: [PATCH] Fix insecure random number generator in backoff logic Replaced `Math.random` with a cryptographically secure random number generator using `node:crypto.randomInt` in `src/lock-ops.mjs` to prevent potential timing attacks or predictability in backoff delays. Added `test/secure-random.test.mjs` to verify the secure random generator. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- src/lock-ops.mjs | 13 +++++++++++-- test/secure-random.test.mjs | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 test/secure-random.test.mjs diff --git a/src/lock-ops.mjs b/src/lock-ops.mjs index df31b7e..374f78b 100644 --- a/src/lock-ops.mjs +++ b/src/lock-ops.mjs @@ -1,3 +1,4 @@ +import { randomInt } from 'node:crypto'; import { SimplePool } from 'nostr-tools/pool'; import { getQueryTimeoutMs, @@ -134,7 +135,13 @@ function isTransientPublishCategory(category) { ].includes(category); } -function calculateBackoffDelayMs(attemptNumber, baseMs, capMs, randomFn = Math.random) { +const MAX_RANDOM = 281474976710655; // 2**48 - 1 + +function secureRandom() { + return randomInt(0, MAX_RANDOM) / MAX_RANDOM; +} + +function calculateBackoffDelayMs(attemptNumber, baseMs, capMs, randomFn = secureRandom) { const maxDelay = Math.min(capMs, baseMs * (2 ** Math.max(0, attemptNumber - 1))); return Math.floor(randomFn() * maxDelay); } @@ -409,7 +416,7 @@ export async function publishLock(relays, event, deps = {}) { retryBaseDelayMs = 500, retryCapDelayMs = 8_000, sleepFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), - randomFn = Math.random, + randomFn = secureRandom, telemetryLogger = console.error, healthLogger = console.error, diagnostics = {}, @@ -590,3 +597,5 @@ export function _resetRelayHealthState() { relayHealthState.metricsByRelay.clear(); relayHealthState.lastSnapshotAt = 0; } + +export const _secureRandom = secureRandom; diff --git a/test/secure-random.test.mjs b/test/secure-random.test.mjs new file mode 100644 index 0000000..af64c3b --- /dev/null +++ b/test/secure-random.test.mjs @@ -0,0 +1,21 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { _secureRandom } from '../src/lock-ops.mjs'; + +describe('secureRandom', () => { + it('returns values in [0, 1)', () => { + for (let i = 0; i < 1000; i++) { + const val = _secureRandom(); + assert.ok(val >= 0, `Value ${val} should be >= 0`); + assert.ok(val < 1, `Value ${val} should be < 1`); + } + }); + + it('returns varied values', () => { + const values = new Set(); + for (let i = 0; i < 100; i++) { + values.add(_secureRandom()); + } + assert.ok(values.size > 90, 'Values should be reasonably unique'); + }); +});