From c7e3d0b59a31fa48aa5c5bfb61d87f4ff0da5143 Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Sun, 2 Aug 2020 08:08:39 -0400 Subject: [PATCH 01/11] crypto: add randomInt function --- doc/api/crypto.md | 40 ++++++++ lib/crypto.js | 4 +- lib/internal/crypto/random.js | 72 +++++++++++++- test/parallel/test-crypto-random.js | 144 ++++++++++++++++++++++++++++ 4 files changed, 258 insertions(+), 2 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 54eb7a8c2984c5..739d79498a92e4 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2800,6 +2800,46 @@ threadpool request. To minimize threadpool task length variation, partition large `randomFill` requests when doing so as part of fulfilling a client request. +### `crypto.randomInt([min, ]max[, callback])` + + +* `min` {integer} Start of random range. **Default**: `0`. +* `max` {integer} End of random range (non-inclusive). +* `callback` {Function} `function(err, n) {}`. + +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`. + +If the `callback` function is not provided, the random integer is generated +synchronously. + +```js +// Asynchronous +crypto.randomInt(3, (err, n) => { + if (err) throw err; + console.log(`Random number chosen from (0, 1, 2): ${n}`); +}); +``` + +```js +// Synchronous +const n = crypto.randomInt(3); +console.log(`Random number chosen from (0, 1, 2): ${n}`); +``` + +```js +crypto.randomInt(1, 7, (err, n) => { + if (err) throw err; + console.log(`The dice rolled: ${n}`); +}); +``` + ### `crypto.scrypt(password, salt, keylen[, options], 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) => { From c83e6a9119d3cea0275ccc347028ccb992e211da Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Thu, 6 Aug 2020 15:03:01 -0400 Subject: [PATCH 03/11] Update lib/internal/crypto/random.js Co-authored-by: Anna Henningsen --- lib/internal/crypto/random.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index f45740e9eab996..6a6542a04a6463 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -152,7 +152,7 @@ function randomInt(min, max, cb) { const range = max - min + 1; if (!(range <= RAND_MAX)) { - throw new ERR_OUT_OF_RANGE('max - min', `<= ${RAND_MAX}`, range); + throw new ERR_OUT_OF_RANGE('max - min', `< ${RAND_MAX}`, range); } const excess = RAND_MAX % range; From e746f6c4aa6148ba365c29ec748234bfd776a777 Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Thu, 6 Aug 2020 15:07:42 -0400 Subject: [PATCH 04/11] Update lib/internal/crypto/random.js Co-authored-by: Anna Henningsen --- lib/internal/crypto/random.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index 6a6542a04a6463..443be45686d033 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -144,8 +144,8 @@ function randomInt(min, max, cb) { 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); + if (!(max >= min)) { + throw new ERR_OUT_OF_RANGE('max', `>= ${min}`, max); } // First we generate a random int between [0..range] From f8e9397b265fc0e3929f3192873216903ea045fb Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Thu, 6 Aug 2020 15:08:11 -0400 Subject: [PATCH 05/11] Update doc/api/crypto.md Co-authored-by: Anna Henningsen --- doc/api/crypto.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 38ddc2f12184ca..b873c9caf02fd4 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2803,7 +2803,7 @@ request. ### `crypto.randomInt([min, ]max[, callback])` * `min` {integer} Start of random range (inclusive). **Default**: `0`. From d6df4b92263357c38c70d7699675f63205f65ce5 Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Thu, 6 Aug 2020 15:52:41 -0400 Subject: [PATCH 06/11] lint(crypto): fix js lint for randomInt --- test/parallel/test-crypto-random.js | 30 +++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/test/parallel/test-crypto-random.js b/test/parallel/test-crypto-random.js index 10aeab0198fa2d..c44071af83ca81 100644 --- a/test/parallel/test-crypto-random.js +++ b/test/parallel/test-crypto-random.js @@ -434,19 +434,25 @@ assert.throws( 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(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(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()), { From 2cb022f93aa83ecfe177a4fc4498a7cbb754b3c6 Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Fri, 7 Aug 2020 00:13:35 -0400 Subject: [PATCH 07/11] Update doc/api/crypto.md Co-authored-by: James M Snell --- doc/api/crypto.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index b873c9caf02fd4..67e34e21bb92da 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2802,8 +2802,7 @@ request. ### `crypto.randomInt([min, ]max[, callback])` * `min` {integer} Start of random range (inclusive). **Default**: `0`. From ca5d07b857fe422aebb9065033cdda6c2c6f58ab Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Thu, 27 Aug 2020 01:52:52 -0400 Subject: [PATCH 08/11] crypto(randomInt): make both min and max params required --- doc/api/crypto.md | 8 ++++---- lib/internal/crypto/random.js | 11 ++--------- test/parallel/test-crypto-random.js | 27 ++++++++++----------------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 67e34e21bb92da..7d4a559c72925c 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2800,12 +2800,12 @@ threadpool request. To minimize threadpool task length variation, partition large `randomFill` requests when doing so as part of fulfilling a client request. -### `crypto.randomInt([min, ]max[, callback])` +### `crypto.randomInt(min, max[, callback])` -* `min` {integer} Start of random range (inclusive). **Default**: `0`. +* `min` {integer} Start of random range (inclusive). * `max` {integer} End of random range (inclusive). * `callback` {Function} `function(err, n) {}`. @@ -2821,7 +2821,7 @@ synchronously. ```js // Asynchronous -crypto.randomInt(3, (err, n) => { +crypto.randomInt(0, 3, (err, n) => { if (err) throw err; console.log(`Random number chosen from (0, 1, 2, 3): ${n}`); }); @@ -2829,7 +2829,7 @@ crypto.randomInt(3, (err, n) => { ```js // Synchronous -const n = crypto.randomInt(3); +const n = crypto.randomInt(0, 3); console.log(`Random number chosen from (0, 1, 2, 3): ${n}`); ``` diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index 443be45686d033..07f70e40d1cdc6 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -127,13 +127,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) - if (typeof max === 'function' || typeof max === 'undefined') { - cb = max; - max = min; - min = 0; - } const isSync = typeof cb === 'undefined'; if (!isSync && typeof cb !== 'function') { throw new ERR_INVALID_CALLBACK(cb); @@ -144,8 +137,8 @@ function randomInt(min, max, cb) { 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); + if (!(max > min)) { + throw new ERR_OUT_OF_RANGE('max', `> ${min}`, max); } // First we generate a random int between [0..range] diff --git a/test/parallel/test-crypto-random.js b/test/parallel/test-crypto-random.js index c44071af83ca81..a068067a0c6b36 100644 --- a/test/parallel/test-crypto-random.js +++ b/test/parallel/test-crypto-random.js @@ -356,7 +356,7 @@ assert.throws( { const randomInts = []; for (let i = 0; i < 100; i++) { - crypto.randomInt(3, common.mustCall((err, n) => { + crypto.randomInt(0, 3, common.mustCall((err, n) => { assert.ifError(err); assert.ok(n >= 0); assert.ok(n <= 3); @@ -376,7 +376,7 @@ assert.throws( // Synchronous API const randomInts = []; for (let i = 0; i < 100; i++) { - const n = crypto.randomInt(3); + const n = crypto.randomInt(0, 3); assert.ok(n >= 0); assert.ok(n <= 3); randomInts.push(n); @@ -419,13 +419,13 @@ assert.throws( 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 safe integer.' + - `${common.invalidArgTypeHelper(i)}`, - }); + assert.throws(() => crypto.randomInt(3, common.mustNotCall()), { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "max" argument must be safe integer.' + + `${common.invalidArgTypeHelper(common.mustNotCall())}`, }); const minInt = Number.MIN_SAFE_INTEGER; @@ -454,7 +454,6 @@ assert.throws( } ); - assert.throws(() => crypto.randomInt(0, 0, common.mustNotCall()), { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', @@ -467,17 +466,11 @@ assert.throws( message: 'The value of "max" is out of range. It must be > 0. Received -1' }); - assert.throws(() => crypto.randomInt(0, common.mustNotCall()), { - code: 'ERR_OUT_OF_RANGE', - name: 'RangeError', - message: 'The value of "max" is out of range. It must be > 0. Received 0' - }); - - assert.throws(() => crypto.randomInt(2 ** 48, common.mustNotCall()), { + assert.throws(() => crypto.randomInt(0, 2 ** 48, common.mustNotCall()), { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', message: 'The value of "max - min" is out of range. ' + - `It must be <= ${(2 ** 48) - 1}. ` + + `It must be < ${(2 ** 48) - 1}. ` + 'Received 281_474_976_710_657' }); From b7a183885053bf6e3473edd3f675f8c1381d4820 Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Mon, 31 Aug 2020 11:27:06 -0400 Subject: [PATCH 09/11] crypto(randomInt): max exclusive, remove min arg --- doc/api/crypto.md | 27 +++---- lib/internal/crypto/random.js | 39 ++++++--- test/parallel/test-crypto-random.js | 120 +++++----------------------- 3 files changed, 61 insertions(+), 125 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 7d4a559c72925c..72333cc803e776 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2800,43 +2800,40 @@ threadpool request. To minimize threadpool task length variation, partition large `randomFill` requests when doing so as part of fulfilling a client request. -### `crypto.randomInt(min, max[, callback])` +### `crypto.randomInt(max[, callback])` -* `min` {integer} Start of random range (inclusive). -* `max` {integer} End of random range (inclusive). +* `max` {integer} End of random range `[0, max)` (`max` is exclusive). + `max` must be less than `2^48`. * `callback` {Function} `function(err, n) {}`. -Return a random integer `n` such that `min <= n <= max`. This +Return a random integer `n` such that `0 <= n < max`. This implementation avoids [modulo bias](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Modulo_bias). -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. +If the `callback` function is not provided, the random integer is +generated synchronously. ```js // Asynchronous -crypto.randomInt(0, 3, (err, n) => { +crypto.randomInt(3, (err, n) => { if (err) throw err; - console.log(`Random number chosen from (0, 1, 2, 3): ${n}`); + console.log(`Random number chosen from (0, 1, 2): ${n}`); }); ``` ```js // Synchronous -const n = crypto.randomInt(0, 3); -console.log(`Random number chosen from (0, 1, 2, 3): ${n}`); +const n = crypto.randomInt(3); +console.log(`Random number chosen from (0, 1, 2): ${n}`); ``` ```js -crypto.randomInt(1, 6, (err, n) => { +crypto.randomInt(6, (err, n) => { if (err) throw err; - console.log(`The dice rolled: ${n}`); + console.log(`The dice rolled: ${n + 1}`); }); ``` diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index 07f70e40d1cdc6..ce92e18af0e116 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -122,11 +122,23 @@ function randomFill(buf, offset, size, cb) { // Largest integer we can read from a buffer. // e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6); -const RAND_MAX = 281474976710655; +const RAND_MAX = 0xFFFF_FFFF_FFFF; + +// Generates an integer in [min, max) range where min is inclusive and max is +// exclusive. +function randomIntRange(min, max, cb) { + // Detect optional min syntax + // randomIntRange(max) + // randomIntRange(max, cb) + const minNotSpecified = typeof max === 'undefined' || + typeof max === 'function'; + + if (minNotSpecified) { + cb = max; + max = min; + min = 0; + } -// Generates an integer in [min, max] range where both min and max are -// inclusive. -function randomInt(min, max, cb) { const isSync = typeof cb === 'undefined'; if (!isSync && typeof cb !== 'function') { throw new ERR_INVALID_CALLBACK(cb); @@ -137,15 +149,16 @@ function randomInt(min, max, cb) { 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); + 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 + 1; + // First we generate a random int between [0..range) + const range = max - min; if (!(range <= RAND_MAX)) { - throw new ERR_OUT_OF_RANGE('max - min', `< ${RAND_MAX}`, range); + throw new ERR_OUT_OF_RANGE(`max${minNotSpecified ? '' : ' - min'}`, + `<= ${RAND_MAX}`, range); } const excess = RAND_MAX % range; @@ -183,6 +196,14 @@ function randomInt(min, max, cb) { } } +// Introduce optional "min" argument eventually? +function randomInt(max, cb) { + if (typeof cb !== 'undefined' && typeof cb !== 'function') { + throw new ERR_INVALID_CALLBACK(cb); + } + return randomIntRange(max, cb); +} + function handleError(ex, buf) { if (ex) throw ex; return buf; diff --git a/test/parallel/test-crypto-random.js b/test/parallel/test-crypto-random.js index a068067a0c6b36..ea3098b4810b2a 100644 --- a/test/parallel/test-crypto-random.js +++ b/test/parallel/test-crypto-random.js @@ -318,56 +318,20 @@ assert.throws( { + // Asynchronous API const randomInts = []; for (let i = 0; i < 100; i++) { - crypto.randomInt(1, 3, common.mustCall((err, n) => { - assert.ifError(err); - assert.ok(n >= 1); - assert.ok(n <= 3); - randomInts.push(n); - if (randomInts.length === 100) { - 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)); - } - })); - } -} -{ - const randomInts = []; - for (let i = 0; i < 100; i++) { - crypto.randomInt(-10, -8, common.mustCall((err, n) => { - assert.ifError(err); - assert.ok(n >= -10); - assert.ok(n <= -8); - randomInts.push(n); - if (randomInts.length === 100) { - assert.ok(!randomInts.includes(-11)); - assert.ok(randomInts.includes(-10)); - assert.ok(randomInts.includes(-9)); - assert.ok(randomInts.includes(-8)); - assert.ok(!randomInts.includes(-7)); - } - })); - } -} -{ - const randomInts = []; - for (let i = 0; i < 100; i++) { - crypto.randomInt(0, 3, common.mustCall((err, n) => { + 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)); + assert.ok(!randomInts.includes(3)); } })); } @@ -376,9 +340,9 @@ assert.throws( // Synchronous API const randomInts = []; for (let i = 0; i < 100; i++) { - const n = crypto.randomInt(0, 3); + const n = crypto.randomInt(3); assert.ok(n >= 0); - assert.ok(n <= 3); + assert.ok(n < 3); randomInts.push(n); } @@ -386,34 +350,12 @@ assert.throws( 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, 5); - assert.ok(n >= 3); - assert.ok(n <= 5); - randomInts.push(n); - } - - assert.ok(randomInts.includes(3)); - assert.ok(randomInts.includes(4)); - assert.ok(randomInts.includes(5)); + assert.ok(!randomInts.includes(3)); } { ['10', true, NaN, null, {}, []].forEach((i) => { - assert.throws(() => crypto.randomInt(i, 100, common.mustNotCall()), { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: 'The "min" argument must be safe integer.' + - `${common.invalidArgTypeHelper(i)}`, - }); - - assert.throws(() => crypto.randomInt(0, i, common.mustNotCall()), { + assert.throws(() => crypto.randomInt(i, common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', message: 'The "max" argument must be safe integer.' + @@ -421,31 +363,10 @@ assert.throws( }); }); - assert.throws(() => crypto.randomInt(3, common.mustNotCall()), { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: 'The "max" argument must be safe integer.' + - `${common.invalidArgTypeHelper(common.mustNotCall())}`, - }); - - 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()), + () => crypto.randomInt(maxInt + 1, common.mustNotCall()), { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', @@ -454,29 +375,26 @@ assert.throws( } ); - assert.throws(() => crypto.randomInt(0, 0, common.mustNotCall()), { - code: 'ERR_OUT_OF_RANGE', - name: 'RangeError', - message: 'The value of "max" is out of range. It must be > 0. Received 0' - }); - - assert.throws(() => crypto.randomInt(0, -1, common.mustNotCall()), { + crypto.randomInt(0, common.mustCall()); + assert.throws(() => crypto.randomInt(-1, common.mustNotCall()), { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', - message: 'The value of "max" is out of range. It must be > 0. Received -1' + message: 'The value of "max" is out of range. It must be >= 0. Received -1' }); - assert.throws(() => crypto.randomInt(0, 2 ** 48, common.mustNotCall()), { + const MAX_RANGE = 0xFFFF_FFFF_FFFF; + crypto.randomInt(MAX_RANGE, common.mustCall()); + assert.throws(() => crypto.randomInt(MAX_RANGE + 1, common.mustNotCall()), { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', - message: 'The value of "max - min" is out of range. ' + - `It must be < ${(2 ** 48) - 1}. ` + - 'Received 281_474_976_710_657' + message: 'The value of "max" is out of range. ' + + `It must be <= ${MAX_RANGE}. ` + + 'Received 281_474_976_710_656' }); [1, true, NaN, null, {}, []].forEach((i) => { assert.throws( - () => crypto.randomInt(1, 2, i), + () => crypto.randomInt(1, i), { code: 'ERR_INVALID_CALLBACK', name: 'TypeError', From b220b3cc98e71869e47f8dd4ec92c70487f0e3d0 Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Mon, 31 Aug 2020 13:00:57 -0400 Subject: [PATCH 10/11] crypto(randomInt): put back optional min argument --- doc/api/crypto.md | 18 +++-- lib/internal/crypto/random.js | 14 +--- test/parallel/test-crypto-random.js | 114 +++++++++++++++++++++++++--- 3 files changed, 116 insertions(+), 30 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 72333cc803e776..1f949e1cc906df 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2800,19 +2800,22 @@ threadpool request. To minimize threadpool task length variation, partition large `randomFill` requests when doing so as part of fulfilling a client request. -### `crypto.randomInt(max[, callback])` +### `crypto.randomInt([min, ]max[, callback])` -* `max` {integer} End of random range `[0, max)` (`max` is exclusive). - `max` must be less than `2^48`. +* `min` {integer} Start of random range (inclusive). **Default**: `0`. +* `max` {integer} End of random range (exclusive). * `callback` {Function} `function(err, n) {}`. -Return a random integer `n` such that `0 <= 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 range (`max - min`) must be less than `2^48`. `min` and `max` must +be safe integers. + If the `callback` function is not provided, the random integer is generated synchronously. @@ -2831,10 +2834,9 @@ console.log(`Random number chosen from (0, 1, 2): ${n}`); ``` ```js -crypto.randomInt(6, (err, n) => { - if (err) throw err; - console.log(`The dice rolled: ${n + 1}`); -}); +// With `min` argument +const n = crypto.randomInt(1, 7); +console.log(`The dice rolled: ${n}`); ``` ### `crypto.scrypt(password, salt, keylen[, options], callback)` diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index ce92e18af0e116..561586472fd835 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -126,10 +126,10 @@ const RAND_MAX = 0xFFFF_FFFF_FFFF; // Generates an integer in [min, max) range where min is inclusive and max is // exclusive. -function randomIntRange(min, max, cb) { +function randomInt(min, max, cb) { // Detect optional min syntax - // randomIntRange(max) - // randomIntRange(max, cb) + // randomInt(max) + // randomInt(max, cb) const minNotSpecified = typeof max === 'undefined' || typeof max === 'function'; @@ -196,14 +196,6 @@ function randomIntRange(min, max, cb) { } } -// Introduce optional "min" argument eventually? -function randomInt(max, cb) { - if (typeof cb !== 'undefined' && typeof cb !== 'function') { - throw new ERR_INVALID_CALLBACK(cb); - } - return randomIntRange(max, cb); -} - function handleError(ex, buf) { if (ex) throw ex; return buf; diff --git a/test/parallel/test-crypto-random.js b/test/parallel/test-crypto-random.js index ea3098b4810b2a..67b63c42b52814 100644 --- a/test/parallel/test-crypto-random.js +++ b/test/parallel/test-crypto-random.js @@ -352,18 +352,99 @@ assert.throws( assert.ok(randomInts.includes(2)); assert.ok(!randomInts.includes(3)); } +{ + // Positive range + const randomInts = []; + for (let i = 0; i < 100; i++) { + crypto.randomInt(1, 3, common.mustCall((err, n) => { + assert.ifError(err); + assert.ok(n >= 1); + assert.ok(n < 3); + randomInts.push(n); + if (randomInts.length === 100) { + assert.ok(!randomInts.includes(0)); + assert.ok(randomInts.includes(1)); + assert.ok(randomInts.includes(2)); + assert.ok(!randomInts.includes(3)); + } + })); + } +} +{ + // Negative range + const randomInts = []; + for (let i = 0; i < 100; i++) { + crypto.randomInt(-10, -8, common.mustCall((err, n) => { + assert.ifError(err); + assert.ok(n >= -10); + assert.ok(n < -8); + randomInts.push(n); + if (randomInts.length === 100) { + assert.ok(!randomInts.includes(-11)); + assert.ok(randomInts.includes(-10)); + assert.ok(randomInts.includes(-9)); + assert.ok(!randomInts.includes(-8)); + } + })); + } +} { ['10', true, NaN, null, {}, []].forEach((i) => { - assert.throws(() => crypto.randomInt(i, common.mustNotCall()), { + const invalidMinError = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "min" argument must be safe integer.' + + `${common.invalidArgTypeHelper(i)}`, + }; + const invalidMaxError = { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError', message: 'The "max" argument must be safe integer.' + `${common.invalidArgTypeHelper(i)}`, - }); + }; + + assert.throws( + () => crypto.randomInt(i, 100), + invalidMinError + ); + assert.throws( + () => crypto.randomInt(i, 100, common.mustNotCall()), + invalidMinError + ); + assert.throws( + () => crypto.randomInt(i), + invalidMaxError + ); + assert.throws( + () => crypto.randomInt(i, common.mustNotCall()), + invalidMaxError + ); + assert.throws( + () => crypto.randomInt(0, i, common.mustNotCall()), + invalidMaxError + ); + assert.throws( + () => crypto.randomInt(0, i), + invalidMaxError + ); }); const maxInt = Number.MAX_SAFE_INTEGER; + const minInt = Number.MIN_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 + 1, common.mustNotCall()), @@ -376,6 +457,7 @@ assert.throws( ); crypto.randomInt(0, common.mustCall()); + crypto.randomInt(0, 0, common.mustCall()); assert.throws(() => crypto.randomInt(-1, common.mustNotCall()), { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', @@ -384,6 +466,18 @@ assert.throws( const MAX_RANGE = 0xFFFF_FFFF_FFFF; crypto.randomInt(MAX_RANGE, common.mustCall()); + crypto.randomInt(1, MAX_RANGE + 1, common.mustCall()); + assert.throws( + () => crypto.randomInt(1, MAX_RANGE + 2, common.mustNotCall()), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: 'The value of "max - min" is out of range. ' + + `It must be <= ${MAX_RANGE}. ` + + 'Received 281_474_976_710_656' + } + ); + assert.throws(() => crypto.randomInt(MAX_RANGE + 1, common.mustNotCall()), { code: 'ERR_OUT_OF_RANGE', name: 'RangeError', @@ -392,14 +486,12 @@ assert.throws( 'Received 281_474_976_710_656' }); - [1, true, NaN, null, {}, []].forEach((i) => { - assert.throws( - () => crypto.randomInt(1, i), - { - code: 'ERR_INVALID_CALLBACK', - name: 'TypeError', - message: `Callback must be a function. Received ${inspect(i)}` - } - ); + [true, NaN, null, {}, [], 10].forEach((i) => { + const cbError = { + code: 'ERR_INVALID_CALLBACK', + name: 'TypeError', + message: `Callback must be a function. Received ${inspect(i)}` + }; + assert.throws(() => crypto.randomInt(0, 1, i), cbError); }); } From e62fe80d849100e98c0b716815659cfed456181c Mon Sep 17 00:00:00 2001 From: Oli Lalonde Date: Wed, 2 Sep 2020 10:05:17 -0400 Subject: [PATCH 11/11] docs: use reference style link in markdown --- doc/api/crypto.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 1f949e1cc906df..ae4adf130f1c0d 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -2810,8 +2810,7 @@ added: REPLACEME * `callback` {Function} `function(err, n) {}`. 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). +implementation avoids [modulo bias][]. The range (`max - min`) must be less than `2^48`. `min` and `max` must be safe integers. @@ -3586,6 +3585,7 @@ See the [list of SSL OP Flags][] for details. [NIST SP 800-131A]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar1.pdf [NIST SP 800-132]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf +[modulo bias]: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Modulo_bias [Nonce-Disrespecting Adversaries]: https://github.com/nonce-disrespect/nonce-disrespect [OpenSSL's SPKAC implementation]: https://www.openssl.org/docs/man1.1.0/apps/openssl-spkac.html [RFC 1421]: https://www.rfc-editor.org/rfc/rfc1421.txt