Skip to content

Commit c409861

Browse files
Support asynchronous generation (#27)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent abd3c1e commit c409861

File tree

5 files changed

+83
-14
lines changed

5 files changed

+83
-14
lines changed

index.d.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,36 @@ declare namespace cryptoRandomString {
6060
type Options = BaseOptions & MergeExclusive<TypeOption, CharactersOption>;
6161
}
6262

63-
/**
64-
Generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.
63+
declare const cryptoRandomString: {
64+
/**
65+
Generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.
66+
67+
@returns A randomized string.
68+
69+
@example
70+
```
71+
import cryptoRandomString = require('crypto-random-string');
72+
73+
cryptoRandomString({length: 10});
74+
//=> '2cf05d94db'
75+
```
76+
*/
77+
(options?: cryptoRandomString.Options): string;
78+
79+
/**
80+
Asynchronously generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.
6581
66-
@returns A randomized string.
82+
@returns A promise which resolves to a randomized string.
6783
68-
@example
69-
```
70-
import cryptoRandomString = require('crypto-random-string');
84+
@example
85+
```
86+
import cryptoRandomString = require('crypto-random-string');
7187
72-
cryptoRandomString({length: 10});
73-
//=> '2cf05d94db'
74-
```
75-
*/
76-
declare function cryptoRandomString(options?: cryptoRandomString.Options): string;
88+
await cryptoRandomString.async({length: 10});
89+
//=> '2cf05d94db'
90+
```
91+
*/
92+
async(options?: cryptoRandomString.Options): Promise<string>;
93+
}
7794

7895
export = cryptoRandomString;

index.js

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
'use strict';
2+
const {promisify} = require('util');
23
const crypto = require('crypto');
34

5+
const randomBytesAsync = promisify(crypto.randomBytes);
6+
47
const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split('');
58
const numericCharacters = '0123456789'.split('');
69
const distinguishableCharacters = 'CDEHKMPRTUWXY012458'.split('');
@@ -32,6 +35,40 @@ const generateForCustomCharacters = (length, characters) => {
3235
return string;
3336
};
3437

38+
const generateForCustomCharactersAsync = async (length, characters) => {
39+
// Generating entropy is faster than complex math operations, so we use the simplest way
40+
const characterCount = characters.length;
41+
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
42+
const entropyLength = 2 * Math.ceil(1.1 * length); // Generating a bit more than required so chances we need more than one pass will be really low
43+
let string = '';
44+
let stringLength = 0;
45+
46+
while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
47+
const entropy = await randomBytesAsync(entropyLength); // eslint-disable-line no-await-in-loop
48+
let entropyPosition = 0;
49+
50+
while (entropyPosition < entropyLength && stringLength < length) {
51+
const entropyValue = entropy.readUInt16LE(entropyPosition);
52+
entropyPosition += 2;
53+
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
54+
continue;
55+
}
56+
57+
string += characters[entropyValue % characterCount];
58+
stringLength++;
59+
}
60+
}
61+
62+
return string;
63+
};
64+
65+
const generateRandomBytes = (byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length);
66+
67+
const generateRandomBytesAsync = async (byteLength, type, length) => {
68+
const buffer = await randomBytesAsync(byteLength);
69+
return buffer.toString(type).slice(0, length);
70+
};
71+
3572
const allowedTypes = [
3673
undefined,
3774
'hex',
@@ -41,7 +78,7 @@ const allowedTypes = [
4178
'distinguishable'
4279
];
4380

44-
module.exports = ({length, type, characters}) => {
81+
const createGenerator = (generateForCustomCharacters, generateRandomBytes) => ({length, type, characters}) => {
4582
if (!(length >= 0 && Number.isFinite(length))) {
4683
throw new TypeError('Expected a `length` to be a non-negative finite number');
4784
}
@@ -63,11 +100,11 @@ module.exports = ({length, type, characters}) => {
63100
}
64101

65102
if (type === 'hex' || (type === undefined && characters === undefined)) {
66-
return crypto.randomBytes(Math.ceil(length * 0.5)).toString('hex').slice(0, length); // Need 0.5 byte entropy per character
103+
return generateRandomBytes(Math.ceil(length * 0.5), 'hex', length); // Need 0.5 byte entropy per character
67104
}
68105

69106
if (type === 'base64') {
70-
return crypto.randomBytes(Math.ceil(length * 0.75)).toString('base64').slice(0, length); // Need 0.75 byte of entropy per character
107+
return generateRandomBytes(Math.ceil(length * 0.75), 'base64', length); // Need 0.75 byte of entropy per character
71108
}
72109

73110
if (type === 'url-safe') {
@@ -92,3 +129,6 @@ module.exports = ({length, type, characters}) => {
92129

93130
return generateForCustomCharacters(length, characters.split(''));
94131
};
132+
133+
module.exports = createGenerator(generateForCustomCharacters, generateRandomBytes);
134+
module.exports.async = createGenerator(generateForCustomCharactersAsync, generateRandomBytesAsync);

index.test-d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ expectType<string>(cryptoRandomString({length: 10}));
55
expectType<string>(cryptoRandomString({length: 10, type: 'url-safe'}));
66
expectType<string>(cryptoRandomString({length: 10, type: 'numeric'}));
77
expectType<string>(cryptoRandomString({length: 10, characters: '1234'}));
8+
expectType<Promise<string>>(cryptoRandomString.async({length: 10}));
89

910
expectError(cryptoRandomString({type: 'url-safe'}));
1011
expectError(cryptoRandomString({length: 10, type: 'url-safe', characters: '1234'}));

readme.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ cryptoRandomString({length: 10, characters: 'abc'});
4040

4141
Returns a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default.
4242

43+
### cryptoRandomString.async(options)
44+
45+
Returns a promise which resolves to a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default.
46+
4347
#### options
4448

4549
Type: `object`

test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ test('main', t => {
2222
t.is(generatedCharacterSetSize({}, 16), 16);
2323
});
2424

25+
test('async', async t => {
26+
t.is((await cryptoRandomString.async({length: 0})).length, 0);
27+
t.is((await cryptoRandomString.async({length: 10})).length, 10);
28+
t.is((await cryptoRandomString.async({length: 100})).length, 100);
29+
t.regex(await cryptoRandomString.async({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic
30+
});
31+
2532
test('hex', t => {
2633
t.is(cryptoRandomString({length: 0, type: 'hex'}).length, 0);
2734
t.is(cryptoRandomString({length: 10, type: 'hex'}).length, 10);

0 commit comments

Comments
 (0)