Skip to content

Commit 62e78ab

Browse files
authoredApr 18, 2022
Add browser support (#43)
1 parent a93cea0 commit 62e78ab

File tree

5 files changed

+197
-144
lines changed

5 files changed

+197
-144
lines changed
 

‎browser.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* eslint-env browser */
2+
import {createStringGenerator, createAsyncStringGenerator} from './core.js';
3+
4+
const toHex = uInt8Array => uInt8Array.map(byte => byte.toString(16).padStart(2, '0')).join('');
5+
6+
const decoder = new TextDecoder('utf8');
7+
const toBase64 = uInt8Array => btoa(decoder.decode(uInt8Array));
8+
9+
// `crypto.getRandomValues` throws an error if too much entropy is requested at once. (https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#exceptions)
10+
const maxEntropy = 65536;
11+
12+
function getRandomValues(byteLength) {
13+
const generatedBytes = [];
14+
15+
while (byteLength > 0) {
16+
const bytesToGenerate = Math.min(byteLength, maxEntropy);
17+
generatedBytes.push(crypto.getRandomValues(new Uint8Array({length: bytesToGenerate})));
18+
byteLength -= bytesToGenerate;
19+
}
20+
21+
const result = new Uint8Array(generatedBytes.reduce((sum, {byteLength}) => sum + byteLength, 0)); // eslint-disable-line unicorn/no-array-reduce
22+
let currentIndex = 0;
23+
24+
for (const bytes of generatedBytes) {
25+
result.set(bytes, currentIndex);
26+
currentIndex += bytes.byteLength;
27+
}
28+
29+
return result;
30+
}
31+
32+
function specialRandomBytes(byteLength, type, length) {
33+
const generatedBytes = getRandomValues(byteLength);
34+
const convert = type === 'hex' ? toHex : toBase64;
35+
36+
return convert(generatedBytes).slice(0, length);
37+
}
38+
39+
const cryptoRandomString = createStringGenerator(specialRandomBytes, getRandomValues);
40+
cryptoRandomString.async = createAsyncStringGenerator(specialRandomBytes, getRandomValues);
41+
42+
export default cryptoRandomString;

‎core.js

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split('');
2+
const numericCharacters = '0123456789'.split('');
3+
const distinguishableCharacters = 'CDEHKMPRTUWXY012458'.split('');
4+
const asciiPrintableCharacters = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.split('');
5+
const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('');
6+
7+
const generateForCustomCharacters = (length, characters, randomBytes) => {
8+
// Generating entropy is faster than complex math operations, so we use the simplest way
9+
const characterCount = characters.length;
10+
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
11+
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
12+
let string = '';
13+
let stringLength = 0;
14+
15+
while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
16+
const entropy = randomBytes(entropyLength);
17+
let entropyPosition = 0;
18+
19+
while (entropyPosition < entropyLength && stringLength < length) {
20+
const entropyValue = entropy.readUInt16LE(entropyPosition);
21+
entropyPosition += 2;
22+
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
23+
continue;
24+
}
25+
26+
string += characters[entropyValue % characterCount];
27+
stringLength++;
28+
}
29+
}
30+
31+
return string;
32+
};
33+
34+
const generateForCustomCharactersAsync = async (length, characters, randomBytesAsync) => {
35+
// Generating entropy is faster than complex math operations, so we use the simplest way
36+
const characterCount = characters.length;
37+
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
38+
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
39+
let string = '';
40+
let stringLength = 0;
41+
42+
while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
43+
const entropy = await randomBytesAsync(entropyLength); // eslint-disable-line no-await-in-loop
44+
let entropyPosition = 0;
45+
46+
while (entropyPosition < entropyLength && stringLength < length) {
47+
const entropyValue = entropy.readUInt16LE(entropyPosition);
48+
entropyPosition += 2;
49+
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
50+
continue;
51+
}
52+
53+
string += characters[entropyValue % characterCount];
54+
stringLength++;
55+
}
56+
}
57+
58+
return string;
59+
};
60+
61+
const allowedTypes = new Set([
62+
undefined,
63+
'hex',
64+
'base64',
65+
'url-safe',
66+
'numeric',
67+
'distinguishable',
68+
'ascii-printable',
69+
'alphanumeric'
70+
]);
71+
72+
const createGenerator = (generateForCustomCharacters, specialRandomBytes, randomBytes) => ({length, type, characters}) => {
73+
if (!(length >= 0 && Number.isFinite(length))) {
74+
throw new TypeError('Expected a `length` to be a non-negative finite number');
75+
}
76+
77+
if (type !== undefined && characters !== undefined) {
78+
throw new TypeError('Expected either `type` or `characters`');
79+
}
80+
81+
if (characters !== undefined && typeof characters !== 'string') {
82+
throw new TypeError('Expected `characters` to be string');
83+
}
84+
85+
if (!allowedTypes.has(type)) {
86+
throw new TypeError(`Unknown type: ${type}`);
87+
}
88+
89+
if (type === undefined && characters === undefined) {
90+
type = 'hex';
91+
}
92+
93+
if (type === 'hex' || (type === undefined && characters === undefined)) {
94+
return specialRandomBytes(Math.ceil(length * 0.5), 'hex', length); // Needs 0.5 bytes of entropy per character
95+
}
96+
97+
if (type === 'base64') {
98+
return specialRandomBytes(Math.ceil(length * 0.75), 'base64', length); // Needs 0.75 bytes of entropy per character
99+
}
100+
101+
if (type === 'url-safe') {
102+
return generateForCustomCharacters(length, urlSafeCharacters, randomBytes);
103+
}
104+
105+
if (type === 'numeric') {
106+
return generateForCustomCharacters(length, numericCharacters, randomBytes);
107+
}
108+
109+
if (type === 'distinguishable') {
110+
return generateForCustomCharacters(length, distinguishableCharacters, randomBytes);
111+
}
112+
113+
if (type === 'ascii-printable') {
114+
return generateForCustomCharacters(length, asciiPrintableCharacters, randomBytes);
115+
}
116+
117+
if (type === 'alphanumeric') {
118+
return generateForCustomCharacters(length, alphanumericCharacters, randomBytes);
119+
}
120+
121+
if (characters.length === 0) {
122+
throw new TypeError('Expected `characters` string length to be greater than or equal to 1');
123+
}
124+
125+
if (characters.length > 0x10000) {
126+
throw new TypeError('Expected `characters` string length to be less or equal to 65536');
127+
}
128+
129+
return generateForCustomCharacters(length, characters.split(''), randomBytes);
130+
};
131+
132+
export function createStringGenerator(specialRandomBytes, randomBytes) {
133+
return createGenerator(generateForCustomCharacters, specialRandomBytes, randomBytes);
134+
}
135+
136+
export function createAsyncStringGenerator(specialRandomBytesAsync, randomBytesAsync) {
137+
return createGenerator(generateForCustomCharactersAsync, specialRandomBytesAsync, randomBytesAsync);
138+
}

‎index.js

+6-141
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,13 @@
1-
import {promisify} from 'util';
2-
import crypto from 'crypto';
1+
import {promisify} from 'node:util';
2+
import crypto from 'node:crypto';
3+
import {createStringGenerator, createAsyncStringGenerator} from './core.js';
34

45
const randomBytesAsync = promisify(crypto.randomBytes);
56

6-
const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split('');
7-
const numericCharacters = '0123456789'.split('');
8-
const distinguishableCharacters = 'CDEHKMPRTUWXY012458'.split('');
9-
const asciiPrintableCharacters = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.split('');
10-
const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('');
11-
12-
const generateForCustomCharacters = (length, characters) => {
13-
// Generating entropy is faster than complex math operations, so we use the simplest way
14-
const characterCount = characters.length;
15-
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
16-
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
17-
let string = '';
18-
let stringLength = 0;
19-
20-
while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
21-
const entropy = crypto.randomBytes(entropyLength);
22-
let entropyPosition = 0;
23-
24-
while (entropyPosition < entropyLength && stringLength < length) {
25-
const entropyValue = entropy.readUInt16LE(entropyPosition);
26-
entropyPosition += 2;
27-
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
28-
continue;
29-
}
30-
31-
string += characters[entropyValue % characterCount];
32-
stringLength++;
33-
}
34-
}
35-
36-
return string;
37-
};
38-
39-
const generateForCustomCharactersAsync = async (length, characters) => {
40-
// Generating entropy is faster than complex math operations, so we use the simplest way
41-
const characterCount = characters.length;
42-
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
43-
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
44-
let string = '';
45-
let stringLength = 0;
46-
47-
while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
48-
const entropy = await randomBytesAsync(entropyLength); // eslint-disable-line no-await-in-loop
49-
let entropyPosition = 0;
50-
51-
while (entropyPosition < entropyLength && stringLength < length) {
52-
const entropyValue = entropy.readUInt16LE(entropyPosition);
53-
entropyPosition += 2;
54-
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
55-
continue;
56-
}
57-
58-
string += characters[entropyValue % characterCount];
59-
stringLength++;
60-
}
61-
}
62-
63-
return string;
64-
};
65-
66-
const generateRandomBytes = (byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length);
67-
68-
const generateRandomBytesAsync = async (byteLength, type, length) => {
7+
const cryptoRandomString = createStringGenerator((byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length), crypto.randomBytes);
8+
cryptoRandomString.async = createAsyncStringGenerator(async (byteLength, type, length) => {
699
const buffer = await randomBytesAsync(byteLength);
7010
return buffer.toString(type).slice(0, length);
71-
};
72-
73-
const allowedTypes = new Set([
74-
undefined,
75-
'hex',
76-
'base64',
77-
'url-safe',
78-
'numeric',
79-
'distinguishable',
80-
'ascii-printable',
81-
'alphanumeric'
82-
]);
83-
84-
const createGenerator = (generateForCustomCharacters, generateRandomBytes) => ({length, type, characters}) => {
85-
if (!(length >= 0 && Number.isFinite(length))) {
86-
throw new TypeError('Expected a `length` to be a non-negative finite number');
87-
}
88-
89-
if (type !== undefined && characters !== undefined) {
90-
throw new TypeError('Expected either `type` or `characters`');
91-
}
92-
93-
if (characters !== undefined && typeof characters !== 'string') {
94-
throw new TypeError('Expected `characters` to be string');
95-
}
96-
97-
if (!allowedTypes.has(type)) {
98-
throw new TypeError(`Unknown type: ${type}`);
99-
}
100-
101-
if (type === undefined && characters === undefined) {
102-
type = 'hex';
103-
}
104-
105-
if (type === 'hex' || (type === undefined && characters === undefined)) {
106-
return generateRandomBytes(Math.ceil(length * 0.5), 'hex', length); // Need 0.5 byte entropy per character
107-
}
108-
109-
if (type === 'base64') {
110-
return generateRandomBytes(Math.ceil(length * 0.75), 'base64', length); // Need 0.75 byte of entropy per character
111-
}
112-
113-
if (type === 'url-safe') {
114-
return generateForCustomCharacters(length, urlSafeCharacters);
115-
}
116-
117-
if (type === 'numeric') {
118-
return generateForCustomCharacters(length, numericCharacters);
119-
}
120-
121-
if (type === 'distinguishable') {
122-
return generateForCustomCharacters(length, distinguishableCharacters);
123-
}
124-
125-
if (type === 'ascii-printable') {
126-
return generateForCustomCharacters(length, asciiPrintableCharacters);
127-
}
128-
129-
if (type === 'alphanumeric') {
130-
return generateForCustomCharacters(length, alphanumericCharacters);
131-
}
132-
133-
if (characters.length === 0) {
134-
throw new TypeError('Expected `characters` string length to be greater than or equal to 1');
135-
}
136-
137-
if (characters.length > 0x10000) {
138-
throw new TypeError('Expected `characters` string length to be less or equal to 65536');
139-
}
140-
141-
return generateForCustomCharacters(length, characters.split(''));
142-
};
143-
144-
const cryptoRandomString = createGenerator(generateForCustomCharacters, generateRandomBytes);
145-
146-
cryptoRandomString.async = createGenerator(generateForCustomCharactersAsync, generateRandomBytesAsync);
11+
}, randomBytesAsync);
14712

14813
export default cryptoRandomString;

‎package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
"url": "https://sindresorhus.com"
1212
},
1313
"type": "module",
14-
"exports": "./index.js",
14+
"exports": {
15+
"types": "./index.d.ts",
16+
"node": "./index.js",
17+
"browser": "./browser.js"
18+
},
1519
"engines": {
1620
"node": ">=12"
1721
},
@@ -20,6 +24,8 @@
2024
},
2125
"files": [
2226
"index.js",
27+
"browser.js",
28+
"core.js",
2329
"index.d.ts"
2430
],
2531
"keywords": [

‎readme.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
55
Can be useful for creating an identifier, slug, salt, PIN code, fixture, etc.
66

7+
Works in Node.js and browsers.
8+
79
## Install
810

9-
```
10-
$ npm install crypto-random-string
11+
```sh
12+
npm install crypto-random-string
1113
```
1214

1315
## Usage

0 commit comments

Comments
 (0)
Please sign in to comment.