Skip to content

Commit 44d89a9

Browse files
olalonderichardlau
authored andcommitted
crypto: add randomInt function
PR-URL: #34600 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Tobias Nießen <[email protected]>
1 parent 1bf5d1a commit 44d89a9

File tree

4 files changed

+301
-2
lines changed

4 files changed

+301
-2
lines changed

doc/api/crypto.md

+39
Original file line numberDiff line numberDiff line change
@@ -2793,6 +2793,44 @@ threadpool request. To minimize threadpool task length variation, partition
27932793
large `randomFill` requests when doing so as part of fulfilling a client
27942794
request.
27952795

2796+
### `crypto.randomInt([min, ]max[, callback])`
2797+
<!-- YAML
2798+
added: REPLACEME
2799+
-->
2800+
2801+
* `min` {integer} Start of random range (inclusive). **Default**: `0`.
2802+
* `max` {integer} End of random range (exclusive).
2803+
* `callback` {Function} `function(err, n) {}`.
2804+
2805+
Return a random integer `n` such that `min <= n < max`. This
2806+
implementation avoids [modulo bias][].
2807+
2808+
The range (`max - min`) must be less than `2^48`. `min` and `max` must
2809+
be safe integers.
2810+
2811+
If the `callback` function is not provided, the random integer is
2812+
generated synchronously.
2813+
2814+
```js
2815+
// Asynchronous
2816+
crypto.randomInt(3, (err, n) => {
2817+
if (err) throw err;
2818+
console.log(`Random number chosen from (0, 1, 2): ${n}`);
2819+
});
2820+
```
2821+
2822+
```js
2823+
// Synchronous
2824+
const n = crypto.randomInt(3);
2825+
console.log(`Random number chosen from (0, 1, 2): ${n}`);
2826+
```
2827+
2828+
```js
2829+
// With `min` argument
2830+
const n = crypto.randomInt(1, 7);
2831+
console.log(`The dice rolled: ${n}`);
2832+
```
2833+
27962834
### `crypto.scrypt(password, salt, keylen[, options], callback)`
27972835
<!-- YAML
27982836
added: v10.5.0
@@ -3565,6 +3603,7 @@ See the [list of SSL OP Flags][] for details.
35653603
[NIST SP 800-131A]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar1.pdf
35663604
[NIST SP 800-132]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf
35673605
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
3606+
[modulo bias]: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Modulo_bias
35683607
[Nonce-Disrespecting Adversaries]: https://github.com/nonce-disrespect/nonce-disrespect
35693608
[OpenSSL's SPKAC implementation]: https://www.openssl.org/docs/man1.1.0/apps/openssl-spkac.html
35703609
[RFC 1421]: https://www.rfc-editor.org/rfc/rfc1421.txt

lib/crypto.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ const {
5252
const {
5353
randomBytes,
5454
randomFill,
55-
randomFillSync
55+
randomFillSync,
56+
randomInt
5657
} = require('internal/crypto/random');
5758
const {
5859
pbkdf2,
@@ -184,6 +185,7 @@ module.exports = {
184185
randomBytes,
185186
randomFill,
186187
randomFillSync,
188+
randomInt,
187189
scrypt,
188190
scryptSync,
189191
sign: signOneShot,

lib/internal/crypto/random.js

+79-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const {
44
MathMin,
55
NumberIsNaN,
6+
NumberIsSafeInteger
67
} = primordials;
78

89
const { AsyncWrap, Providers } = internalBinding('async_wrap');
@@ -119,6 +120,82 @@ function randomFill(buf, offset, size, cb) {
119120
_randomBytes(buf, offset, size, wrap);
120121
}
121122

123+
// Largest integer we can read from a buffer.
124+
// e.g.: Buffer.from("ff".repeat(6), "hex").readUIntBE(0, 6);
125+
const RAND_MAX = 0xFFFF_FFFF_FFFF;
126+
127+
// Generates an integer in [min, max) range where min is inclusive and max is
128+
// exclusive.
129+
function randomInt(min, max, cb) {
130+
// Detect optional min syntax
131+
// randomInt(max)
132+
// randomInt(max, cb)
133+
const minNotSpecified = typeof max === 'undefined' ||
134+
typeof max === 'function';
135+
136+
if (minNotSpecified) {
137+
cb = max;
138+
max = min;
139+
min = 0;
140+
}
141+
142+
const isSync = typeof cb === 'undefined';
143+
if (!isSync && typeof cb !== 'function') {
144+
throw new ERR_INVALID_CALLBACK(cb);
145+
}
146+
if (!NumberIsSafeInteger(min)) {
147+
throw new ERR_INVALID_ARG_TYPE('min', 'safe integer', min);
148+
}
149+
if (!NumberIsSafeInteger(max)) {
150+
throw new ERR_INVALID_ARG_TYPE('max', 'safe integer', max);
151+
}
152+
if (!(max >= min)) {
153+
throw new ERR_OUT_OF_RANGE('max', `>= ${min}`, max);
154+
}
155+
156+
// First we generate a random int between [0..range)
157+
const range = max - min;
158+
159+
if (!(range <= RAND_MAX)) {
160+
throw new ERR_OUT_OF_RANGE(`max${minNotSpecified ? '' : ' - min'}`,
161+
`<= ${RAND_MAX}`, range);
162+
}
163+
164+
const excess = RAND_MAX % range;
165+
const randLimit = RAND_MAX - excess;
166+
167+
if (isSync) {
168+
// Sync API
169+
while (true) {
170+
const x = randomBytes(6).readUIntBE(0, 6);
171+
// If x > (maxVal - (maxVal % range)), we will get "modulo bias"
172+
if (x > randLimit) {
173+
// Try again
174+
continue;
175+
}
176+
const n = (x % range) + min;
177+
return n;
178+
}
179+
} else {
180+
// Async API
181+
const pickAttempt = () => {
182+
randomBytes(6, (err, bytes) => {
183+
if (err) return cb(err);
184+
const x = bytes.readUIntBE(0, 6);
185+
// If x > (maxVal - (maxVal % range)), we will get "modulo bias"
186+
if (x > randLimit) {
187+
// Try again
188+
return pickAttempt();
189+
}
190+
const n = (x % range) + min;
191+
cb(null, n);
192+
});
193+
};
194+
195+
pickAttempt();
196+
}
197+
}
198+
122199
function handleError(ex, buf) {
123200
if (ex) throw ex;
124201
return buf;
@@ -127,5 +204,6 @@ function handleError(ex, buf) {
127204
module.exports = {
128205
randomBytes,
129206
randomFill,
130-
randomFillSync
207+
randomFillSync,
208+
randomInt
131209
};

test/parallel/test-crypto-random.js

+180
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,183 @@ assert.throws(
315315
assert.strictEqual(desc.writable, true);
316316
assert.strictEqual(desc.enumerable, false);
317317
});
318+
319+
320+
{
321+
// Asynchronous API
322+
const randomInts = [];
323+
for (let i = 0; i < 100; i++) {
324+
crypto.randomInt(3, common.mustCall((err, n) => {
325+
assert.ifError(err);
326+
assert.ok(n >= 0);
327+
assert.ok(n < 3);
328+
randomInts.push(n);
329+
if (randomInts.length === 100) {
330+
assert.ok(!randomInts.includes(-1));
331+
assert.ok(randomInts.includes(0));
332+
assert.ok(randomInts.includes(1));
333+
assert.ok(randomInts.includes(2));
334+
assert.ok(!randomInts.includes(3));
335+
}
336+
}));
337+
}
338+
}
339+
{
340+
// Synchronous API
341+
const randomInts = [];
342+
for (let i = 0; i < 100; i++) {
343+
const n = crypto.randomInt(3);
344+
assert.ok(n >= 0);
345+
assert.ok(n < 3);
346+
randomInts.push(n);
347+
}
348+
349+
assert.ok(!randomInts.includes(-1));
350+
assert.ok(randomInts.includes(0));
351+
assert.ok(randomInts.includes(1));
352+
assert.ok(randomInts.includes(2));
353+
assert.ok(!randomInts.includes(3));
354+
}
355+
{
356+
// Positive range
357+
const randomInts = [];
358+
for (let i = 0; i < 100; i++) {
359+
crypto.randomInt(1, 3, common.mustCall((err, n) => {
360+
assert.ifError(err);
361+
assert.ok(n >= 1);
362+
assert.ok(n < 3);
363+
randomInts.push(n);
364+
if (randomInts.length === 100) {
365+
assert.ok(!randomInts.includes(0));
366+
assert.ok(randomInts.includes(1));
367+
assert.ok(randomInts.includes(2));
368+
assert.ok(!randomInts.includes(3));
369+
}
370+
}));
371+
}
372+
}
373+
{
374+
// Negative range
375+
const randomInts = [];
376+
for (let i = 0; i < 100; i++) {
377+
crypto.randomInt(-10, -8, common.mustCall((err, n) => {
378+
assert.ifError(err);
379+
assert.ok(n >= -10);
380+
assert.ok(n < -8);
381+
randomInts.push(n);
382+
if (randomInts.length === 100) {
383+
assert.ok(!randomInts.includes(-11));
384+
assert.ok(randomInts.includes(-10));
385+
assert.ok(randomInts.includes(-9));
386+
assert.ok(!randomInts.includes(-8));
387+
}
388+
}));
389+
}
390+
}
391+
{
392+
393+
['10', true, NaN, null, {}, []].forEach((i) => {
394+
const invalidMinError = {
395+
code: 'ERR_INVALID_ARG_TYPE',
396+
name: 'TypeError',
397+
message: 'The "min" argument must be safe integer.' +
398+
`${common.invalidArgTypeHelper(i)}`,
399+
};
400+
const invalidMaxError = {
401+
code: 'ERR_INVALID_ARG_TYPE',
402+
name: 'TypeError',
403+
message: 'The "max" argument must be safe integer.' +
404+
`${common.invalidArgTypeHelper(i)}`,
405+
};
406+
407+
assert.throws(
408+
() => crypto.randomInt(i, 100),
409+
invalidMinError
410+
);
411+
assert.throws(
412+
() => crypto.randomInt(i, 100, common.mustNotCall()),
413+
invalidMinError
414+
);
415+
assert.throws(
416+
() => crypto.randomInt(i),
417+
invalidMaxError
418+
);
419+
assert.throws(
420+
() => crypto.randomInt(i, common.mustNotCall()),
421+
invalidMaxError
422+
);
423+
assert.throws(
424+
() => crypto.randomInt(0, i, common.mustNotCall()),
425+
invalidMaxError
426+
);
427+
assert.throws(
428+
() => crypto.randomInt(0, i),
429+
invalidMaxError
430+
);
431+
});
432+
433+
const maxInt = Number.MAX_SAFE_INTEGER;
434+
const minInt = Number.MIN_SAFE_INTEGER;
435+
436+
crypto.randomInt(minInt, minInt + 5, common.mustCall());
437+
crypto.randomInt(maxInt - 5, maxInt, common.mustCall());
438+
439+
assert.throws(
440+
() => crypto.randomInt(minInt - 1, minInt + 5, common.mustNotCall()),
441+
{
442+
code: 'ERR_INVALID_ARG_TYPE',
443+
name: 'TypeError',
444+
message: 'The "min" argument must be safe integer.' +
445+
`${common.invalidArgTypeHelper(minInt - 1)}`,
446+
}
447+
);
448+
449+
assert.throws(
450+
() => crypto.randomInt(maxInt + 1, common.mustNotCall()),
451+
{
452+
code: 'ERR_INVALID_ARG_TYPE',
453+
name: 'TypeError',
454+
message: 'The "max" argument must be safe integer.' +
455+
`${common.invalidArgTypeHelper(maxInt + 1)}`,
456+
}
457+
);
458+
459+
crypto.randomInt(0, common.mustCall());
460+
crypto.randomInt(0, 0, common.mustCall());
461+
assert.throws(() => crypto.randomInt(-1, common.mustNotCall()), {
462+
code: 'ERR_OUT_OF_RANGE',
463+
name: 'RangeError',
464+
message: 'The value of "max" is out of range. It must be >= 0. Received -1'
465+
});
466+
467+
const MAX_RANGE = 0xFFFF_FFFF_FFFF;
468+
crypto.randomInt(MAX_RANGE, common.mustCall());
469+
crypto.randomInt(1, MAX_RANGE + 1, common.mustCall());
470+
assert.throws(
471+
() => crypto.randomInt(1, MAX_RANGE + 2, common.mustNotCall()),
472+
{
473+
code: 'ERR_OUT_OF_RANGE',
474+
name: 'RangeError',
475+
message: 'The value of "max - min" is out of range. ' +
476+
`It must be <= ${MAX_RANGE}. ` +
477+
'Received 281_474_976_710_656'
478+
}
479+
);
480+
481+
assert.throws(() => crypto.randomInt(MAX_RANGE + 1, common.mustNotCall()), {
482+
code: 'ERR_OUT_OF_RANGE',
483+
name: 'RangeError',
484+
message: 'The value of "max" is out of range. ' +
485+
`It must be <= ${MAX_RANGE}. ` +
486+
'Received 281_474_976_710_656'
487+
});
488+
489+
[true, NaN, null, {}, [], 10].forEach((i) => {
490+
const cbError = {
491+
code: 'ERR_INVALID_CALLBACK',
492+
name: 'TypeError',
493+
message: `Callback must be a function. Received ${inspect(i)}`
494+
};
495+
assert.throws(() => crypto.randomInt(0, 1, i), cbError);
496+
});
497+
}

0 commit comments

Comments
 (0)