Skip to content

Commit c7e3d0b

Browse files
committed
crypto: add randomInt function
1 parent da4d8de commit c7e3d0b

File tree

4 files changed

+258
-2
lines changed

4 files changed

+258
-2
lines changed

doc/api/crypto.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,6 +2800,46 @@ threadpool request. To minimize threadpool task length variation, partition
28002800
large `randomFill` requests when doing so as part of fulfilling a client
28012801
request.
28022802

2803+
### `crypto.randomInt([min, ]max[, callback])`
2804+
<!-- YAML
2805+
added:
2806+
- v14.8.0
2807+
-->
2808+
2809+
* `min` {integer} Start of random range. **Default**: `0`.
2810+
* `max` {integer} End of random range (non-inclusive).
2811+
* `callback` {Function} `function(err, n) {}`.
2812+
2813+
Return a random integer `n` such that `min <= n < max`. This
2814+
implementation avoids [modulo
2815+
bias](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Modulo_bias).
2816+
2817+
The maximum supported range value (`max - min`) is `2^48 - 1`.
2818+
2819+
If the `callback` function is not provided, the random integer is generated
2820+
synchronously.
2821+
2822+
```js
2823+
// Asynchronous
2824+
crypto.randomInt(3, (err, n) => {
2825+
if (err) throw err;
2826+
console.log(`Random number chosen from (0, 1, 2): ${n}`);
2827+
});
2828+
```
2829+
2830+
```js
2831+
// Synchronous
2832+
const n = crypto.randomInt(3);
2833+
console.log(`Random number chosen from (0, 1, 2): ${n}`);
2834+
```
2835+
2836+
```js
2837+
crypto.randomInt(1, 7, (err, n) => {
2838+
if (err) throw err;
2839+
console.log(`The dice rolled: ${n}`);
2840+
});
2841+
```
2842+
28032843
### `crypto.scrypt(password, salt, keylen[, options], callback)`
28042844
<!-- YAML
28052845
added: v10.5.0

lib/crypto.js

Lines changed: 3 additions & 1 deletion
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

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const {
44
MathMin,
55
NumberIsNaN,
6+
NumberIsInteger
67
} = primordials;
78

89
const { AsyncWrap, Providers } = internalBinding('async_wrap');
@@ -119,6 +120,74 @@ 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 = 281474976710655;
126+
127+
function randomInt(min, max, cb) {
128+
// randomInt(max, cb)
129+
// randomInt(max)
130+
if (typeof max === 'function' || typeof max === 'undefined') {
131+
cb = max;
132+
max = min;
133+
min = 0;
134+
}
135+
const isSync = typeof cb === 'undefined';
136+
if (!isSync && typeof cb !== 'function') {
137+
throw new ERR_INVALID_CALLBACK(cb);
138+
}
139+
if (!NumberIsInteger(min)) {
140+
throw new ERR_INVALID_ARG_TYPE('min', 'integer', min);
141+
}
142+
if (!NumberIsInteger(max)) {
143+
throw new ERR_INVALID_ARG_TYPE('max', 'integer', max);
144+
}
145+
if (!(max > min)) {
146+
throw new ERR_OUT_OF_RANGE('max', `> ${min}`, max);
147+
}
148+
149+
// First we generate a random int between [0..range)
150+
const range = max - min;
151+
152+
if (!(range <= RAND_MAX)) {
153+
throw new ERR_OUT_OF_RANGE('max - min', `<= ${RAND_MAX}`, range);
154+
}
155+
156+
const excess = RAND_MAX % range;
157+
const randLimit = RAND_MAX - excess;
158+
159+
if (isSync) {
160+
// Sync API
161+
while (true) {
162+
const x = randomBytes(6).readUIntBE(0, 6);
163+
// If x > (maxVal - (maxVal % range)), we will get "modulo bias"
164+
if (x > randLimit) {
165+
// Try again
166+
continue;
167+
}
168+
const n = (x % range) + min;
169+
return n;
170+
}
171+
} else {
172+
// Async API
173+
const pickAttempt = () => {
174+
randomBytes(6, (err, bytes) => {
175+
if (err) return cb(err);
176+
const x = bytes.readUIntBE(0, 6);
177+
// If x > (maxVal - (maxVal % range)), we will get "modulo bias"
178+
if (x > randLimit) {
179+
// Try again
180+
return pickAttempt();
181+
}
182+
const n = (x % range) + min;
183+
cb(null, n);
184+
});
185+
};
186+
187+
pickAttempt();
188+
}
189+
}
190+
122191
function handleError(ex, buf) {
123192
if (ex) throw ex;
124193
return buf;
@@ -127,5 +196,6 @@ function handleError(ex, buf) {
127196
module.exports = {
128197
randomBytes,
129198
randomFill,
130-
randomFillSync
199+
randomFillSync,
200+
randomInt
131201
};

test/parallel/test-crypto-random.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,147 @@ assert.throws(
315315
assert.strictEqual(desc.writable, true);
316316
assert.strictEqual(desc.enumerable, false);
317317
});
318+
319+
320+
{
321+
const randomInts = [];
322+
for (let i = 0; i < 100; i++) {
323+
crypto.randomInt(1, 4, common.mustCall((err, n) => {
324+
assert.ifError(err);
325+
assert.ok(n >= 1);
326+
assert.ok(n < 4);
327+
randomInts.push(n);
328+
if (randomInts.length === 100) {
329+
assert.ok(!randomInts.includes(0));
330+
assert.ok(randomInts.includes(1));
331+
assert.ok(randomInts.includes(2));
332+
assert.ok(randomInts.includes(3));
333+
assert.ok(!randomInts.includes(4));
334+
}
335+
}));
336+
}
337+
}
338+
{
339+
const randomInts = [];
340+
for (let i = 0; i < 100; i++) {
341+
crypto.randomInt(-10, -7, common.mustCall((err, n) => {
342+
assert.ifError(err);
343+
assert.ok(n >= -10);
344+
assert.ok(n < -7);
345+
randomInts.push(n);
346+
if (randomInts.length === 100) {
347+
assert.ok(!randomInts.includes(-11));
348+
assert.ok(randomInts.includes(-10));
349+
assert.ok(randomInts.includes(-9));
350+
assert.ok(randomInts.includes(-8));
351+
assert.ok(!randomInts.includes(-7));
352+
}
353+
}));
354+
}
355+
}
356+
{
357+
const randomInts = [];
358+
for (let i = 0; i < 100; i++) {
359+
crypto.randomInt(3, common.mustCall((err, n) => {
360+
assert.ifError(err);
361+
assert.ok(n >= 0);
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+
}
369+
}));
370+
}
371+
}
372+
{
373+
// Synchronous API
374+
const randomInts = [];
375+
for (let i = 0; i < 100; i++) {
376+
const n = crypto.randomInt(3);
377+
assert.ok(n >= 0);
378+
assert.ok(n < 3);
379+
randomInts.push(n);
380+
}
381+
382+
assert.ok(randomInts.includes(0));
383+
assert.ok(randomInts.includes(1));
384+
assert.ok(randomInts.includes(2));
385+
}
386+
{
387+
// Synchronous API with min
388+
const randomInts = [];
389+
for (let i = 0; i < 100; i++) {
390+
const n = crypto.randomInt(3, 6);
391+
assert.ok(n >= 3);
392+
assert.ok(n < 6);
393+
randomInts.push(n);
394+
}
395+
396+
assert.ok(randomInts.includes(3));
397+
assert.ok(randomInts.includes(4));
398+
assert.ok(randomInts.includes(5));
399+
}
400+
{
401+
402+
['10', true, NaN, null, {}, []].forEach((i) => {
403+
assert.throws(() => crypto.randomInt(i, 100, common.mustNotCall()), {
404+
code: 'ERR_INVALID_ARG_TYPE',
405+
name: 'TypeError',
406+
message: 'The "min" argument must be integer.' +
407+
`${common.invalidArgTypeHelper(i)}`,
408+
});
409+
410+
assert.throws(() => crypto.randomInt(0, i, common.mustNotCall()), {
411+
code: 'ERR_INVALID_ARG_TYPE',
412+
name: 'TypeError',
413+
message: 'The "max" argument must be integer.' +
414+
`${common.invalidArgTypeHelper(i)}`,
415+
});
416+
417+
assert.throws(() => crypto.randomInt(i, common.mustNotCall()), {
418+
code: 'ERR_INVALID_ARG_TYPE',
419+
name: 'TypeError',
420+
message: 'The "max" argument must be integer.' +
421+
`${common.invalidArgTypeHelper(i)}`,
422+
});
423+
});
424+
425+
assert.throws(() => crypto.randomInt(0, 0, common.mustNotCall()), {
426+
code: 'ERR_OUT_OF_RANGE',
427+
name: 'RangeError',
428+
message: 'The value of "max" is out of range. It must be > 0. Received 0'
429+
});
430+
431+
assert.throws(() => crypto.randomInt(0, -1, common.mustNotCall()), {
432+
code: 'ERR_OUT_OF_RANGE',
433+
name: 'RangeError',
434+
message: 'The value of "max" is out of range. It must be > 0. Received -1'
435+
});
436+
437+
assert.throws(() => crypto.randomInt(0, common.mustNotCall()), {
438+
code: 'ERR_OUT_OF_RANGE',
439+
name: 'RangeError',
440+
message: 'The value of "max" is out of range. It must be > 0. Received 0'
441+
});
442+
443+
assert.throws(() => crypto.randomInt(2 ** 48, common.mustNotCall()), {
444+
code: 'ERR_OUT_OF_RANGE',
445+
name: 'RangeError',
446+
message: 'The value of "max - min" is out of range. ' +
447+
`It must be <= ${(2 ** 48) - 1}. ` +
448+
'Received 281_474_976_710_656'
449+
});
450+
451+
[1, true, NaN, null, {}, []].forEach((i) => {
452+
assert.throws(
453+
() => crypto.randomInt(1, 2, i),
454+
{
455+
code: 'ERR_INVALID_CALLBACK',
456+
name: 'TypeError',
457+
message: `Callback must be a function. Received ${inspect(i)}`
458+
}
459+
);
460+
});
461+
}

0 commit comments

Comments
 (0)