Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 53c9975

Browse filesBrowse files
olalondeaddaleax
authored andcommitted
crypto: add randomInt function
PR-URL: #34600 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Tobias Nießen <tniessen@tnie.de>
1 parent 2c409a2 commit 53c9975
Copy full SHA for 53c9975

File tree

Expand file treeCollapse file tree

4 files changed

+301
-2
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

4 files changed

+301
-2
lines changed
Open diff view settings
Collapse file

‎doc/api/crypto.md‎

Copy file name to clipboardExpand all lines: doc/api/crypto.md
+39Lines changed: 39 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -2771,6 +2771,44 @@ threadpool request. To minimize threadpool task length variation, partition
27712771
large `randomFill` requests when doing so as part of fulfilling a client
27722772
request.
27732773

2774+
### `crypto.randomInt([min, ]max[, callback])`
2775+
<!-- YAML
2776+
added: REPLACEME
2777+
-->
2778+
2779+
* `min` {integer} Start of random range (inclusive). **Default**: `0`.
2780+
* `max` {integer} End of random range (exclusive).
2781+
* `callback` {Function} `function(err, n) {}`.
2782+
2783+
Return a random integer `n` such that `min <= n < max`. This
2784+
implementation avoids [modulo bias][].
2785+
2786+
The range (`max - min`) must be less than `2^48`. `min` and `max` must
2787+
be safe integers.
2788+
2789+
If the `callback` function is not provided, the random integer is
2790+
generated synchronously.
2791+
2792+
```js
2793+
// Asynchronous
2794+
crypto.randomInt(3, (err, n) => {
2795+
if (err) throw err;
2796+
console.log(`Random number chosen from (0, 1, 2): ${n}`);
2797+
});
2798+
```
2799+
2800+
```js
2801+
// Synchronous
2802+
const n = crypto.randomInt(3);
2803+
console.log(`Random number chosen from (0, 1, 2): ${n}`);
2804+
```
2805+
2806+
```js
2807+
// With `min` argument
2808+
const n = crypto.randomInt(1, 7);
2809+
console.log(`The dice rolled: ${n}`);
2810+
```
2811+
27742812
### `crypto.scrypt(password, salt, keylen[, options], callback)`
27752813
<!-- YAML
27762814
added: v10.5.0
@@ -3538,6 +3576,7 @@ See the [list of SSL OP Flags][] for details.
35383576
[NIST SP 800-131A]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar1.pdf
35393577
[NIST SP 800-132]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf
35403578
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
3579+
[modulo bias]: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Modulo_bias
35413580
[Nonce-Disrespecting Adversaries]: https://github.com/nonce-disrespect/nonce-disrespect
35423581
[OpenSSL's SPKAC implementation]: https://www.openssl.org/docs/man1.1.0/apps/openssl-spkac.html
35433582
[RFC 1421]: https://www.rfc-editor.org/rfc/rfc1421.txt
Collapse file

‎lib/crypto.js‎

Copy file name to clipboardExpand all lines: lib/crypto.js
+3-1Lines 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,
Collapse file

‎lib/internal/crypto/random.js‎

Copy file name to clipboardExpand all lines: lib/internal/crypto/random.js
+79-1Lines changed: 79 additions & 1 deletion
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
};
Collapse file

‎test/parallel/test-crypto-random.js‎

Copy file name to clipboardExpand all lines: test/parallel/test-crypto-random.js
+180Lines changed: 180 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.