Skip to content

Commit 51402ba

Browse files
Yanis Bensonsindresorhus
andcommitted
Add type and characters options (#4)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent d605421 commit 51402ba

File tree

6 files changed

+211
-7
lines changed

6 files changed

+211
-7
lines changed

index.d.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,54 @@
1+
import {MergeExclusive} from 'type-fest';
2+
3+
declare namespace cryptoRandomString {
4+
interface TypeOptions {
5+
/**
6+
Use only characters from a predefined set of allowed characters.
7+
8+
Cannot be set at the same time as the `characters` option.
9+
10+
@default 'hex'
11+
12+
@example
13+
```
14+
cryptoRandomString(10, {type:'hex'});
15+
//=> '87fc70e2b9'
16+
17+
cryptoRandomString(10, {type:'base64'});
18+
//=> 'mhsX7xmIv/'
19+
20+
cryptoRandomString(10, {type:'url-safe'});
21+
//=> 'VEjfNW3Yej'
22+
```
23+
*/
24+
type?: 'hex' | 'base64' | 'url-safe';
25+
}
26+
27+
interface CharactersOptions {
28+
/**
29+
Use only characters from a custom set of allowed characters.
30+
31+
Cannot be set at the same time as the `type` option.
32+
33+
Minimum length: `1`
34+
Maximum length: `65536`
35+
36+
@example
37+
```
38+
cryptoRandomString(10, {characters:'0123456789'});
39+
//=> '8796225811'
40+
```
41+
*/
42+
characters: string;
43+
}
44+
type Options = MergeExclusive<TypeOptions, CharactersOptions>;
45+
}
46+
147
/**
2-
Generate a [cryptographically strong](https://en.m.wikipedia.org/wiki/Strong_cryptography) random string.
48+
Generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.
349
450
@param length - Length of the returned string.
5-
@returns A [`hex`](https://en.wikipedia.org/wiki/Hexadecimal) string.
51+
@returns Returns a randomized string.
652
753
@example
854
```
@@ -12,6 +58,6 @@ cryptoRandomString(10);
1258
//=> '2cf05d94db'
1359
```
1460
*/
15-
declare function cryptoRandomString(length: number): string;
61+
declare function cryptoRandomString(length: number, options?: cryptoRandomString.Options): string;
1662

1763
export = cryptoRandomString;

index.js

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,80 @@
11
'use strict';
22
const crypto = require('crypto');
33

4-
module.exports = length => {
4+
const urlSafeChars = 'abcdefjhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split('');
5+
6+
const generateForCustomCharacters = (length, chars) => {
7+
// Generating entropy is faster than complex math operations, so we use the simplest way
8+
const characterCount = chars.length;
9+
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
10+
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
11+
let string = '';
12+
let stringLength = 0;
13+
14+
while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
15+
const entropy = crypto.randomBytes(entropyLength);
16+
let entropyPosition = 0;
17+
18+
while (entropyPosition < entropyLength && stringLength < length) {
19+
const entropyValue = entropy.readUInt16LE(entropyPosition);
20+
entropyPosition += 2;
21+
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
22+
continue;
23+
}
24+
25+
string += chars[entropyValue % characterCount];
26+
stringLength++;
27+
}
28+
}
29+
30+
return string;
31+
};
32+
33+
const allowedTypes = [undefined, 'hex', 'base64', 'url-safe'];
34+
35+
module.exports = (length, opts) => {
536
if (!Number.isFinite(length)) {
637
throw new TypeError('Expected a finite number');
738
}
839

9-
return crypto.randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length);
40+
let type = opts === undefined ? undefined : opts.type;
41+
const characters = opts === undefined ? undefined : opts.characters;
42+
43+
if (type !== undefined && characters !== undefined) {
44+
throw new TypeError('Expected either type or characters');
45+
}
46+
47+
if (characters !== undefined && typeof characters !== 'string') {
48+
throw new TypeError('Expected characters to be string');
49+
}
50+
51+
if (!allowedTypes.includes(type)) {
52+
throw new TypeError(`Unknown type: ${type}`);
53+
}
54+
55+
if (type === undefined && characters === undefined) {
56+
type = 'hex';
57+
}
58+
59+
if (type === 'hex' || (type === undefined && characters === undefined)) {
60+
return crypto.randomBytes(Math.ceil(length * 0.5)).toString('hex').slice(0, length); // Need 0.5 byte entropy per character
61+
}
62+
63+
if (type === 'base64') {
64+
return crypto.randomBytes(Math.ceil(length * 0.75)).toString('base64').slice(0, length); // Need 0.75 byte of entropy per character
65+
}
66+
67+
if (type === 'url-safe') {
68+
return generateForCustomCharacters(length, urlSafeChars);
69+
}
70+
71+
if (characters.length === 0) {
72+
throw new TypeError('Expected `characters` string length to be greater than or equal to 1');
73+
}
74+
75+
if (characters.length > 0x10000) {
76+
throw new TypeError('Expected `characters` string length to be less or equal to 65536');
77+
}
78+
79+
return generateForCustomCharacters(length, characters.split(''));
1080
};

index.test-d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ import {expectType} from 'tsd';
22
import cryptoRandomString = require('.');
33

44
expectType<string>(cryptoRandomString(10));
5+
expectType<string>(cryptoRandomString(10, {type: 'url-safe'}));
6+
expectType<string>(cryptoRandomString(10, {characters: '1234'}));

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
"secure",
3333
"hex"
3434
],
35+
"dependencies": {
36+
"type-fest": "^0.4.1"
37+
},
3538
"devDependencies": {
3639
"ava": "^1.4.1",
3740
"tsd": "^0.7.2",

readme.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,57 @@ const cryptoRandomString = require('crypto-random-string');
1919

2020
cryptoRandomString(10);
2121
//=> '2cf05d94db'
22+
23+
cryptoRandomString(10, {type: 'hex'});
24+
//=> 'c00f094c79'
25+
26+
cryptoRandomString(10, {type: 'base64'});
27+
//=> 'YMiMbaQl6I'
28+
29+
cryptoRandomString(10, {type: 'url-safe'});
30+
//=> 'YN-tqc8pOw'
31+
32+
cryptoRandomString(10, {characters: '1234567890'});
33+
//=> '1791935639'
2234
```
2335

2436

2537
## API
2638

27-
### cryptoRandomString(length)
39+
### cryptoRandomString(length, [options])
2840

29-
Returns a [`hex`](https://en.wikipedia.org/wiki/Hexadecimal) string.
41+
Returns a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default.
3042

3143
#### length
3244

3345
Type: `number`
3446

3547
Length of the returned string.
3648

49+
#### options
50+
51+
Type: `object`
52+
53+
##### type
54+
55+
Type: `string`<br>
56+
Default: `hex`<br>
57+
Values: `hex` `base64` `url-safe`
58+
59+
Use only characters from a predefined set of allowed characters.
60+
61+
Cannot be set at the same time as the `characters` option.
62+
63+
##### characters
64+
65+
Type: `string`<br>
66+
Minimum length: `1`<br>
67+
Maximum length: `65536`
68+
69+
Use only characters from a custom set of allowed characters.
70+
71+
Cannot be set at the same time as the `type` option.
72+
3773

3874
## Related
3975

test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,51 @@ test('main', t => {
55
t.is(cryptoRandomString(0).length, 0);
66
t.is(cryptoRandomString(10).length, 10);
77
t.is(cryptoRandomString(100).length, 100);
8+
t.regex(cryptoRandomString(100), /^[a-f\d]*$/); // Sanity check, probabilistic
9+
});
10+
11+
test('hex', t => {
12+
t.is(cryptoRandomString(0, {type: 'hex'}).length, 0);
13+
t.is(cryptoRandomString(10, {type: 'hex'}).length, 10);
14+
t.is(cryptoRandomString(100, {type: 'hex'}).length, 100);
15+
t.regex(cryptoRandomString(100, {type: 'hex'}), /^[a-f\d]*$/); // Sanity check, probabilistic
16+
});
17+
18+
test('base64', t => {
19+
t.is(cryptoRandomString(0, {type: 'base64'}).length, 0);
20+
t.is(cryptoRandomString(10, {type: 'base64'}).length, 10);
21+
t.is(cryptoRandomString(100, {type: 'base64'}).length, 100);
22+
t.regex(cryptoRandomString(100, {type: 'base64'}), /^[a-zA-Z\d/+]*$/); // Sanity check, probabilistic
23+
});
24+
25+
test('url-safe', t => {
26+
t.is(cryptoRandomString(0, {type: 'url-safe'}).length, 0);
27+
t.is(cryptoRandomString(10, {type: 'url-safe'}).length, 10);
28+
t.is(cryptoRandomString(100, {type: 'url-safe'}).length, 100);
29+
t.regex(cryptoRandomString(100, {type: 'url-safe'}), /^[a-zA-Z\d._~-]*$/); // Sanity check, probabilistic
30+
});
31+
32+
test('characters', t => {
33+
t.is(cryptoRandomString(0, {characters: '1234'}).length, 0);
34+
t.is(cryptoRandomString(10, {characters: '1234'}).length, 10);
35+
t.is(cryptoRandomString(100, {characters: '1234'}).length, 100);
36+
t.regex(cryptoRandomString(100, {characters: '1234'}), /^[1-4]*$/); // Sanity check, probabilistic
37+
});
38+
39+
test('argument errors', t => {
40+
t.throws(() => {
41+
cryptoRandomString(Infinity);
42+
});
43+
44+
t.throws(() => {
45+
cryptoRandomString(0, {type: 'hex', characters: '1234'});
46+
});
47+
48+
t.throws(() => {
49+
cryptoRandomString(0, {characters: 42});
50+
});
51+
52+
t.throws(() => {
53+
cryptoRandomString(0, {type: 'unknown'});
54+
});
855
});

0 commit comments

Comments
 (0)