diff --git a/packages/coinjoin/src/backend/CoinjoinBackendClient.ts b/packages/coinjoin/src/backend/CoinjoinBackendClient.ts index 99de08ffa8..449ed206f0 100644 --- a/packages/coinjoin/src/backend/CoinjoinBackendClient.ts +++ b/packages/coinjoin/src/backend/CoinjoinBackendClient.ts @@ -1,4 +1,4 @@ -import { scheduleAction, arrayShuffle, urlToOnion } from '@trezor/utils'; +import { scheduleAction, arrayShuffle, urlToOnion, getWeakRandomInt } from '@trezor/utils'; import { TypedEmitter } from '@trezor/utils'; import type { BlockbookAPI } from '@trezor/blockchain-link/src/workers/blockbook/websocket'; @@ -38,7 +38,7 @@ export class CoinjoinBackendClient { constructor(settings: CoinjoinBackendClientSettings) { this.logger = settings.logger; - this.blockbookUrls = arrayShuffle(settings.blockbookUrls); + this.blockbookUrls = arrayShuffle(settings.blockbookUrls, { randomInt: getWeakRandomInt }); this.onionDomains = settings.onionDomains ?? {}; this.blockbookRequestId = Math.floor(Math.random() * settings.blockbookUrls.length); this.websockets = new CoinjoinWebsocketController(settings); diff --git a/packages/coinjoin/src/client/round/outputRegistration.ts b/packages/coinjoin/src/client/round/outputRegistration.ts index 19d9c5a815..6c7ac00340 100644 --- a/packages/coinjoin/src/client/round/outputRegistration.ts +++ b/packages/coinjoin/src/client/round/outputRegistration.ts @@ -1,4 +1,4 @@ -import { getWeakRandomId, arrayShuffle } from '@trezor/utils'; +import { getWeakRandomId, arrayShuffle, getWeakRandomInt } from '@trezor/utils'; import * as coordinator from '../coordinator'; import * as middleware from '../middleware'; @@ -160,7 +160,7 @@ export const outputRegistration = async ( const assignedAddresses: AccountAddress[] = []; return Promise.all( - arrayShuffle(outputs).map(output => + arrayShuffle(outputs, { randomInt: getWeakRandomInt }).map(output => registerOutput(round, account, output, assignedAddresses, options), ), ); @@ -170,7 +170,9 @@ export const outputRegistration = async ( round.setSessionPhase(SessionPhase.AwaitingOthersOutputs); // inform coordinator that each registered input is ready to sign await Promise.all( - arrayShuffle(round.inputs).map(input => readyToSign(round, input, options)), + arrayShuffle(round.inputs, { randomInt: getWeakRandomInt }).map(input => + readyToSign(round, input, options), + ), ); logger.info(`Ready to sign ~~${round.id}~~`); } catch (error) { diff --git a/packages/coinjoin/src/client/round/transactionSigning.ts b/packages/coinjoin/src/client/round/transactionSigning.ts index d8a5677696..9424d587da 100644 --- a/packages/coinjoin/src/client/round/transactionSigning.ts +++ b/packages/coinjoin/src/client/round/transactionSigning.ts @@ -1,4 +1,4 @@ -import { arrayShuffle } from '@trezor/utils'; +import { arrayShuffle, getWeakRandomInt } from '@trezor/utils'; import * as coordinator from '../coordinator'; import * as middleware from '../middleware'; @@ -234,7 +234,7 @@ export const transactionSigning = async ( round.setSessionPhase(SessionPhase.SendingSignature); await Promise.all( - arrayShuffle(round.inputs).map(input => + arrayShuffle(round.inputs, { randomInt: getWeakRandomInt }).map(input => sendTxSignature(round, resolvedTime, input, options), ), ); diff --git a/packages/coinjoin/tests/client/CoinjoinRound.test.ts b/packages/coinjoin/tests/client/CoinjoinRound.test.ts index 4b17e5ee07..3de3079a47 100644 --- a/packages/coinjoin/tests/client/CoinjoinRound.test.ts +++ b/packages/coinjoin/tests/client/CoinjoinRound.test.ts @@ -12,7 +12,7 @@ jest.mock('@trezor/utils', () => { return { __esModule: true, ...originalModule, - getRandomNumberInRange: () => 0, + getWeakRandomNumberInRange: () => 0, }; }); diff --git a/packages/coinjoin/tests/client/connectionConfirmation.test.ts b/packages/coinjoin/tests/client/connectionConfirmation.test.ts index 985863ec82..573358c02a 100644 --- a/packages/coinjoin/tests/client/connectionConfirmation.test.ts +++ b/packages/coinjoin/tests/client/connectionConfirmation.test.ts @@ -14,7 +14,7 @@ jest.mock('@trezor/utils', () => { return { __esModule: true, ...originalModule, - getRandomNumberInRange: () => 0, + getWeakRandomNumberInRange: () => 0, }; }); diff --git a/packages/coinjoin/tests/client/inputRegistration.test.ts b/packages/coinjoin/tests/client/inputRegistration.test.ts index d6afb9f851..b90b12437a 100644 --- a/packages/coinjoin/tests/client/inputRegistration.test.ts +++ b/packages/coinjoin/tests/client/inputRegistration.test.ts @@ -10,7 +10,7 @@ jest.mock('@trezor/utils', () => { return { __esModule: true, ...originalModule, - getRandomNumberInRange: () => 0, + getWeakRandomNumberInRange: () => 0, }; }); diff --git a/packages/coinjoin/tests/client/outputRegistration.test.ts b/packages/coinjoin/tests/client/outputRegistration.test.ts index 890efd0155..f927f41d9c 100644 --- a/packages/coinjoin/tests/client/outputRegistration.test.ts +++ b/packages/coinjoin/tests/client/outputRegistration.test.ts @@ -11,7 +11,7 @@ jest.mock('@trezor/utils', () => { return { __esModule: true, ...originalModule, - getRandomNumberInRange: () => 0, + getWeakRandomNumberInRange: () => 0, }; }); diff --git a/packages/coinjoin/tests/client/transactionSigning.test.ts b/packages/coinjoin/tests/client/transactionSigning.test.ts index 96addd9da8..66cf63eb7b 100644 --- a/packages/coinjoin/tests/client/transactionSigning.test.ts +++ b/packages/coinjoin/tests/client/transactionSigning.test.ts @@ -13,7 +13,7 @@ jest.mock('@trezor/utils', () => { return { __esModule: true, ...originalModule, - getRandomNumberInRange: jest.fn(() => 0), + getWeakRandomNumberInRange: jest.fn(() => 0), }; }); diff --git a/packages/coinjoin/tests/utils/roundUtils.test.ts b/packages/coinjoin/tests/utils/roundUtils.test.ts index 1d54ea200f..c28166694f 100644 --- a/packages/coinjoin/tests/utils/roundUtils.test.ts +++ b/packages/coinjoin/tests/utils/roundUtils.test.ts @@ -19,7 +19,7 @@ jest.mock('@trezor/utils', () => { return { __esModule: true, ...originalModule, - getRandomNumberInRange: jest.fn(originalModule.getRandomNumberInRange), + getWeakRandomNumberInRange: jest.fn(originalModule.getWeakRandomNumberInRange), }; }); diff --git a/packages/utils/src/arrayShuffle.ts b/packages/utils/src/arrayShuffle.ts index 00ccb64e34..ee5b0f6d33 100644 --- a/packages/utils/src/arrayShuffle.ts +++ b/packages/utils/src/arrayShuffle.ts @@ -5,10 +5,14 @@ * * This method does not mutate the original array. */ -export const arrayShuffle = (array: readonly T[]): T[] => { +export const arrayShuffle = ( + array: readonly T[], + { randomInt }: { randomInt: (min: number, max: number) => number }, +): T[] => { const shuffled = array.slice(); for (let i = shuffled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); + const j = randomInt(0, i + 1); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } diff --git a/packages/utils/src/getWeakRandomInt.ts b/packages/utils/src/getWeakRandomInt.ts new file mode 100644 index 0000000000..df458c5c02 --- /dev/null +++ b/packages/utils/src/getWeakRandomInt.ts @@ -0,0 +1,13 @@ +/** + * @param min Inclusive + * @param max Exclusive + */ +export const getWeakRandomInt = (min: number, max: number) => { + if (min >= max) { + throw new RangeError( + `The value of "max" is out of range. It must be greater than the value of "min" (${min}). Received ${max}`, + ); + } + + return Math.floor(Math.random() * (max - min) + min); +}; diff --git a/packages/utils/src/getWeakRandomNumberInRange.ts b/packages/utils/src/getWeakRandomNumberInRange.ts index 3c16a0315e..ba9c2e225e 100644 --- a/packages/utils/src/getWeakRandomNumberInRange.ts +++ b/packages/utils/src/getWeakRandomNumberInRange.ts @@ -1,2 +1,8 @@ +/** + * @deprecated Use `getWeakRandomInt` instead. + * + * @param min Inclusive + * @param max Inclusive + */ export const getWeakRandomNumberInRange = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 29f1bc009f..9c295f83da 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -22,6 +22,7 @@ export * from './getNumberFromPixelString'; export * from './getWeakRandomNumberInRange'; export * from './getSynchronize'; export * from './getWeakRandomId'; +export * from './getWeakRandomInt'; export * from './hasUppercaseLetter'; export * from './isAscii'; export * from './isHex'; diff --git a/packages/utils/tests/arrayShuffle.test.ts b/packages/utils/tests/arrayShuffle.test.ts index cf2a94a5d4..5cb3bb21d0 100644 --- a/packages/utils/tests/arrayShuffle.test.ts +++ b/packages/utils/tests/arrayShuffle.test.ts @@ -1,4 +1,5 @@ import { arrayShuffle } from '../src/arrayShuffle'; +import { getWeakRandomInt } from '../src/getWeakRandomInt'; const KEYS = ['a', 'b', 'c', 'd', 'e']; const SAMPLES = 10000; @@ -13,7 +14,7 @@ describe(arrayShuffle.name, () => { const samples = Object.fromEntries(KEYS.map(key => [key, new Array(KEYS.length).fill(0)])); for (let sample = 0; sample < SAMPLES; ++sample) { - const shuffled = arrayShuffle(KEYS); + const shuffled = arrayShuffle(KEYS, { randomInt: getWeakRandomInt }); for (let i = 0; i < shuffled.length; ++i) { samples[shuffled[i]][i]++; } diff --git a/packages/utils/tests/getWeakRandomInt.test.ts b/packages/utils/tests/getWeakRandomInt.test.ts new file mode 100644 index 0000000000..0f1e6132a2 --- /dev/null +++ b/packages/utils/tests/getWeakRandomInt.test.ts @@ -0,0 +1,26 @@ +import { getWeakRandomInt } from '../src'; + +describe(getWeakRandomInt.name, () => { + it('raises same error as randomInt from crypto when max <= min', () => { + const EXPECTED_ERROR = new RangeError( + 'The value of "max" is out of range. It must be greater than the value of "min" (0). Received -1', + ); + + expect(() => getWeakRandomInt(0, -1)).toThrowError(EXPECTED_ERROR); + }); + + it('returns same value when range is trivial', () => { + expect(getWeakRandomInt(0, 1)).toEqual(0); + expect(getWeakRandomInt(100, 101)).toEqual(100); + }); + + it('returns same value when range is trivial', () => { + for (let i = 0; i < 10_000; i++) { + const result = getWeakRandomInt(0, 100); + + expect(Number.isInteger(result)).toBe(true); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThan(100); + } + }); +}); diff --git a/packages/utils/tests/getWeakRandomNumberInRange.test.ts b/packages/utils/tests/getWeakRandomNumberInRange.test.ts new file mode 100644 index 0000000000..25d2290976 --- /dev/null +++ b/packages/utils/tests/getWeakRandomNumberInRange.test.ts @@ -0,0 +1,11 @@ +import { getWeakRandomNumberInRange } from '../src'; + +describe(getWeakRandomNumberInRange.name, () => { + it('returns value in range', () => { + for (let i = 0; i < 10_000; i++) { + const result = getWeakRandomNumberInRange(0, 100); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(100); + } + }); +});