Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require Node.js 14.16 #44

Merged
merged 17 commits into from
May 10, 2022
7 changes: 3 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ jobs:
fail-fast: false
matrix:
node-version:
- 14
- 12
- 16
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm install
Expand Down
35 changes: 12 additions & 23 deletions browser.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
/* eslint-env browser */
import {createStringGenerator, createAsyncStringGenerator} from './core.js';

const toHex = uInt8Array => uInt8Array.map(byte => byte.toString(16).padStart(2, '0')).join('');

const decoder = new TextDecoder('utf8');
const toBase64 = uInt8Array => btoa(decoder.decode(uInt8Array));
const toHex = uInt8Array => [...uInt8Array].map(byte => byte.toString(16).padStart(2, '0')).join('');
const toBase64 = uInt8Array => btoa(String.fromCodePoint(...uInt8Array));

// `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)
const maxEntropy = 65536;
const maxEntropy = 65_536;

function getRandomValues(byteLength) {
const generatedBytes = [];
const generatedBytes = new Uint8Array(byteLength);

while (byteLength > 0) {
const bytesToGenerate = Math.min(byteLength, maxEntropy);
generatedBytes.push(crypto.getRandomValues(new Uint8Array({length: bytesToGenerate})));
byteLength -= bytesToGenerate;
for (let totalGeneratedBytes = 0; totalGeneratedBytes < byteLength; totalGeneratedBytes += maxEntropy) {
generatedBytes.set(
crypto.getRandomValues(new Uint8Array(Math.min(maxEntropy, byteLength - totalGeneratedBytes))),
totalGeneratedBytes,
);
}
Richienb marked this conversation as resolved.
Show resolved Hide resolved

const result = new Uint8Array(generatedBytes.reduce((sum, {byteLength}) => sum + byteLength, 0)); // eslint-disable-line unicorn/no-array-reduce
let currentIndex = 0;

for (const bytes of generatedBytes) {
result.set(bytes, currentIndex);
currentIndex += bytes.byteLength;
}

return result;
return generatedBytes;
}

function specialRandomBytes(byteLength, type, length) {
Expand All @@ -36,7 +27,5 @@ function specialRandomBytes(byteLength, type, length) {
return convert(generatedBytes).slice(0, length);
}

const cryptoRandomString = createStringGenerator(specialRandomBytes, getRandomValues);
cryptoRandomString.async = createAsyncStringGenerator(specialRandomBytes, getRandomValues);

export default cryptoRandomString;
export default createStringGenerator(specialRandomBytes, getRandomValues);
export const cryptoRandomStringAsync = createAsyncStringGenerator(specialRandomBytes, getRandomValues);
26 changes: 14 additions & 12 deletions core.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split('');
const numericCharacters = '0123456789'.split('');
const distinguishableCharacters = 'CDEHKMPRTUWXY012458'.split('');
const asciiPrintableCharacters = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.split('');
const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('');
const urlSafeCharacters = [...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'];
const numericCharacters = [...'0123456789'];
const distinguishableCharacters = [...'CDEHKMPRTUWXY012458'];
const asciiPrintableCharacters = [...'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'];
const alphanumericCharacters = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'];

const readUInt16LE = (uInt8Array, offset) => uInt8Array[offset] + (uInt8Array[offset + 1] << 8); // eslint-disable-line no-bitwise

const generateForCustomCharacters = (length, characters, randomBytes) => {
// Generating entropy is faster than complex math operations, so we use the simplest way
const characterCount = characters.length;
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
const maxValidSelector = (Math.floor(0x1_00_00 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
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
let string = '';
let stringLength = 0;
Expand All @@ -17,7 +19,7 @@ const generateForCustomCharacters = (length, characters, randomBytes) => {
let entropyPosition = 0;

while (entropyPosition < entropyLength && stringLength < length) {
const entropyValue = entropy.readUInt16LE(entropyPosition);
const entropyValue = readUInt16LE(entropy, entropyPosition);
entropyPosition += 2;
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
continue;
Expand All @@ -34,7 +36,7 @@ const generateForCustomCharacters = (length, characters, randomBytes) => {
const generateForCustomCharactersAsync = async (length, characters, randomBytesAsync) => {
// Generating entropy is faster than complex math operations, so we use the simplest way
const characterCount = characters.length;
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
const maxValidSelector = (Math.floor(0x1_00_00 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
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
let string = '';
let stringLength = 0;
Expand All @@ -44,7 +46,7 @@ const generateForCustomCharactersAsync = async (length, characters, randomBytesA
let entropyPosition = 0;

while (entropyPosition < entropyLength && stringLength < length) {
const entropyValue = entropy.readUInt16LE(entropyPosition);
const entropyValue = readUInt16LE(entropy, entropyPosition);
entropyPosition += 2;
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
continue;
Expand All @@ -66,7 +68,7 @@ const allowedTypes = new Set([
'numeric',
'distinguishable',
'ascii-printable',
'alphanumeric'
'alphanumeric',
]);

const createGenerator = (generateForCustomCharacters, specialRandomBytes, randomBytes) => ({length, type, characters}) => {
Expand Down Expand Up @@ -122,11 +124,11 @@ const createGenerator = (generateForCustomCharacters, specialRandomBytes, random
throw new TypeError('Expected `characters` string length to be greater than or equal to 1');
}

if (characters.length > 0x10000) {
if (characters.length > 0x1_00_00) {
throw new TypeError('Expected `characters` string length to be less or equal to 65536');
}

return generateForCustomCharacters(length, characters.split(''), randomBytes);
return generateForCustomCharacters(length, characters, randomBytes);
};

export function createStringGenerator(specialRandomBytes, randomBytes) {
Expand Down
54 changes: 25 additions & 29 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,42 +68,38 @@ interface CharactersOption {

export type Options = BaseOptions & MergeExclusive<TypeOption, CharactersOption>;

declare const cryptoRandomString: {
/**
Generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.

@returns A randomized string.
/**
Generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.

@example
```
import cryptoRandomString from 'crypto-random-string';
@returns A randomized string.

cryptoRandomString({length: 10});
//=> '2cf05d94db'
```
*/
(options: Options): string;
@example
```
import cryptoRandomString from 'crypto-random-string';

/**
Asynchronously generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.
cryptoRandomString({length: 10});
//=> '2cf05d94db'
```
*/
export default function cryptoRandomString(options: Options): string;

For most use-cases, there's really no good reason to use this async version. From the Node.js docs:
/**
Asynchronously generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.

> The `crypto.randomBytes()` method will not complete until there is sufficient entropy available. This should normally never take longer than a few milliseconds. The only time when generating the random bytes may conceivably block for a longer period of time is right after boot, when the whole system is still low on entropy.
For most use-cases, there's really no good reason to use this async version. From the Node.js docs:

In general, anything async comes with some overhead on it's own.
> The `crypto.randomBytes()` method will not complete until there is sufficient entropy available. This should normally never take longer than a few milliseconds. The only time when generating the random bytes may conceivably block for a longer period of time is right after boot, when the whole system is still low on entropy.

@returns A promise which resolves to a randomized string.
In general, anything async comes with some overhead on it's own.

@example
```
import cryptoRandomString from 'crypto-random-string';
@returns A promise which resolves to a randomized string.

await cryptoRandomString.async({length: 10});
//=> '2cf05d94db'
```
*/
async(options: Options): Promise<string>;
};
@example
```
import {cryptoRandomStringAsync} from 'crypto-random-string';

export default cryptoRandomString;
await cryptoRandomStringAsync({length: 10});
//=> '2cf05d94db'
```
*/
export function cryptoRandomStringAsync(options: Options): Promise<string>;
14 changes: 7 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// TODO: When targeting Node.js 16, remove `cryptoRandomStringAsync` and use `crypto.webcrypto.getRandomValues` to interop with the browser code.
// TODO: Later, when targeting Node.js 18, only use the browser code
import {promisify} from 'node:util';
import crypto from 'node:crypto';
import {randomBytes} from 'node:crypto';
import {createStringGenerator, createAsyncStringGenerator} from './core.js';

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

const cryptoRandomString = createStringGenerator((byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length), crypto.randomBytes);
cryptoRandomString.async = createAsyncStringGenerator(async (byteLength, type, length) => {
export default createStringGenerator((byteLength, type, length) => randomBytes(byteLength).toString(type).slice(0, length), size => new Uint8Array(randomBytes(size)));
export const cryptoRandomStringAsync = createAsyncStringGenerator(async (byteLength, type, length) => {
const buffer = await randomBytesAsync(byteLength);
return buffer.toString(type).slice(0, length);
}, randomBytesAsync);

export default cryptoRandomString;
}, async size => new Uint8Array(await randomBytesAsync(size)));
4 changes: 2 additions & 2 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {expectType, expectError} from 'tsd';
import cryptoRandomString from './index.js';
import cryptoRandomString, {cryptoRandomStringAsync} from './index.js';

expectType<string>(cryptoRandomString({length: 10}));
expectType<string>(cryptoRandomString({length: 10, type: 'url-safe'}));
expectType<string>(cryptoRandomString({length: 10, type: 'numeric'}));
expectType<string>(cryptoRandomString({length: 10, characters: '1234'}));
expectType<Promise<string>>(cryptoRandomString.async({length: 10}));
expectType<Promise<string>>(cryptoRandomStringAsync({length: 10}));

expectError(cryptoRandomString({type: 'url-safe'}));
expectError(cryptoRandomString({length: 10, type: 'url-safe', characters: '1234'}));
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"browser": "./browser.js"
},
"engines": {
"node": ">=12"
"node": ">=14.16"
},
"scripts": {
"test": "xo && ava && tsd"
Expand Down Expand Up @@ -45,11 +45,12 @@
"protect"
],
"dependencies": {
"type-fest": "^1.0.1"
"type-fest": "^2.12.2"
},
"devDependencies": {
"ava": "^3.15.0",
"tsd": "^0.14.0",
"xo": "^0.38.2"
"ava": "^4.2.0",
"dot-prop": "^7.2.0",
"tsd": "^0.20.0",
"xo": "^0.48.0"
}
}
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ cryptoRandomString({length: 10, characters: 'abc'});

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

### cryptoRandomString.async(options)
### cryptoRandomStringAsync(options)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to indicate somehow that this is a named export and the other one is a default export.


Returns a promise which resolves to a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default.

Expand Down
Loading