mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-02 21:45:14 +01:00
feat: implement strong randomInt that works in browser without need for polyfill lib
This commit is contained in:
committed by
Peter Sanderson
parent
560dd755d0
commit
3f4312e834
@@ -38,5 +38,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.1.2"
|
||||
},
|
||||
"browser": {
|
||||
"crypto": false
|
||||
}
|
||||
}
|
||||
|
||||
58
packages/utils/src/getRandomInt.ts
Normal file
58
packages/utils/src/getRandomInt.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { getRandomValues as cryptoGetRandomValues } from 'crypto';
|
||||
|
||||
/**
|
||||
* Before changing anything here, see the Modulo Bias problem!
|
||||
* @see https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
|
||||
*
|
||||
* @param min Inclusive
|
||||
* @param max Exclusive (to match the crypto.randomInt() function API
|
||||
*/
|
||||
export const getRandomInt = (min: number, max: number) => {
|
||||
if (!Number.isSafeInteger(min)) {
|
||||
throw new RangeError(
|
||||
`The "min" argument must be a safe integer. Received type ${typeof min} (${min})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isSafeInteger(max)) {
|
||||
throw new RangeError(
|
||||
`The "max" argument must be a safe integer. Received type ${typeof max} (${max})`,
|
||||
);
|
||||
}
|
||||
|
||||
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 MAX_RANGE_32_BITS = 0xffffffff + 1;
|
||||
const range = max - min;
|
||||
|
||||
if (range > MAX_RANGE_32_BITS) {
|
||||
throw new RangeError(
|
||||
`This function only provide 32 bits of entropy, therefore range cannot be more then 2^32.`,
|
||||
);
|
||||
}
|
||||
|
||||
const getRandomValues =
|
||||
typeof window !== 'undefined'
|
||||
? (array: Uint32Array) => window.crypto.getRandomValues(array)
|
||||
: (array: Uint32Array) => cryptoGetRandomValues(array);
|
||||
|
||||
const array = new Uint32Array(1); // This provides 32 bits of entropy.
|
||||
|
||||
// It is crucial to avoid modulo bias.
|
||||
// See: https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
|
||||
|
||||
// We calculate the maximum possible value that can be evenly distributed across the desired range.
|
||||
const maxRange = MAX_RANGE_32_BITS - (MAX_RANGE_32_BITS % range);
|
||||
|
||||
let randomValue: number;
|
||||
do {
|
||||
getRandomValues(array);
|
||||
randomValue = array[0];
|
||||
} while (randomValue >= maxRange);
|
||||
|
||||
return min + (randomValue % range);
|
||||
};
|
||||
@@ -21,6 +21,7 @@ export * from './getMutex';
|
||||
export * from './getNumberFromPixelString';
|
||||
export * from './getWeakRandomNumberInRange';
|
||||
export * from './getSynchronize';
|
||||
export * from './getRandomInt';
|
||||
export * from './getWeakRandomId';
|
||||
export * from './getWeakRandomInt';
|
||||
export * from './hasUppercaseLetter';
|
||||
|
||||
80
packages/utils/tests/getRandomInt.test.ts
Normal file
80
packages/utils/tests/getRandomInt.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { randomInt } from 'crypto';
|
||||
|
||||
import { getRandomInt } from '../src/getRandomInt';
|
||||
|
||||
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('raises error for range > 2^32', () => {
|
||||
const EXPECTED_ERROR = new RangeError(
|
||||
'This function only provide 32 bits of entropy, therefore range cannot be more then 2^32.',
|
||||
);
|
||||
|
||||
expect(() => getRandomInt(0, Math.pow(2, 32) + 1)).toThrowError(EXPECTED_ERROR);
|
||||
});
|
||||
|
||||
it('raises same error for unsafe integer', () => {
|
||||
const UNSAFE_INTEGER = 2 ** 53;
|
||||
|
||||
const EXPECTED_ERROR_MIN = new RangeError(
|
||||
'The "min" argument must be a safe integer. Received type number (9007199254740992)',
|
||||
);
|
||||
|
||||
expect(() => randomInt(UNSAFE_INTEGER, UNSAFE_INTEGER + 1)).toThrowError(
|
||||
EXPECTED_ERROR_MIN,
|
||||
);
|
||||
expect(() => getRandomInt(UNSAFE_INTEGER, UNSAFE_INTEGER + 1)).toThrowError(
|
||||
EXPECTED_ERROR_MIN,
|
||||
);
|
||||
|
||||
const EXPECTED_ERROR_MAX = new RangeError(
|
||||
'The "max" argument must be a safe integer. Received type number (9007199254740992)',
|
||||
);
|
||||
const EXPECTED_ERROR_MAX_NEGATIVE = new RangeError(
|
||||
'The "max" argument must be a safe integer. Received type number (-9007199254740992)',
|
||||
);
|
||||
|
||||
expect(() => randomInt(0, UNSAFE_INTEGER)).toThrowError(EXPECTED_ERROR_MAX);
|
||||
expect(() => getRandomInt(0, UNSAFE_INTEGER)).toThrowError(EXPECTED_ERROR_MAX);
|
||||
expect(() => randomInt(0, -UNSAFE_INTEGER)).toThrowError(EXPECTED_ERROR_MAX_NEGATIVE);
|
||||
expect(() => getRandomInt(0, -UNSAFE_INTEGER)).toThrowError(EXPECTED_ERROR_MAX_NEGATIVE);
|
||||
});
|
||||
|
||||
// This test takes 100+seconds to run. It is very needed for development and debugging,
|
||||
// but due to the time it takes, it is skipped in CI.
|
||||
it.skip('return value in given range and in uniform distribution', () => {
|
||||
const SAMPLES = 1000_000;
|
||||
const RANGE = 100;
|
||||
const MIN = 0;
|
||||
const MAX = MIN + RANGE;
|
||||
const TOLERANCE = 0.1;
|
||||
const EXPECTED = SAMPLES / RANGE;
|
||||
|
||||
const LOWER_BOUND = (1 - TOLERANCE) * EXPECTED;
|
||||
const UPPER_BOUND = (1 + TOLERANCE) * EXPECTED;
|
||||
|
||||
const distribution = new Map<number, number>();
|
||||
|
||||
for (let i = 0; i < SAMPLES; i++) {
|
||||
const result = getRandomInt(MIN, MAX);
|
||||
|
||||
distribution.set(result, (distribution.get(result) ?? 0) + 1);
|
||||
|
||||
expect(Number.isInteger(result)).toBe(true);
|
||||
expect(result).toBeGreaterThanOrEqual(MIN);
|
||||
expect(result).toBeLessThan(MAX);
|
||||
}
|
||||
|
||||
Array.from(distribution.keys()).forEach(key => {
|
||||
expect(distribution.get(key)).toBeGreaterThanOrEqual(LOWER_BOUND);
|
||||
expect(distribution.get(key)).toBeLessThanOrEqual(UPPER_BOUND);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user