From c068dc453f02f635e7941b533b86656d2c59907a Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Thu, 6 Aug 2020 14:23:02 -0400 Subject: [PATCH] crypto(randomInt): make max arg inclusive --- doc/api/crypto.md | 17 +++++----- lib/internal/crypto/random.js | 16 +++++---- test/parallel/test-crypto-random.js | 51 ++++++++++++++++++++++------- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 739d79498a92e4..38ddc2f12184ca 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2803,18 +2803,19 @@ request. ### `crypto.randomInt([min, ]max[, callback])` -* `min` {integer} Start of random range. **Default**: `0`. -* `max` {integer} End of random range (non-inclusive). +* `min` {integer} Start of random range (inclusive). **Default**: `0`. +* `max` {integer} End of random range (inclusive). * `callback` {Function} `function(err, n) {}`. -Return a random integer `n` such that `min <= n < max`. This +Return a random integer `n` such that `min <= n <= max`. This implementation avoids [modulo bias](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Modulo_bias). -The maximum supported range value (`max - min`) is `2^48 - 1`. +The cardinality of the range (`max - min + 1`) must be at most `2^48 - +1`. `min` and `max` must be safe integers. If the `callback` function is not provided, the random integer is generated synchronously. @@ -2823,18 +2824,18 @@ synchronously. // Asynchronous crypto.randomInt(3, (err, n) => { if (err) throw err; - console.log(`Random number chosen from (0, 1, 2): ${n}`); + console.log(`Random number chosen from (0, 1, 2, 3): ${n}`); }); ``` ```js // Synchronous const n = crypto.randomInt(3); -console.log(`Random number chosen from (0, 1, 2): ${n}`); +console.log(`Random number chosen from (0, 1, 2, 3): ${n}`); ``` ```js -crypto.randomInt(1, 7, (err, n) => { +crypto.randomInt(1, 6, (err, n) => { if (err) throw err; console.log(`The dice rolled: ${n}`); }); diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index c9251dff46145d..f45740e9eab996 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -3,7 +3,7 @@ const { MathMin, NumberIsNaN, - NumberIsInteger + NumberIsSafeInteger } = primordials; const { AsyncWrap, Providers } = internalBinding('async_wrap'); @@ -124,6 +124,8 @@ function randomFill(buf, offset, size, cb) { // e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6); const RAND_MAX = 281474976710655; +// Generates an integer in [min, max] range where both min and max are +// inclusive. function randomInt(min, max, cb) { // randomInt(max, cb) // randomInt(max) @@ -136,18 +138,18 @@ function randomInt(min, max, cb) { if (!isSync && typeof cb !== 'function') { throw new ERR_INVALID_CALLBACK(cb); } - if (!NumberIsInteger(min)) { - throw new ERR_INVALID_ARG_TYPE('min', 'integer', min); + if (!NumberIsSafeInteger(min)) { + throw new ERR_INVALID_ARG_TYPE('min', 'safe integer', min); } - if (!NumberIsInteger(max)) { - throw new ERR_INVALID_ARG_TYPE('max', 'integer', max); + if (!NumberIsSafeInteger(max)) { + throw new ERR_INVALID_ARG_TYPE('max', 'safe integer', max); } if (!(max > min)) { throw new ERR_OUT_OF_RANGE('max', `> ${min}`, max); } - // First we generate a random int between [0..range) - const range = max - min; + // First we generate a random int between [0..range] + const range = max - min + 1; if (!(range <= RAND_MAX)) { throw new ERR_OUT_OF_RANGE('max - min', `<= ${RAND_MAX}`, range); diff --git a/test/parallel/test-crypto-random.js b/test/parallel/test-crypto-random.js index fc183f85b28af8..10aeab0198fa2d 100644 --- a/test/parallel/test-crypto-random.js +++ b/test/parallel/test-crypto-random.js @@ -320,10 +320,10 @@ assert.throws( { const randomInts = []; for (let i = 0; i < 100; i++) { - crypto.randomInt(1, 4, common.mustCall((err, n) => { + crypto.randomInt(1, 3, common.mustCall((err, n) => { assert.ifError(err); assert.ok(n >= 1); - assert.ok(n < 4); + assert.ok(n <= 3); randomInts.push(n); if (randomInts.length === 100) { assert.ok(!randomInts.includes(0)); @@ -338,10 +338,10 @@ assert.throws( { const randomInts = []; for (let i = 0; i < 100; i++) { - crypto.randomInt(-10, -7, common.mustCall((err, n) => { + crypto.randomInt(-10, -8, common.mustCall((err, n) => { assert.ifError(err); assert.ok(n >= -10); - assert.ok(n < -7); + assert.ok(n <= -8); randomInts.push(n); if (randomInts.length === 100) { assert.ok(!randomInts.includes(-11)); @@ -359,12 +359,15 @@ assert.throws( crypto.randomInt(3, common.mustCall((err, n) => { assert.ifError(err); assert.ok(n >= 0); - assert.ok(n < 3); + assert.ok(n <= 3); randomInts.push(n); if (randomInts.length === 100) { + assert.ok(!randomInts.includes(-1)); assert.ok(randomInts.includes(0)); assert.ok(randomInts.includes(1)); assert.ok(randomInts.includes(2)); + assert.ok(randomInts.includes(3)); + assert.ok(!randomInts.includes(4)); } })); } @@ -375,21 +378,24 @@ assert.throws( for (let i = 0; i < 100; i++) { const n = crypto.randomInt(3); assert.ok(n >= 0); - assert.ok(n < 3); + assert.ok(n <= 3); randomInts.push(n); } + assert.ok(!randomInts.includes(-1)); assert.ok(randomInts.includes(0)); assert.ok(randomInts.includes(1)); assert.ok(randomInts.includes(2)); + assert.ok(randomInts.includes(3)); + assert.ok(!randomInts.includes(4)); } { // Synchronous API with min const randomInts = []; for (let i = 0; i < 100; i++) { - const n = crypto.randomInt(3, 6); + const n = crypto.randomInt(3, 5); assert.ok(n >= 3); - assert.ok(n < 6); + assert.ok(n <= 5); randomInts.push(n); } @@ -403,25 +409,46 @@ assert.throws( assert.throws(() => crypto.randomInt(i, 100, common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "min" argument must be integer.' + + message: 'The "min" argument must be safe integer.' + `${common.invalidArgTypeHelper(i)}`, }); assert.throws(() => crypto.randomInt(0, i, common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "max" argument must be integer.' + + message: 'The "max" argument must be safe integer.' + `${common.invalidArgTypeHelper(i)}`, }); assert.throws(() => crypto.randomInt(i, common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', - message: 'The "max" argument must be integer.' + + message: 'The "max" argument must be safe integer.' + `${common.invalidArgTypeHelper(i)}`, }); }); + const minInt = Number.MIN_SAFE_INTEGER; + const maxInt = Number.MAX_SAFE_INTEGER; + + crypto.randomInt(minInt, minInt + 5, common.mustCall()); + crypto.randomInt(maxInt - 5, maxInt, common.mustCall()); + + assert.throws(() => crypto.randomInt(minInt - 1, minInt + 5, common.mustNotCall()), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "min" argument must be safe integer.' + + `${common.invalidArgTypeHelper(minInt - 1)}`, + }); + + assert.throws(() => crypto.randomInt(maxInt - 5, maxInt + 1, common.mustNotCall()), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "max" argument must be safe integer.' + + `${common.invalidArgTypeHelper(maxInt + 1)}`, + }); + + assert.throws(() => crypto.randomInt(0, 0, common.mustNotCall()), { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', @@ -445,7 +472,7 @@ assert.throws( name: 'RangeError', message: 'The value of "max - min" is out of range. ' + `It must be <= ${(2 ** 48) - 1}. ` + - 'Received 281_474_976_710_656' + 'Received 281_474_976_710_657' }); [1, true, NaN, null, {}, []].forEach((i) => {