From fb7dc6a871e3085f813c78f79c1ae497ae01629d Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Mon, 14 Oct 2024 14:14:24 +0200 Subject: [PATCH 1/2] chore: code improvement for arrayShuffle --- packages/utils/src/arrayShuffle.ts | 9 ++++++--- packages/utils/tests/arrayShuffle.test.ts | 11 ++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/utils/src/arrayShuffle.ts b/packages/utils/src/arrayShuffle.ts index 80fed358b75..00ccb64e34a 100644 --- a/packages/utils/src/arrayShuffle.ts +++ b/packages/utils/src/arrayShuffle.ts @@ -1,8 +1,11 @@ /** - * Randomly shuffles the elements in an array. This method - * does not mutate the original array. + * Implementation of the Fisher-Yates shuffle algorithm. + * The algorithm produces an unbiased permutation: every permutation is equally likely. + * @link https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + * + * This method does not mutate the original array. */ -export const arrayShuffle = (array: readonly T[]) => { +export const arrayShuffle = (array: readonly T[]): T[] => { const shuffled = array.slice(); for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); diff --git a/packages/utils/tests/arrayShuffle.test.ts b/packages/utils/tests/arrayShuffle.test.ts index 46db9c18bf0..cf2a94a5d43 100644 --- a/packages/utils/tests/arrayShuffle.test.ts +++ b/packages/utils/tests/arrayShuffle.test.ts @@ -5,19 +5,24 @@ const SAMPLES = 10000; const TOLERANCE = 0.1; const EXPECTED = SAMPLES / KEYS.length; -describe('arrayShuffle', () => { +const LOWER_BOUND = (1 - TOLERANCE) * EXPECTED; +const UPPER_BOUND = (1 + TOLERANCE) * EXPECTED; + +describe(arrayShuffle.name, () => { it('shuffles randomly', () => { 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); for (let i = 0; i < shuffled.length; ++i) { samples[shuffled[i]][i]++; } } + KEYS.forEach(key => samples[key].forEach(count => { - expect(count).toBeGreaterThanOrEqual((1 - TOLERANCE) * EXPECTED); - expect(count).toBeLessThanOrEqual((1 + TOLERANCE) * EXPECTED); + expect(count).toBeGreaterThanOrEqual(LOWER_BOUND); + expect(count).toBeLessThanOrEqual(UPPER_BOUND); }), ); }); From 42529a9dd86f9aa2151d091217bf758a65d5e653 Mon Sep 17 00:00:00 2001 From: Peter Sanderson Date: Mon, 14 Oct 2024 14:09:56 +0200 Subject: [PATCH 2/2] fix: better randomInt, and arrayShuffle --- .../coinjoin/src/client/round/endedRound.ts | 4 +-- .../src/client/round/inputRegistration.ts | 4 +-- packages/coinjoin/src/utils/roundUtils.ts | 4 +-- .../tests/client/CoinjoinRound.test.ts | 4 +-- .../tests/client/transactionSigning.test.ts | 8 ++--- .../coinjoin/tests/utils/roundUtils.test.ts | 20 ++++++------- packages/connect-popup/package.json | 1 + .../webpack/prod.webpack.config.ts | 5 ++++ packages/connect-web/package.json | 3 +- .../connect-web/webpack/dev.webpack.config.ts | 7 +++++ .../webpack/prod.webpack.config.ts | 4 +++ packages/connect-webextension/package.json | 1 + .../webpack/prod.webpack.config.ts | 5 ++++ .../configs/base.webpack.config.ts | 2 +- .../TransactionList/NoSearchResults.tsx | 4 +-- packages/transport-bridge/package.json | 1 + .../webpack/ui.webpack.config.ts | 5 ++++ packages/transport-test/package.json | 3 +- packages/utils/src/arrayShuffle.ts | 4 ++- packages/utils/src/getRandomInt.ts | 17 +++++++++++ packages/utils/src/getRandomNumberInRange.ts | 2 -- .../utils/src/getWeakRandomNumberInRange.ts | 5 ++++ packages/utils/src/index.ts | 3 +- packages/utils/tests/getRandomInt.test.ts | 30 +++++++++++++++++++ yarn.lock | 5 ++++ 25 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 packages/utils/src/getRandomInt.ts delete mode 100644 packages/utils/src/getRandomNumberInRange.ts create mode 100644 packages/utils/src/getWeakRandomNumberInRange.ts create mode 100644 packages/utils/tests/getRandomInt.test.ts diff --git a/packages/coinjoin/src/client/round/endedRound.ts b/packages/coinjoin/src/client/round/endedRound.ts index 844af2a7324..1d561b9f35f 100644 --- a/packages/coinjoin/src/client/round/endedRound.ts +++ b/packages/coinjoin/src/client/round/endedRound.ts @@ -1,4 +1,4 @@ -import { enumUtils, getRandomNumberInRange } from '@trezor/utils'; +import { enumUtils, getWeakRandomNumberInRange } from '@trezor/utils'; import type { CoinjoinRound, CoinjoinRoundOptions } from '../CoinjoinRound'; import { EndRoundState, WabiSabiProtocolErrorCode } from '../../enums'; @@ -56,7 +56,7 @@ export const ended = (round: CoinjoinRound, { logger, network }: CoinjoinRoundOp // repeated input-registration will tell if they are really banned, // make sure that addresses registered in round are recycled (reset Infinity sentence) const minute = 60 * 1000; - const sentenceEnd = getRandomNumberInRange(5 * minute, 10 * minute); + const sentenceEnd = getWeakRandomNumberInRange(5 * minute, 10 * minute); [...inputs, ...addresses].forEach(vinvout => prison.detain(vinvout, { sentenceEnd, diff --git a/packages/coinjoin/src/client/round/inputRegistration.ts b/packages/coinjoin/src/client/round/inputRegistration.ts index 2e20812a4a3..9bc93b77d5b 100644 --- a/packages/coinjoin/src/client/round/inputRegistration.ts +++ b/packages/coinjoin/src/client/round/inputRegistration.ts @@ -1,4 +1,4 @@ -import { getRandomNumberInRange } from '@trezor/utils'; +import { getWeakRandomNumberInRange } from '@trezor/utils'; import * as coordinator from '../coordinator'; import * as middleware from '../middleware'; @@ -56,7 +56,7 @@ const registerInput = async ( // setup random delay for registration request. we want each input to be registered in different time as different TOR identity // note that this may cause that the input will not be registered if phase change before expected deadline const deadline = round.phaseDeadline - Date.now() - ROUND_SELECTION_REGISTRATION_OFFSET; - const delay = deadline > 0 ? getRandomNumberInRange(0, deadline) : 0; + const delay = deadline > 0 ? getWeakRandomNumberInRange(0, deadline) : 0; logger.info( `Trying to register ~~${input.outpoint}~~ to ~~${round.id}~~ with delay ${delay}ms and deadline ${round.phaseDeadline}`, ); diff --git a/packages/coinjoin/src/utils/roundUtils.ts b/packages/coinjoin/src/utils/roundUtils.ts index 93fa5ab67d1..763d294a43b 100644 --- a/packages/coinjoin/src/utils/roundUtils.ts +++ b/packages/coinjoin/src/utils/roundUtils.ts @@ -1,5 +1,5 @@ import { bufferutils, Transaction, Network } from '@trezor/utxo-lib'; -import { getRandomNumberInRange } from '@trezor/utils'; +import { getWeakRandomNumberInRange } from '@trezor/utils'; import { COORDINATOR_FEE_RATE_FALLBACK, @@ -88,7 +88,7 @@ export const scheduleDelay = ( // and at most 1 sec before the calculated max (so there's room for randomness) const min = clamp(minimumDelay, 0, max - 1000); - return getRandomNumberInRange(min, max); + return getWeakRandomNumberInRange(min, max); }; // NOTE: deadlines are not accurate. phase may change earlier diff --git a/packages/coinjoin/tests/client/CoinjoinRound.test.ts b/packages/coinjoin/tests/client/CoinjoinRound.test.ts index f6a3dc638d4..4b17e5ee079 100644 --- a/packages/coinjoin/tests/client/CoinjoinRound.test.ts +++ b/packages/coinjoin/tests/client/CoinjoinRound.test.ts @@ -188,7 +188,7 @@ describe(`CoinjoinRound`, () => { it('onPhaseChange lock cool off resolved', async () => { const delayMock = jest - .spyOn(trezorUtils, 'getRandomNumberInRange') + .spyOn(trezorUtils, 'getWeakRandomNumberInRange') .mockImplementation(() => 800); const constantsMock = jest @@ -396,7 +396,7 @@ describe(`CoinjoinRound`, () => { it('unregisterAccount when round is locked', async () => { const delayMock = jest - .spyOn(trezorUtils, 'getRandomNumberInRange') + .spyOn(trezorUtils, 'getWeakRandomNumberInRange') .mockImplementation(() => 800); const constantsMock = jest diff --git a/packages/coinjoin/tests/client/transactionSigning.test.ts b/packages/coinjoin/tests/client/transactionSigning.test.ts index 35db099f4f4..96addd9da80 100644 --- a/packages/coinjoin/tests/client/transactionSigning.test.ts +++ b/packages/coinjoin/tests/client/transactionSigning.test.ts @@ -1,5 +1,5 @@ import { networks } from '@trezor/utxo-lib'; -import { getRandomNumberInRange } from '@trezor/utils'; +import { getWeakRandomNumberInRange } from '@trezor/utils'; import { transactionSigning } from '../../src/client/round/transactionSigning'; import { createServer } from '../mocks/server'; @@ -529,7 +529,7 @@ describe('transactionSigning signature delay', () => { ); // signature is sent in range 17-67 sec. (resolve time is less than 50 sec TX_SIGNING_DELAY) - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(17000, 67000); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(17000, 67000); expect(response.isSignedSuccessfully()).toBe(true); }); @@ -558,7 +558,7 @@ describe('transactionSigning signature delay', () => { ); // signature is sent in range 0-46.21 sec. (resolve time is greater than 50 sec of TX_SIGNING_DELAY) - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(0, 46210); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(0, 46210); expect(response.isSignedSuccessfully()).toBe(true); }); @@ -588,7 +588,7 @@ describe('transactionSigning signature delay', () => { ); // signature is sent in default range 0-1 sec. - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(0, 1000); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(0, 1000); expect(response.isSignedSuccessfully()).toBe(true); }); }); diff --git a/packages/coinjoin/tests/utils/roundUtils.test.ts b/packages/coinjoin/tests/utils/roundUtils.test.ts index fbb6597ea29..1d54ea200f9 100644 --- a/packages/coinjoin/tests/utils/roundUtils.test.ts +++ b/packages/coinjoin/tests/utils/roundUtils.test.ts @@ -1,4 +1,4 @@ -import { getRandomNumberInRange } from '@trezor/utils'; +import { getWeakRandomNumberInRange } from '@trezor/utils'; import { getCommitmentData, @@ -150,38 +150,38 @@ describe('roundUtils', () => { // default (no min, no max) range 0-10 sec. resultInRange(scheduleDelay(60000), 0, 10000); - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(0, 10000); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(0, 10000); // range 3-10sec. resultInRange(scheduleDelay(20000, 3000), 3000, 10000); - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(3000, 10000); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(3000, 10000); // deadlineOffset < 0, range 0-1 sec. resultInRange(scheduleDelay(1000, 3000), 0, 1000); - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(0, 1000); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(0, 1000); // deadline < min, range 9-10 sec. resultInRange(scheduleDelay(60000, 61000), 9000, 10000); - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(9000, 10000); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(9000, 10000); // deadline < min && deadline < max, range 49-50 sec. resultInRange(scheduleDelay(60000, 61000, 62000), 49000, 50000); - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(49000, 50000); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(49000, 50000); // deadline > min && deadline < max, range 3-20 sec. resultInRange(scheduleDelay(30000, 3000, 50000), 3000, 20000); - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(3000, 20000); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(3000, 20000); // min < 0 && deadline < max && deadlineOffset > 0, range 0-2.5 sec. resultInRange(scheduleDelay(12500, -3000, 50000), 0, 2500); - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(0, 2500); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(0, 2500); // min < 0 && max < 0 && deadlineOffset > 0, range 0-1 sec. resultInRange(scheduleDelay(12500, -10000, -5000), 0, 1000); - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(0, 1000); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(0, 1000); // min < 0 && max < 0 && deadlineOffset < 0, range 0-1 sec. resultInRange(scheduleDelay(7500, -10000, -5000), 0, 1000); - expect(getRandomNumberInRange).toHaveBeenLastCalledWith(0, 1000); + expect(getWeakRandomNumberInRange).toHaveBeenLastCalledWith(0, 1000); }); }); diff --git a/packages/connect-popup/package.json b/packages/connect-popup/package.json index 41db6cafa1b..e51c8f574a4 100644 --- a/packages/connect-popup/package.json +++ b/packages/connect-popup/package.json @@ -25,6 +25,7 @@ "@trezor/device-utils": "workspace:*", "@trezor/transport": "workspace:*", "@trezor/urls": "workspace:*", + "crypto-browserify": "^3.12.0", "eth-phishing-detect": "^1.2.0", "react-dom": "18.2.0" }, diff --git a/packages/connect-popup/webpack/prod.webpack.config.ts b/packages/connect-popup/webpack/prod.webpack.config.ts index 3840b4b4fbe..61fb6cea857 100644 --- a/packages/connect-popup/webpack/prod.webpack.config.ts +++ b/packages/connect-popup/webpack/prod.webpack.config.ts @@ -69,6 +69,11 @@ const config: webpack.Configuration = { extensions: ['.ts', '.tsx', '.js', '.jsx'], modules: ['node_modules'], mainFields: ['browser', 'module', 'main'], + fallback: { + // Polyfills crypto API for Node.js libraries in the browser. 'crypto' does not run without 'stream' + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + }, }, plugins: [ new DefinePlugin({ diff --git a/packages/connect-web/package.json b/packages/connect-web/package.json index fb194105987..a9e9c68b70a 100644 --- a/packages/connect-web/package.json +++ b/packages/connect-web/package.json @@ -45,7 +45,8 @@ "dependencies": { "@trezor/connect": "workspace:*", "@trezor/connect-common": "workspace:*", - "@trezor/utils": "workspace:*" + "@trezor/utils": "workspace:*", + "crypto-browserify": "^3.12.0" }, "devDependencies": { "@babel/preset-typescript": "^7.24.7", diff --git a/packages/connect-web/webpack/dev.webpack.config.ts b/packages/connect-web/webpack/dev.webpack.config.ts index febebc2b2a3..e7729f73d78 100644 --- a/packages/connect-web/webpack/dev.webpack.config.ts +++ b/packages/connect-web/webpack/dev.webpack.config.ts @@ -24,6 +24,13 @@ const dev = { libraryTarget: 'umd', libraryExport: 'default', }, + resolve: { + fallback: { + // Polyfills crypto API for NodeJS libraries in the browser. 'crypto' does not run without 'stream' + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + }, + }, plugins: [ // connect-web dev needs to be served from https // to allow injection in 3rd party builds using trezor-connect-src param diff --git a/packages/connect-web/webpack/prod.webpack.config.ts b/packages/connect-web/webpack/prod.webpack.config.ts index fbbc088265f..5efb28dd580 100644 --- a/packages/connect-web/webpack/prod.webpack.config.ts +++ b/packages/connect-web/webpack/prod.webpack.config.ts @@ -49,6 +49,10 @@ const config: webpack.Configuration = { mainFields: ['browser', 'module', 'main'], extensions: ['.ts', '.js'], fallback: { + // Polyfills crypto API for Node.js libraries in the browser. 'crypto' does not run without 'stream' + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + fs: false, // ignore "fs" import in markdown-it-imsize path: false, // ignore "path" import in markdown-it-imsize }, diff --git a/packages/connect-webextension/package.json b/packages/connect-webextension/package.json index 1bd1ccd2fd7..f3e04960a3b 100644 --- a/packages/connect-webextension/package.json +++ b/packages/connect-webextension/package.json @@ -42,6 +42,7 @@ "@trezor/connect-common": "workspace:*", "@trezor/connect-web": "workspace:*", "@trezor/utils": "workspace:*", + "crypto-browserify": "^3.12.0", "events": "^3.3.0" }, "devDependencies": { diff --git a/packages/connect-webextension/webpack/prod.webpack.config.ts b/packages/connect-webextension/webpack/prod.webpack.config.ts index eed17adc2fd..b2bd3a8e16b 100644 --- a/packages/connect-webextension/webpack/prod.webpack.config.ts +++ b/packages/connect-webextension/webpack/prod.webpack.config.ts @@ -38,6 +38,11 @@ const config: webpack.Configuration = { modules: ['node_modules'], mainFields: ['browser', 'module', 'main'], extensions: ['.ts', '.js'], + fallback: { + // Polyfills crypto API for Node.js libraries in the browser. 'crypto' does not run without 'stream' + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + }, }, performance: { hints: false, diff --git a/packages/suite-build/configs/base.webpack.config.ts b/packages/suite-build/configs/base.webpack.config.ts index d66451a8db9..e9ea077c94b 100644 --- a/packages/suite-build/configs/base.webpack.config.ts +++ b/packages/suite-build/configs/base.webpack.config.ts @@ -44,7 +44,7 @@ const config: webpack.Configuration = { src: path.resolve(__dirname, '../../suite/src/'), }, fallback: { - // Polyfills crypto API for NodeJS libraries in the browser. 'crypto' does not run without 'stream' + // Polyfills crypto API for Node.js libraries in the browser. 'crypto' does not run without 'stream' crypto: require.resolve('crypto-browserify'), stream: require.resolve('stream-browserify'), // Not required diff --git a/packages/suite/src/views/wallet/transactions/TransactionList/NoSearchResults.tsx b/packages/suite/src/views/wallet/transactions/TransactionList/NoSearchResults.tsx index e3b3b586d11..a1479ead448 100644 --- a/packages/suite/src/views/wallet/transactions/TransactionList/NoSearchResults.tsx +++ b/packages/suite/src/views/wallet/transactions/TransactionList/NoSearchResults.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import styled from 'styled-components'; import { Card, Column, variables } from '@trezor/components'; import { Translation } from 'src/components/suite'; -import { getRandomNumberInRange } from '@trezor/utils'; +import { getWeakRandomNumberInRange } from '@trezor/utils'; import { typography } from '@trezor/theme'; const NoResults = styled.div` @@ -46,7 +46,7 @@ const getTip = (num: number) => { }; export const NoSearchResults = () => { - const [tip] = useState(getRandomNumberInRange(1, 10)); + const [tip] = useState(getWeakRandomNumberInRange(1, 10)); return ( diff --git a/packages/transport-bridge/package.json b/packages/transport-bridge/package.json index 4fdf17f2483..4182acde07c 100644 --- a/packages/transport-bridge/package.json +++ b/packages/transport-bridge/package.json @@ -33,6 +33,7 @@ "@trezor/theme": "workspace:*", "@trezor/transport": "workspace:*", "@trezor/utils": "workspace:*", + "crypto-browserify": "^3.12.0", "json-stable-stringify": "^1.1.1", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/packages/transport-bridge/webpack/ui.webpack.config.ts b/packages/transport-bridge/webpack/ui.webpack.config.ts index 95ed619a4de..155a3b2ee47 100644 --- a/packages/transport-bridge/webpack/ui.webpack.config.ts +++ b/packages/transport-bridge/webpack/ui.webpack.config.ts @@ -58,6 +58,11 @@ const config: webpack.Configuration = { extensions: ['.ts', '.tsx', '.js', '.jsx'], modules: ['node_modules'], mainFields: ['browser', 'module', 'main'], + fallback: { + // Polyfills crypto API for Node.js libraries in the browser. 'crypto' does not run without 'stream' + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + }, }, plugins: [ new HtmlWebpackPlugin({ diff --git a/packages/transport-test/package.json b/packages/transport-test/package.json index cb3acba0e56..652797647b7 100644 --- a/packages/transport-test/package.json +++ b/packages/transport-test/package.json @@ -14,7 +14,7 @@ "test:e2e:new-bridge:hw": "USE_HW=true USE_NODE_BRIDGE=true yarn test:e2e:bridge", "test:e2e:new-bridge:emu": "USE_HW=false USE_NODE_BRIDGE=true yarn test:e2e:bridge", "build:e2e:api:node": "yarn esbuild ./e2e/api/api.test.ts --bundle --outfile=./e2e/dist/api.test.node.js --platform=node --target=node18 --external:usb", - "build:e2e:api:browser": "yarn esbuild ./e2e/api/api.test.ts --bundle --outfile=./e2e/dist/api.test.browser.js --platform=browser --external:usb && cp e2e/ui/api.test.html e2e/dist/index.html", + "build:e2e:api:browser": "yarn esbuild ./e2e/api/api.test.ts --bundle --alias:crypto=crypto-browserify --alias:stream=stream-browserify --outfile=./e2e/dist/api.test.browser.js --platform=browser --external:usb && cp e2e/ui/api.test.html e2e/dist/index.html", "test:e2e:api:node:hw": "yarn build:e2e:api:node && node ./e2e/dist/api.test.node.js", "test:e2e:api:browser:hw": "yarn build:e2e:api:browser && npx http-serve ./e2e/dist" }, @@ -25,6 +25,7 @@ "@trezor/trezor-user-env-link": "workspace:^", "@trezor/utils": "workspace:*", "buffer": "^6.0.3", + "crypto-browserify": "^3.12.0", "esbuild": "^0.23.1", "jest": "^29.7.0", "jest-extended": "^4.0.2", diff --git a/packages/utils/src/arrayShuffle.ts b/packages/utils/src/arrayShuffle.ts index 00ccb64e34a..06e6f410771 100644 --- a/packages/utils/src/arrayShuffle.ts +++ b/packages/utils/src/arrayShuffle.ts @@ -1,3 +1,5 @@ +import { getRandomInt } from './getRandomInt'; + /** * Implementation of the Fisher-Yates shuffle algorithm. * The algorithm produces an unbiased permutation: every permutation is equally likely. @@ -8,7 +10,7 @@ export const arrayShuffle = (array: readonly T[]): T[] => { const shuffled = array.slice(); for (let i = shuffled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); + const j = getRandomInt(0, i); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } diff --git a/packages/utils/src/getRandomInt.ts b/packages/utils/src/getRandomInt.ts new file mode 100644 index 00000000000..be60ca72ece --- /dev/null +++ b/packages/utils/src/getRandomInt.ts @@ -0,0 +1,17 @@ +import { randomBytes } from 'crypto'; + +/** + * Crypto.randomInt() function is not implemented by polyfill 'crypto-browserify' + * @see https://github.com/browserify/crypto-browserify/issues/224 + */ +export const getRandomInt = (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}`, + ); + } + + const randomValue = parseInt(randomBytes(4).toString('hex'), 16); + + return min + (randomValue % (max - min)); +}; diff --git a/packages/utils/src/getRandomNumberInRange.ts b/packages/utils/src/getRandomNumberInRange.ts deleted file mode 100644 index 045e0789027..00000000000 --- a/packages/utils/src/getRandomNumberInRange.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const getRandomNumberInRange = (min: number, max: number) => - Math.floor(Math.random() * (max - min + 1)) + min; diff --git a/packages/utils/src/getWeakRandomNumberInRange.ts b/packages/utils/src/getWeakRandomNumberInRange.ts new file mode 100644 index 00000000000..43e5d479135 --- /dev/null +++ b/packages/utils/src/getWeakRandomNumberInRange.ts @@ -0,0 +1,5 @@ +/** + * @deprecated Consider using `getRandomInt` which is cryptographically secure. + */ +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 d1b93bc1c1a..34655421a00 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -19,7 +19,7 @@ export * from './createTimeoutPromise'; export * from './getLocaleSeparators'; export * from './getMutex'; export * from './getNumberFromPixelString'; -export * from './getRandomNumberInRange'; +export * from './getWeakRandomNumberInRange'; export * from './getSynchronize'; export * from './getWeakRandomId'; export * from './hasUppercaseLetter'; @@ -40,6 +40,7 @@ export * from './topologicalSort'; export * from './truncateMiddle'; export * from './typedEventEmitter'; export * from './urlToOnion'; +export * from './getRandomInt'; export * from './logs'; export * from './logsManager'; export * from './bigNumber'; diff --git a/packages/utils/tests/getRandomInt.test.ts b/packages/utils/tests/getRandomInt.test.ts new file mode 100644 index 00000000000..e282bb806d5 --- /dev/null +++ b/packages/utils/tests/getRandomInt.test.ts @@ -0,0 +1,30 @@ +import { randomInt } from 'crypto'; + +import { getRandomInt } from '../src'; + +describe(getRandomInt.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(() => randomInt(0, -1)).toThrowError(EXPECTED_ERROR); + expect(() => getRandomInt(0, -1)).toThrowError(EXPECTED_ERROR); + }); + + it('returns same value when range is trivial', () => { + expect(randomInt(0, 1)).toEqual(0); + expect(getRandomInt(0, 1)).toEqual(0); + + expect(randomInt(100, 101)).toEqual(100); + expect(getRandomInt(100, 101)).toEqual(100); + }); + + it('returns same value when range is trivial', () => { + for (let i = 0; i < 10_000; i++) { + const result = getRandomInt(0, 100); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThan(100); + } + }); +}); diff --git a/yarn.lock b/yarn.lock index edcce42cec7..d3123e62175 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11422,6 +11422,7 @@ __metadata: "@types/react": "npm:18.2.79" babel-loader: "npm:^9.1.3" copy-webpack-plugin: "npm:^12.0.2" + crypto-browserify: "npm:^3.12.0" eth-phishing-detect: "npm:^1.2.0" html-webpack-plugin: "npm:^5.6.0" react-dom: "npm:18.2.0" @@ -11484,6 +11485,7 @@ __metadata: "@types/chrome": "npm:^0.0.270" "@types/w3c-web-usb": "npm:^1.0.10" babel-loader: "npm:^9.1.3" + crypto-browserify: "npm:^3.12.0" html-webpack-plugin: "npm:^5.6.0" rimraf: "npm:^6.0.1" selfsigned: "npm:^2.4.1" @@ -11514,6 +11516,7 @@ __metadata: "@types/chrome": "npm:^0.0.270" babel-loader: "npm:^9.1.3" copy-webpack-plugin: "npm:^12.0.2" + crypto-browserify: "npm:^3.12.0" events: "npm:^3.3.0" rimraf: "npm:^6.0.1" terser-webpack-plugin: "npm:^5.3.9" @@ -12166,6 +12169,7 @@ __metadata: "@trezor/transport": "workspace:*" "@trezor/utils": "workspace:*" "@types/json-stable-stringify": "npm:^1" + crypto-browserify: "npm:^3.12.0" esbuild: "npm:^0.23.1" html-webpack-plugin: "npm:^5.6.0" json-stable-stringify: "npm:^1.1.1" @@ -12198,6 +12202,7 @@ __metadata: "@trezor/trezor-user-env-link": "workspace:^" "@trezor/utils": "workspace:*" buffer: "npm:^6.0.3" + crypto-browserify: "npm:^3.12.0" esbuild: "npm:^0.23.1" jest: "npm:^29.7.0" jest-extended: "npm:^4.0.2"