From bb59780cd7ebec9c4f187b51540264def8c0e9f6 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 19 Apr 2022 01:32:02 +1200 Subject: [PATCH 01/16] Require Node.js 14 Signed-off-by: Richie Bendall --- browser.js | 10 ++-- core.js | 20 +++---- index.d.ts | 54 +++++++++---------- index.js | 6 +-- index.test-d.ts | 4 +- package.json | 12 ++--- readme.md | 2 +- test.js | 139 +++++++++++++++++++++++++----------------------- 8 files changed, 122 insertions(+), 125 deletions(-) diff --git a/browser.js b/browser.js index f621fb7..e96a23a 100644 --- a/browser.js +++ b/browser.js @@ -7,7 +7,7 @@ const decoder = new TextDecoder('utf8'); const toBase64 = uInt8Array => btoa(decoder.decode(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 = []; @@ -18,7 +18,7 @@ function getRandomValues(byteLength) { byteLength -= bytesToGenerate; } - const result = new Uint8Array(generatedBytes.reduce((sum, {byteLength}) => sum + byteLength, 0)); // eslint-disable-line unicorn/no-array-reduce + const result = new Uint8Array(generatedBytes.reduce((sum, {byteLength}) => sum + byteLength, 0)); let currentIndex = 0; for (const bytes of generatedBytes) { @@ -36,7 +36,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); diff --git a/core.js b/core.js index 7098b42..2c77e93 100644 --- a/core.js +++ b/core.js @@ -1,13 +1,13 @@ -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 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; @@ -34,7 +34,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; @@ -66,7 +66,7 @@ const allowedTypes = new Set([ 'numeric', 'distinguishable', 'ascii-printable', - 'alphanumeric' + 'alphanumeric', ]); const createGenerator = (generateForCustomCharacters, specialRandomBytes, randomBytes) => ({length, type, characters}) => { @@ -122,11 +122,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) { diff --git a/index.d.ts b/index.d.ts index 6952c9b..a49d5d9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -68,42 +68,38 @@ interface CharactersOption { export type Options = BaseOptions & MergeExclusive; -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; -}; +@example +``` +import {cryptoRandomStringAsync} from 'crypto-random-string'; -export default cryptoRandomString; +await cryptoRandomStringAsync({length: 10}); +//=> '2cf05d94db' +``` +*/ +export function cryptoRandomStringAsync(options: Options): Promise; diff --git a/index.js b/index.js index 1810aa9..ec03cb0 100644 --- a/index.js +++ b/index.js @@ -4,10 +4,8 @@ import {createStringGenerator, createAsyncStringGenerator} from './core.js'; const randomBytesAsync = promisify(crypto.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) => crypto.randomBytes(byteLength).toString(type).slice(0, length), crypto.randomBytes); +export const cryptoRandomStringAsync = createAsyncStringGenerator(async (byteLength, type, length) => { const buffer = await randomBytesAsync(byteLength); return buffer.toString(type).slice(0, length); }, randomBytesAsync); - -export default cryptoRandomString; diff --git a/index.test-d.ts b/index.test-d.ts index 3c0dddf..cc2c416 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,11 +1,11 @@ import {expectType, expectError} from 'tsd'; -import cryptoRandomString from './index.js'; +import cryptoRandomString, {cryptoRandomStringAsync} from './index.js'; expectType(cryptoRandomString({length: 10})); expectType(cryptoRandomString({length: 10, type: 'url-safe'})); expectType(cryptoRandomString({length: 10, type: 'numeric'})); expectType(cryptoRandomString({length: 10, characters: '1234'})); -expectType>(cryptoRandomString.async({length: 10})); +expectType>(cryptoRandomStringAsync({length: 10})); expectError(cryptoRandomString({type: 'url-safe'})); expectError(cryptoRandomString({length: 10, type: 'url-safe', characters: '1234'})); diff --git a/package.json b/package.json index f903582..12243d6 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ "browser": "./browser.js" }, "engines": { - "node": ">=12" + "node": ">=14" }, "scripts": { - "test": "xo && ava && tsd" + "test": "xo && uvu && tsd" }, "files": [ "index.js", @@ -45,11 +45,11 @@ "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" + "tsd": "^0.20.0", + "uvu": "^0.5.3", + "xo": "^0.48.0" } } diff --git a/readme.md b/readme.md index 786c239..ce9bf26 100644 --- a/readme.md +++ b/readme.md @@ -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) Returns a promise which resolves to a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default. diff --git a/test.js b/test.js index acdf632..a36e68c 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,6 @@ -import test from 'ava'; -import cryptoRandomString from './index.js'; +import {test} from 'uvu'; +import {is, match, throws} from 'uvu/assert'; +import cryptoRandomString, {cryptoRandomStringAsync} from './index.js'; // Probabilistic, result is always less than or equal to actual set size, chance it is less is below 1e-256 for sizes up to 32656. const generatedCharacterSetSize = (options, targetSize) => { @@ -14,103 +15,107 @@ const generatedCharacterSetSize = (options, targetSize) => { return set.size; }; -test('main', t => { - t.is(cryptoRandomString({length: 0}).length, 0); - t.is(cryptoRandomString({length: 10}).length, 10); - t.is(cryptoRandomString({length: 100}).length, 100); - t.regex(cryptoRandomString({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic - t.is(generatedCharacterSetSize({}, 16), 16); +test('main', () => { + is(cryptoRandomString({length: 0}).length, 0); + is(cryptoRandomString({length: 10}).length, 10); + is(cryptoRandomString({length: 100}).length, 100); + match(cryptoRandomString({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize({}, 16), 16); }); -test('async', async t => { - t.is((await cryptoRandomString.async({length: 0})).length, 0); - t.is((await cryptoRandomString.async({length: 10})).length, 10); - t.is((await cryptoRandomString.async({length: 100})).length, 100); - t.regex(await cryptoRandomString.async({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic +test('async', async () => { + /* eslint-disable unicornn/no-await-expression-member */ + is((await cryptoRandomStringAsync({length: 0})).length, 0); + is((await cryptoRandomStringAsync({length: 10})).length, 10); + is((await cryptoRandomStringAsync({length: 100})).length, 100); + /* eslint-enable unicornn/no-await-expression-member */ + match(await cryptoRandomStringAsync({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic }); -test('hex', t => { - t.is(cryptoRandomString({length: 0, type: 'hex'}).length, 0); - t.is(cryptoRandomString({length: 10, type: 'hex'}).length, 10); - t.is(cryptoRandomString({length: 100, type: 'hex'}).length, 100); - t.regex(cryptoRandomString({length: 100, type: 'hex'}), /^[a-f\d]*$/); // Sanity check, probabilistic - t.is(generatedCharacterSetSize({type: 'hex'}, 16), 16); +test('hex', () => { + is(cryptoRandomString({length: 0, type: 'hex'}).length, 0); + is(cryptoRandomString({length: 10, type: 'hex'}).length, 10); + is(cryptoRandomString({length: 100, type: 'hex'}).length, 100); + match(cryptoRandomString({length: 100, type: 'hex'}), /^[a-f\d]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize({type: 'hex'}, 16), 16); }); -test('base64', t => { - t.is(cryptoRandomString({length: 0, type: 'base64'}).length, 0); - t.is(cryptoRandomString({length: 10, type: 'base64'}).length, 10); - t.is(cryptoRandomString({length: 100, type: 'base64'}).length, 100); - t.regex(cryptoRandomString({length: 100, type: 'base64'}), /^[a-zA-Z\d/+]*$/); // Sanity check, probabilistic - t.is(generatedCharacterSetSize({type: 'base64'}, 64), 64); +test('base64', () => { + is(cryptoRandomString({length: 0, type: 'base64'}).length, 0); + is(cryptoRandomString({length: 10, type: 'base64'}).length, 10); + is(cryptoRandomString({length: 100, type: 'base64'}).length, 100); + match(cryptoRandomString({length: 100, type: 'base64'}), /^[a-zA-Z\d/+]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize({type: 'base64'}, 64), 64); }); -test('url-safe', t => { - t.is(cryptoRandomString({length: 0, type: 'url-safe'}).length, 0); - t.is(cryptoRandomString({length: 10, type: 'url-safe'}).length, 10); - t.is(cryptoRandomString({length: 100, type: 'url-safe'}).length, 100); - t.regex(cryptoRandomString({length: 100, type: 'url-safe'}), /^[\w.~-]*$/); // Sanity check, probabilistic - t.is(generatedCharacterSetSize({type: 'url-safe'}, 66), 66); +test('url-safe', () => { + is(cryptoRandomString({length: 0, type: 'url-safe'}).length, 0); + is(cryptoRandomString({length: 10, type: 'url-safe'}).length, 10); + is(cryptoRandomString({length: 100, type: 'url-safe'}).length, 100); + match(cryptoRandomString({length: 100, type: 'url-safe'}), /^[\w.~-]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize({type: 'url-safe'}, 66), 66); }); -test('numeric', t => { - t.is(cryptoRandomString({length: 0, type: 'numeric'}).length, 0); - t.is(cryptoRandomString({length: 10, type: 'numeric'}).length, 10); - t.is(cryptoRandomString({length: 100, type: 'numeric'}).length, 100); - t.regex(cryptoRandomString({length: 100, type: 'numeric'}), /^\d*$/); // Sanity check, probabilistic - t.is(generatedCharacterSetSize({type: 'numeric'}, 10), 10); +test('numeric', () => { + is(cryptoRandomString({length: 0, type: 'numeric'}).length, 0); + is(cryptoRandomString({length: 10, type: 'numeric'}).length, 10); + is(cryptoRandomString({length: 100, type: 'numeric'}).length, 100); + match(cryptoRandomString({length: 100, type: 'numeric'}), /^\d*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize({type: 'numeric'}, 10), 10); }); -test('distinguishable', t => { - t.is(cryptoRandomString({length: 0, type: 'distinguishable'}).length, 0); - t.is(cryptoRandomString({length: 10, type: 'distinguishable'}).length, 10); - t.is(cryptoRandomString({length: 100, type: 'distinguishable'}).length, 100); - t.regex(cryptoRandomString({length: 100, type: 'distinguishable'}), /^[CDEHKMPRTUWXY012458]*$/); // Sanity check, probabilistic - t.is(generatedCharacterSetSize({type: 'distinguishable'}, 19), 19); +test('distinguishable', () => { + is(cryptoRandomString({length: 0, type: 'distinguishable'}).length, 0); + is(cryptoRandomString({length: 10, type: 'distinguishable'}).length, 10); + is(cryptoRandomString({length: 100, type: 'distinguishable'}).length, 100); + match(cryptoRandomString({length: 100, type: 'distinguishable'}), /^[CDEHKMPRTUWXY012458]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize({type: 'distinguishable'}, 19), 19); }); -test('ascii-printable', t => { - t.is(cryptoRandomString({length: 0, type: 'ascii-printable'}).length, 0); - t.is(cryptoRandomString({length: 10, type: 'ascii-printable'}).length, 10); - t.is(cryptoRandomString({length: 100, type: 'ascii-printable'}).length, 100); - t.regex(cryptoRandomString({length: 100, type: 'ascii-printable'}), /^[!"#$%&'()*+,-./\w:;<=>?@[\\\]^`{|}~]*$/); // Sanity check, probabilistic +test('ascii-printable', () => { + is(cryptoRandomString({length: 0, type: 'ascii-printable'}).length, 0); + is(cryptoRandomString({length: 10, type: 'ascii-printable'}).length, 10); + is(cryptoRandomString({length: 100, type: 'ascii-printable'}).length, 100); + match(cryptoRandomString({length: 100, type: 'ascii-printable'}), /^[!"#$%&'()*+,-./\w:;<=>?@[\\\]^`{|}~]*$/); // Sanity check, probabilistic }); -test('alphanumeric', t => { - t.is(cryptoRandomString({length: 0, type: 'alphanumeric'}).length, 0); - t.is(cryptoRandomString({length: 10, type: 'alphanumeric'}).length, 10); - t.is(cryptoRandomString({length: 100, type: 'alphanumeric'}).length, 100); - t.regex(cryptoRandomString({length: 100, type: 'alphanumeric'}), /^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]*$/); // Sanity check, probabilistic - t.is(generatedCharacterSetSize({type: 'alphanumeric'}, 19), 62); +test('alphanumeric', () => { + is(cryptoRandomString({length: 0, type: 'alphanumeric'}).length, 0); + is(cryptoRandomString({length: 10, type: 'alphanumeric'}).length, 10); + is(cryptoRandomString({length: 100, type: 'alphanumeric'}).length, 100); + match(cryptoRandomString({length: 100, type: 'alphanumeric'}), /^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize({type: 'alphanumeric'}, 19), 62); }); -test('characters', t => { - t.is(cryptoRandomString({length: 0, characters: '1234'}).length, 0); - t.is(cryptoRandomString({length: 10, characters: '1234'}).length, 10); - t.is(cryptoRandomString({length: 100, characters: '1234'}).length, 100); - t.regex(cryptoRandomString({length: 100, characters: '1234'}), /^[1-4]*$/); // Sanity check, probabilistic - t.is(generatedCharacterSetSize({characters: '1234'}, 4), 4); - t.is(generatedCharacterSetSize({characters: '0123456789'}, 10), 10); +test('characters', () => { + is(cryptoRandomString({length: 0, characters: '1234'}).length, 0); + is(cryptoRandomString({length: 10, characters: '1234'}).length, 10); + is(cryptoRandomString({length: 100, characters: '1234'}).length, 100); + match(cryptoRandomString({length: 100, characters: '1234'}), /^[1-4]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize({characters: '1234'}, 4), 4); + is(generatedCharacterSetSize({characters: '0123456789'}, 10), 10); }); -test('argument errors', t => { - t.throws(() => { +test('argument errors', () => { + throws(() => { cryptoRandomString({length: Number.POSITIVE_INFINITY}); }); - t.throws(() => { + throws(() => { cryptoRandomString({length: -1}); }); - t.throws(() => { + throws(() => { cryptoRandomString({length: 0, type: 'hex', characters: '1234'}); }); - t.throws(() => { + throws(() => { cryptoRandomString({length: 0, characters: 42}); }); - t.throws(() => { + throws(() => { cryptoRandomString({length: 0, type: 'unknown'}); }); }); + +test.run(); From 87c40d2ae818c1c8ba9ddb1671250cf11afed018 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 19 Apr 2022 01:48:22 +1200 Subject: [PATCH 02/16] Fix Signed-off-by: Richie Bendall --- test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test.js b/test.js index a36e68c..9bc0abd 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,5 @@ import {test} from 'uvu'; -import {is, match, throws} from 'uvu/assert'; +import {is, match, throws} from 'uvu/assert'; // eslint-disable-line node/file-extension-in-import import cryptoRandomString, {cryptoRandomStringAsync} from './index.js'; // Probabilistic, result is always less than or equal to actual set size, chance it is less is below 1e-256 for sizes up to 32656. @@ -24,11 +24,11 @@ test('main', () => { }); test('async', async () => { - /* eslint-disable unicornn/no-await-expression-member */ + /* eslint-disable unicorn/no-await-expression-member */ is((await cryptoRandomStringAsync({length: 0})).length, 0); is((await cryptoRandomStringAsync({length: 10})).length, 10); is((await cryptoRandomStringAsync({length: 100})).length, 100); - /* eslint-enable unicornn/no-await-expression-member */ + /* eslint-enable unicorn/no-await-expression-member */ match(await cryptoRandomStringAsync({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic }); From 2326ee09538f38afbd2488ca6e3fe182e98609bc Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 21 Apr 2022 13:55:52 +1200 Subject: [PATCH 03/16] Test Signed-off-by: Richie Bendall --- .github/workflows/main.yml | 3 +- package.json | 1 + test.js | 228 ++++++++++++++++++++----------------- 3 files changed, 127 insertions(+), 105 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 41fe626..3921e01 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,8 +10,7 @@ jobs: fail-fast: false matrix: node-version: - - 14 - - 12 + - 16 steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 diff --git a/package.json b/package.json index 12243d6..185b2da 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "type-fest": "^2.12.2" }, "devDependencies": { + "dot-prop": "^7.2.0", "tsd": "^0.20.0", "uvu": "^0.5.3", "xo": "^0.48.0" diff --git a/test.js b/test.js index 9bc0abd..e36031e 100644 --- a/test.js +++ b/test.js @@ -1,121 +1,143 @@ -import {test} from 'uvu'; +import {webcrypto} from 'node:crypto'; +import {hasProperty, setProperty} from 'dot-prop'; +import {suite} from 'uvu'; import {is, match, throws} from 'uvu/assert'; // eslint-disable-line node/file-extension-in-import -import cryptoRandomString, {cryptoRandomStringAsync} from './index.js'; - -// Probabilistic, result is always less than or equal to actual set size, chance it is less is below 1e-256 for sizes up to 32656. -const generatedCharacterSetSize = (options, targetSize) => { - const set = new Set(); - const length = targetSize * 640; - const string = cryptoRandomString({...options, length}); - - for (let i = 0; i < length; i++) { - set.add(string[i]); - } - - return set.size; -}; - -test('main', () => { - is(cryptoRandomString({length: 0}).length, 0); - is(cryptoRandomString({length: 10}).length, 10); - is(cryptoRandomString({length: 100}).length, 100); - match(cryptoRandomString({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize({}, 16), 16); -}); - -test('async', async () => { - /* eslint-disable unicorn/no-await-expression-member */ - is((await cryptoRandomStringAsync({length: 0})).length, 0); - is((await cryptoRandomStringAsync({length: 10})).length, 10); - is((await cryptoRandomStringAsync({length: 100})).length, 100); - /* eslint-enable unicorn/no-await-expression-member */ - match(await cryptoRandomStringAsync({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic -}); - -test('hex', () => { - is(cryptoRandomString({length: 0, type: 'hex'}).length, 0); - is(cryptoRandomString({length: 10, type: 'hex'}).length, 10); - is(cryptoRandomString({length: 100, type: 'hex'}).length, 100); - match(cryptoRandomString({length: 100, type: 'hex'}), /^[a-f\d]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize({type: 'hex'}, 16), 16); -}); - -test('base64', () => { - is(cryptoRandomString({length: 0, type: 'base64'}).length, 0); - is(cryptoRandomString({length: 10, type: 'base64'}).length, 10); - is(cryptoRandomString({length: 100, type: 'base64'}).length, 100); - match(cryptoRandomString({length: 100, type: 'base64'}), /^[a-zA-Z\d/+]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize({type: 'base64'}, 64), 64); -}); - -test('url-safe', () => { - is(cryptoRandomString({length: 0, type: 'url-safe'}).length, 0); - is(cryptoRandomString({length: 10, type: 'url-safe'}).length, 10); - is(cryptoRandomString({length: 100, type: 'url-safe'}).length, 100); - match(cryptoRandomString({length: 100, type: 'url-safe'}), /^[\w.~-]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize({type: 'url-safe'}, 66), 66); -}); +import browserCryptoRandomString, {cryptoRandomStringAsync as browserCryptoRandomStringAsync} from './browser.js'; +import nodeCryptoRandomString, {cryptoRandomStringAsync as nodeCryptoRandomStringAsync} from './index.js'; + +if (!hasProperty(globalThis, 'crypto.getRandomValues')) { + setProperty(globalThis, 'crypto.getRandomValues', webcrypto.getRandomValues); +} + +function runTests(test) { + // Probabilistic, result is always less than or equal to actual set size, chance it is less is below 1e-256 for sizes up to 32656. + const generatedCharacterSetSize = (cryptoRandomString, options, targetSize) => { + const set = new Set(); + const length = targetSize * 640; + const string = cryptoRandomString({...options, length}); + + for (let i = 0; i < length; i++) { + set.add(string[i]); + } + + return set.size; + }; + + test('main', ({cryptoRandomString}) => { + is(cryptoRandomString({length: 0}).length, 0); + is(cryptoRandomString({length: 10}).length, 10); + is(cryptoRandomString({length: 100}).length, 100); + match(cryptoRandomString({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize(cryptoRandomString, {}, 16), 16); + }); -test('numeric', () => { - is(cryptoRandomString({length: 0, type: 'numeric'}).length, 0); - is(cryptoRandomString({length: 10, type: 'numeric'}).length, 10); - is(cryptoRandomString({length: 100, type: 'numeric'}).length, 100); - match(cryptoRandomString({length: 100, type: 'numeric'}), /^\d*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize({type: 'numeric'}, 10), 10); -}); + test('async', async ({cryptoRandomStringAsync}) => { + /* eslint-disable unicorn/no-await-expression-member */ + is((await cryptoRandomStringAsync({length: 0})).length, 0); + is((await cryptoRandomStringAsync({length: 10})).length, 10); + is((await cryptoRandomStringAsync({length: 100})).length, 100); + /* eslint-enable unicorn/no-await-expression-member */ + match(await cryptoRandomStringAsync({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic + }); -test('distinguishable', () => { - is(cryptoRandomString({length: 0, type: 'distinguishable'}).length, 0); - is(cryptoRandomString({length: 10, type: 'distinguishable'}).length, 10); - is(cryptoRandomString({length: 100, type: 'distinguishable'}).length, 100); - match(cryptoRandomString({length: 100, type: 'distinguishable'}), /^[CDEHKMPRTUWXY012458]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize({type: 'distinguishable'}, 19), 19); -}); + test('hex', ({cryptoRandomString}) => { + is(cryptoRandomString({length: 0, type: 'hex'}).length, 0); + is(cryptoRandomString({length: 10, type: 'hex'}).length, 10); + is(cryptoRandomString({length: 100, type: 'hex'}).length, 100); + match(cryptoRandomString({length: 100, type: 'hex'}), /^[a-f\d]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize(cryptoRandomString, {type: 'hex'}, 16), 16); + }); -test('ascii-printable', () => { - is(cryptoRandomString({length: 0, type: 'ascii-printable'}).length, 0); - is(cryptoRandomString({length: 10, type: 'ascii-printable'}).length, 10); - is(cryptoRandomString({length: 100, type: 'ascii-printable'}).length, 100); - match(cryptoRandomString({length: 100, type: 'ascii-printable'}), /^[!"#$%&'()*+,-./\w:;<=>?@[\\\]^`{|}~]*$/); // Sanity check, probabilistic -}); + test('base64', ({cryptoRandomString}) => { + is(cryptoRandomString({length: 0, type: 'base64'}).length, 0); + is(cryptoRandomString({length: 10, type: 'base64'}).length, 10); + is(cryptoRandomString({length: 100, type: 'base64'}).length, 100); + match(cryptoRandomString({length: 100, type: 'base64'}), /^[a-zA-Z\d/+]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize(cryptoRandomString, {type: 'base64'}, 64), 64); + }); -test('alphanumeric', () => { - is(cryptoRandomString({length: 0, type: 'alphanumeric'}).length, 0); - is(cryptoRandomString({length: 10, type: 'alphanumeric'}).length, 10); - is(cryptoRandomString({length: 100, type: 'alphanumeric'}).length, 100); - match(cryptoRandomString({length: 100, type: 'alphanumeric'}), /^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize({type: 'alphanumeric'}, 19), 62); -}); + test('url-safe', ({cryptoRandomString}) => { + is(cryptoRandomString({length: 0, type: 'url-safe'}).length, 0); + is(cryptoRandomString({length: 10, type: 'url-safe'}).length, 10); + is(cryptoRandomString({length: 100, type: 'url-safe'}).length, 100); + match(cryptoRandomString({length: 100, type: 'url-safe'}), /^[\w.~-]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize(cryptoRandomString, {type: 'url-safe'}, 66), 66); + }); -test('characters', () => { - is(cryptoRandomString({length: 0, characters: '1234'}).length, 0); - is(cryptoRandomString({length: 10, characters: '1234'}).length, 10); - is(cryptoRandomString({length: 100, characters: '1234'}).length, 100); - match(cryptoRandomString({length: 100, characters: '1234'}), /^[1-4]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize({characters: '1234'}, 4), 4); - is(generatedCharacterSetSize({characters: '0123456789'}, 10), 10); -}); + test('numeric', ({cryptoRandomString}) => { + is(cryptoRandomString({length: 0, type: 'numeric'}).length, 0); + is(cryptoRandomString({length: 10, type: 'numeric'}).length, 10); + is(cryptoRandomString({length: 100, type: 'numeric'}).length, 100); + match(cryptoRandomString({length: 100, type: 'numeric'}), /^\d*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize(cryptoRandomString, {type: 'numeric'}, 10), 10); + }); -test('argument errors', () => { - throws(() => { - cryptoRandomString({length: Number.POSITIVE_INFINITY}); + test('distinguishable', ({cryptoRandomString}) => { + is(cryptoRandomString({length: 0, type: 'distinguishable'}).length, 0); + is(cryptoRandomString({length: 10, type: 'distinguishable'}).length, 10); + is(cryptoRandomString({length: 100, type: 'distinguishable'}).length, 100); + match(cryptoRandomString({length: 100, type: 'distinguishable'}), /^[CDEHKMPRTUWXY012458]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize(cryptoRandomString, {type: 'distinguishable'}, 19), 19); }); - throws(() => { - cryptoRandomString({length: -1}); + test('ascii-printable', ({cryptoRandomString}) => { + is(cryptoRandomString({length: 0, type: 'ascii-printable'}).length, 0); + is(cryptoRandomString({length: 10, type: 'ascii-printable'}).length, 10); + is(cryptoRandomString({length: 100, type: 'ascii-printable'}).length, 100); + match(cryptoRandomString({length: 100, type: 'ascii-printable'}), /^[!"#$%&'()*+,-./\w:;<=>?@[\\\]^`{|}~]*$/); // Sanity check, probabilistic }); - throws(() => { - cryptoRandomString({length: 0, type: 'hex', characters: '1234'}); + test('alphanumeric', ({cryptoRandomString}) => { + is(cryptoRandomString({length: 0, type: 'alphanumeric'}).length, 0); + is(cryptoRandomString({length: 10, type: 'alphanumeric'}).length, 10); + is(cryptoRandomString({length: 100, type: 'alphanumeric'}).length, 100); + match(cryptoRandomString({length: 100, type: 'alphanumeric'}), /^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize(cryptoRandomString, {type: 'alphanumeric'}, 19), 62); }); - throws(() => { - cryptoRandomString({length: 0, characters: 42}); + test('characters', ({cryptoRandomString}) => { + is(cryptoRandomString({length: 0, characters: '1234'}).length, 0); + is(cryptoRandomString({length: 10, characters: '1234'}).length, 10); + is(cryptoRandomString({length: 100, characters: '1234'}).length, 100); + match(cryptoRandomString({length: 100, characters: '1234'}), /^[1-4]*$/); // Sanity check, probabilistic + is(generatedCharacterSetSize(cryptoRandomString, {characters: '1234'}, 4), 4); + is(generatedCharacterSetSize(cryptoRandomString, {characters: '0123456789'}, 10), 10); }); - throws(() => { - cryptoRandomString({length: 0, type: 'unknown'}); + test('argument errors', ({cryptoRandomString}) => { + throws(() => { + cryptoRandomString({length: Number.POSITIVE_INFINITY}); + }); + + throws(() => { + cryptoRandomString({length: -1}); + }); + + throws(() => { + cryptoRandomString({length: 0, type: 'hex', characters: '1234'}); + }); + + throws(() => { + cryptoRandomString({length: 0, characters: 42}); + }); + + throws(() => { + cryptoRandomString({length: 0, type: 'unknown'}); + }); }); + + test.run(); +} + +const nodeTests = suite('Node.js', { + cryptoRandomString: nodeCryptoRandomString, + cryptoRandomStringAsync: nodeCryptoRandomStringAsync, +}); + +const browserTests = suite('Browser', { + cryptoRandomString: browserCryptoRandomString, + cryptoRandomStringAsync: browserCryptoRandomStringAsync, }); -test.run(); +runTests(nodeTests); +runTests(browserTests); From 31efb7969325f56e68520a0ed3e38e659fc1b480 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 21 Apr 2022 16:13:28 +1200 Subject: [PATCH 04/16] Fix Signed-off-by: Richie Bendall --- .github/workflows/main.yml | 4 ++-- browser.js | 27 +++++++++------------------ core.js | 6 ++++-- index.js | 4 ++-- test.js | 12 ++++-------- 5 files changed, 21 insertions(+), 32 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3921e01..a2c4c4a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,8 +12,8 @@ jobs: node-version: - 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 diff --git a/browser.js b/browser.js index e96a23a..a42c0c8 100644 --- a/browser.js +++ b/browser.js @@ -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 = 65_536; function getRandomValues(byteLength) { - const generatedBytes = []; - - while (byteLength > 0) { - const bytesToGenerate = Math.min(byteLength, maxEntropy); - generatedBytes.push(crypto.getRandomValues(new Uint8Array({length: bytesToGenerate}))); - byteLength -= bytesToGenerate; - } - - const result = new Uint8Array(generatedBytes.reduce((sum, {byteLength}) => sum + byteLength, 0)); - let currentIndex = 0; + const generatedBytes = new Uint8Array(byteLength); - for (const bytes of generatedBytes) { - result.set(bytes, currentIndex); - currentIndex += bytes.byteLength; + for (let totalGeneratedBytes = 0; totalGeneratedBytes < byteLength; totalGeneratedBytes += maxEntropy) { + generatedBytes.set( + crypto.getRandomValues(new Uint8Array(Math.min(maxEntropy, byteLength - totalGeneratedBytes))), + totalGeneratedBytes, + ); } - return result; + return generatedBytes; } function specialRandomBytes(byteLength, type, length) { diff --git a/core.js b/core.js index 2c77e93..6b0d5b1 100644 --- a/core.js +++ b/core.js @@ -4,6 +4,8 @@ const distinguishableCharacters = [...'CDEHKMPRTUWXY012458']; const asciiPrintableCharacters = [...'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~']; const alphanumericCharacters = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789']; +const readUInt16LE = (uInt8Array, offset) => uInt8Array[offset] + (uInt8Array[offset + 1] * (2 ** 8)); + const generateForCustomCharacters = (length, characters, randomBytes) => { // Generating entropy is faster than complex math operations, so we use the simplest way const characterCount = characters.length; @@ -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; @@ -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; diff --git a/index.js b/index.js index ec03cb0..3bf6906 100644 --- a/index.js +++ b/index.js @@ -4,8 +4,8 @@ import {createStringGenerator, createAsyncStringGenerator} from './core.js'; const randomBytesAsync = promisify(crypto.randomBytes); -export default createStringGenerator((byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length), crypto.randomBytes); +export default createStringGenerator((byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length), size => new Uint8Array(crypto.randomBytes(size))); export const cryptoRandomStringAsync = createAsyncStringGenerator(async (byteLength, type, length) => { const buffer = await randomBytesAsync(byteLength); return buffer.toString(type).slice(0, length); -}, randomBytesAsync); +}, async size => new Uint8Array(await randomBytesAsync(size))); diff --git a/test.js b/test.js index e36031e..fe205ea 100644 --- a/test.js +++ b/test.js @@ -129,15 +129,11 @@ function runTests(test) { test.run(); } -const nodeTests = suite('Node.js', { +runTests(suite('Node.js', { cryptoRandomString: nodeCryptoRandomString, cryptoRandomStringAsync: nodeCryptoRandomStringAsync, -}); - -const browserTests = suite('Browser', { +})); +runTests(suite('Browser', { cryptoRandomString: browserCryptoRandomString, cryptoRandomStringAsync: browserCryptoRandomStringAsync, -}); - -runTests(nodeTests); -runTests(browserTests); +})); From 12c3d20d4441386ab319761a8fd34d94399cd617 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 21 Apr 2022 16:16:22 +1200 Subject: [PATCH 05/16] Tweak Signed-off-by: Richie Bendall --- core.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core.js b/core.js index 6b0d5b1..94f8c75 100644 --- a/core.js +++ b/core.js @@ -4,7 +4,7 @@ const distinguishableCharacters = [...'CDEHKMPRTUWXY012458']; const asciiPrintableCharacters = [...'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~']; const alphanumericCharacters = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789']; -const readUInt16LE = (uInt8Array, offset) => uInt8Array[offset] + (uInt8Array[offset + 1] * (2 ** 8)); +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 From 4f69e0bc9352d355e5979aca98d647fd17d9a5ff Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 21 Apr 2022 16:21:35 +1200 Subject: [PATCH 06/16] Add todo Signed-off-by: Richie Bendall --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 3bf6906..e86d91e 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ +// TODO: When targeting Node.js 16, use `crypto.webcrypto.getRandomValues` to interop with the browser code and when targeting Node.js 18, only use the browser code import {promisify} from 'node:util'; import crypto from 'node:crypto'; import {createStringGenerator, createAsyncStringGenerator} from './core.js'; From 819c9ba4c03ddb30d4f105273b7a692d918e5d37 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 21 Apr 2022 16:28:08 +1200 Subject: [PATCH 07/16] Import destructuring Signed-off-by: Richie Bendall --- index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index e86d91e..14f7449 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,11 @@ -// TODO: When targeting Node.js 16, use `crypto.webcrypto.getRandomValues` to interop with the browser code and when targeting Node.js 18, only use the browser code +// TODO: When targeting Node.js 16, use `crypto.webcrypto.getRandomValues` to interop with the browser code and 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); -export default createStringGenerator((byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length), size => new Uint8Array(crypto.randomBytes(size))); +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); From a8edd5e1a3d1d25bd9a71f2f6136b04c49787e5f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 21 Apr 2022 16:29:24 +1200 Subject: [PATCH 08/16] Reword todo Signed-off-by: Richie Bendall --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 14f7449..266127a 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ -// TODO: When targeting Node.js 16, use `crypto.webcrypto.getRandomValues` to interop with the browser code and when targeting Node.js 18, only use the browser code. +// 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 {randomBytes} from 'node:crypto'; import {createStringGenerator, createAsyncStringGenerator} from './core.js'; From 0d66f3c6959e74e733674756b324a7ee8dcb693b Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 21 Apr 2022 20:32:48 +1200 Subject: [PATCH 09/16] Update package.json Co-authored-by: Sindre Sorhus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 185b2da..152a6e5 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "browser": "./browser.js" }, "engines": { - "node": ">=14" + "node": ">=14.16" }, "scripts": { "test": "xo && uvu && tsd" From 85fe77b5bf7030dccdbd8d91797912b54150e57d Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 21 Apr 2022 20:41:01 +1200 Subject: [PATCH 10/16] Rename variable Signed-off-by: Richie Bendall --- test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.js b/test.js index fe205ea..08176cb 100644 --- a/test.js +++ b/test.js @@ -16,8 +16,8 @@ function runTests(test) { const length = targetSize * 640; const string = cryptoRandomString({...options, length}); - for (let i = 0; i < length; i++) { - set.add(string[i]); + for (let index = 0; index < length; index++) { + set.add(string[index]); } return set.size; From 7a4577dfd49ef3505cc6a079edf73fddd39aa344 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Thu, 21 Apr 2022 22:09:58 +1200 Subject: [PATCH 11/16] Move to `ava` Signed-off-by: Richie Bendall --- package.json | 4 +- test.js | 219 +++++++++++++++++++++++++-------------------------- 2 files changed, 110 insertions(+), 113 deletions(-) diff --git a/package.json b/package.json index 152a6e5..cba46c5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "node": ">=14.16" }, "scripts": { - "test": "xo && uvu && tsd" + "test": "xo && ava && tsd" }, "files": [ "index.js", @@ -48,9 +48,9 @@ "type-fest": "^2.12.2" }, "devDependencies": { + "ava": "^4.2.0", "dot-prop": "^7.2.0", "tsd": "^0.20.0", - "uvu": "^0.5.3", "xo": "^0.48.0" } } diff --git a/test.js b/test.js index 08176cb..37dd76d 100644 --- a/test.js +++ b/test.js @@ -1,7 +1,6 @@ import {webcrypto} from 'node:crypto'; import {hasProperty, setProperty} from 'dot-prop'; -import {suite} from 'uvu'; -import {is, match, throws} from 'uvu/assert'; // eslint-disable-line node/file-extension-in-import +import test from 'ava'; import browserCryptoRandomString, {cryptoRandomStringAsync as browserCryptoRandomStringAsync} from './browser.js'; import nodeCryptoRandomString, {cryptoRandomStringAsync as nodeCryptoRandomStringAsync} from './index.js'; @@ -9,131 +8,129 @@ if (!hasProperty(globalThis, 'crypto.getRandomValues')) { setProperty(globalThis, 'crypto.getRandomValues', webcrypto.getRandomValues); } -function runTests(test) { - // Probabilistic, result is always less than or equal to actual set size, chance it is less is below 1e-256 for sizes up to 32656. - const generatedCharacterSetSize = (cryptoRandomString, options, targetSize) => { - const set = new Set(); - const length = targetSize * 640; - const string = cryptoRandomString({...options, length}); - - for (let index = 0; index < length; index++) { - set.add(string[index]); - } - - return set.size; - }; - - test('main', ({cryptoRandomString}) => { - is(cryptoRandomString({length: 0}).length, 0); - is(cryptoRandomString({length: 10}).length, 10); - is(cryptoRandomString({length: 100}).length, 100); - match(cryptoRandomString({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize(cryptoRandomString, {}, 16), 16); - }); +// Probabilistic, result is always less than or equal to actual set size, chance it is less is below 1e-256 for sizes up to 32656. +const generatedCharacterSetSize = (cryptoRandomString, options, targetSize) => { + const set = new Set(); + const length = targetSize * 640; + const string = cryptoRandomString({...options, length}); - test('async', async ({cryptoRandomStringAsync}) => { - /* eslint-disable unicorn/no-await-expression-member */ - is((await cryptoRandomStringAsync({length: 0})).length, 0); - is((await cryptoRandomStringAsync({length: 10})).length, 10); - is((await cryptoRandomStringAsync({length: 100})).length, 100); - /* eslint-enable unicorn/no-await-expression-member */ - match(await cryptoRandomStringAsync({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic - }); + for (let index = 0; index < length; index++) { + set.add(string[index]); + } - test('hex', ({cryptoRandomString}) => { - is(cryptoRandomString({length: 0, type: 'hex'}).length, 0); - is(cryptoRandomString({length: 10, type: 'hex'}).length, 10); - is(cryptoRandomString({length: 100, type: 'hex'}).length, 100); - match(cryptoRandomString({length: 100, type: 'hex'}), /^[a-f\d]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize(cryptoRandomString, {type: 'hex'}, 16), 16); - }); + return set.size; +}; - test('base64', ({cryptoRandomString}) => { - is(cryptoRandomString({length: 0, type: 'base64'}).length, 0); - is(cryptoRandomString({length: 10, type: 'base64'}).length, 10); - is(cryptoRandomString({length: 100, type: 'base64'}).length, 100); - match(cryptoRandomString({length: 100, type: 'base64'}), /^[a-zA-Z\d/+]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize(cryptoRandomString, {type: 'base64'}, 64), 64); +function runTest(title, macro) { + test(`Node.js: ${title}`, macro, { + cryptoRandomString: nodeCryptoRandomString, + cryptoRandomStringAsync: nodeCryptoRandomStringAsync, }); - - test('url-safe', ({cryptoRandomString}) => { - is(cryptoRandomString({length: 0, type: 'url-safe'}).length, 0); - is(cryptoRandomString({length: 10, type: 'url-safe'}).length, 10); - is(cryptoRandomString({length: 100, type: 'url-safe'}).length, 100); - match(cryptoRandomString({length: 100, type: 'url-safe'}), /^[\w.~-]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize(cryptoRandomString, {type: 'url-safe'}, 66), 66); + test(`Browser: ${title}`, macro, { + cryptoRandomString: browserCryptoRandomString, + cryptoRandomStringAsync: browserCryptoRandomStringAsync, }); +} - test('numeric', ({cryptoRandomString}) => { - is(cryptoRandomString({length: 0, type: 'numeric'}).length, 0); - is(cryptoRandomString({length: 10, type: 'numeric'}).length, 10); - is(cryptoRandomString({length: 100, type: 'numeric'}).length, 100); - match(cryptoRandomString({length: 100, type: 'numeric'}), /^\d*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize(cryptoRandomString, {type: 'numeric'}, 10), 10); - }); +runTest('main', test.macro((t, {cryptoRandomString}) => { + t.is(cryptoRandomString({length: 0}).length, 0); + t.is(cryptoRandomString({length: 10}).length, 10); + t.is(cryptoRandomString({length: 100}).length, 100); + t.regex(cryptoRandomString({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic + t.is(generatedCharacterSetSize(cryptoRandomString, {}, 16), 16); +})); - test('distinguishable', ({cryptoRandomString}) => { - is(cryptoRandomString({length: 0, type: 'distinguishable'}).length, 0); - is(cryptoRandomString({length: 10, type: 'distinguishable'}).length, 10); - is(cryptoRandomString({length: 100, type: 'distinguishable'}).length, 100); - match(cryptoRandomString({length: 100, type: 'distinguishable'}), /^[CDEHKMPRTUWXY012458]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize(cryptoRandomString, {type: 'distinguishable'}, 19), 19); - }); +runTest('async', test.macro(async (t, {cryptoRandomStringAsync}) => { + /* eslint-disable unicorn/no-await-expression-member */ + t.is((await cryptoRandomStringAsync({length: 0})).length, 0); + t.is((await cryptoRandomStringAsync({length: 10})).length, 10); + t.is((await cryptoRandomStringAsync({length: 100})).length, 100); + /* eslint-enable unicorn/no-await-expression-member */ + t.regex(await cryptoRandomStringAsync({length: 100}), /^[a-f\d]*$/); // Sanity check, probabilistic +})); - test('ascii-printable', ({cryptoRandomString}) => { - is(cryptoRandomString({length: 0, type: 'ascii-printable'}).length, 0); - is(cryptoRandomString({length: 10, type: 'ascii-printable'}).length, 10); - is(cryptoRandomString({length: 100, type: 'ascii-printable'}).length, 100); - match(cryptoRandomString({length: 100, type: 'ascii-printable'}), /^[!"#$%&'()*+,-./\w:;<=>?@[\\\]^`{|}~]*$/); // Sanity check, probabilistic - }); +runTest('hex', test.macro((t, {cryptoRandomString}) => { + t.is(cryptoRandomString({length: 0, type: 'hex'}).length, 0); + t.is(cryptoRandomString({length: 10, type: 'hex'}).length, 10); + t.is(cryptoRandomString({length: 100, type: 'hex'}).length, 100); + t.regex(cryptoRandomString({length: 100, type: 'hex'}), /^[a-f\d]*$/); // Sanity check, probabilistic + t.is(generatedCharacterSetSize(cryptoRandomString, {type: 'hex'}, 16), 16); +})); - test('alphanumeric', ({cryptoRandomString}) => { - is(cryptoRandomString({length: 0, type: 'alphanumeric'}).length, 0); - is(cryptoRandomString({length: 10, type: 'alphanumeric'}).length, 10); - is(cryptoRandomString({length: 100, type: 'alphanumeric'}).length, 100); - match(cryptoRandomString({length: 100, type: 'alphanumeric'}), /^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize(cryptoRandomString, {type: 'alphanumeric'}, 19), 62); - }); +runTest('base64', test.macro((t, {cryptoRandomString}) => { + t.is(cryptoRandomString({length: 0, type: 'base64'}).length, 0); + t.is(cryptoRandomString({length: 10, type: 'base64'}).length, 10); + t.is(cryptoRandomString({length: 100, type: 'base64'}).length, 100); + t.regex(cryptoRandomString({length: 100, type: 'base64'}), /^[a-zA-Z\d/+]*$/); // Sanity check, probabilistic + t.is(generatedCharacterSetSize(cryptoRandomString, {type: 'base64'}, 64), 64); +})); - test('characters', ({cryptoRandomString}) => { - is(cryptoRandomString({length: 0, characters: '1234'}).length, 0); - is(cryptoRandomString({length: 10, characters: '1234'}).length, 10); - is(cryptoRandomString({length: 100, characters: '1234'}).length, 100); - match(cryptoRandomString({length: 100, characters: '1234'}), /^[1-4]*$/); // Sanity check, probabilistic - is(generatedCharacterSetSize(cryptoRandomString, {characters: '1234'}, 4), 4); - is(generatedCharacterSetSize(cryptoRandomString, {characters: '0123456789'}, 10), 10); - }); +runTest('url-safe', test.macro((t, {cryptoRandomString}) => { + t.is(cryptoRandomString({length: 0, type: 'url-safe'}).length, 0); + t.is(cryptoRandomString({length: 10, type: 'url-safe'}).length, 10); + t.is(cryptoRandomString({length: 100, type: 'url-safe'}).length, 100); + t.regex(cryptoRandomString({length: 100, type: 'url-safe'}), /^[\w.~-]*$/); // Sanity check, probabilistic + t.is(generatedCharacterSetSize(cryptoRandomString, {type: 'url-safe'}, 66), 66); +})); - test('argument errors', ({cryptoRandomString}) => { - throws(() => { - cryptoRandomString({length: Number.POSITIVE_INFINITY}); - }); +runTest('numeric', test.macro((t, {cryptoRandomString}) => { + t.is(cryptoRandomString({length: 0, type: 'numeric'}).length, 0); + t.is(cryptoRandomString({length: 10, type: 'numeric'}).length, 10); + t.is(cryptoRandomString({length: 100, type: 'numeric'}).length, 100); + t.regex(cryptoRandomString({length: 100, type: 'numeric'}), /^\d*$/); // Sanity check, probabilistic + t.is(generatedCharacterSetSize(cryptoRandomString, {type: 'numeric'}, 10), 10); +})); - throws(() => { - cryptoRandomString({length: -1}); - }); +runTest('distinguishable', test.macro((t, {cryptoRandomString}) => { + t.is(cryptoRandomString({length: 0, type: 'distinguishable'}).length, 0); + t.is(cryptoRandomString({length: 10, type: 'distinguishable'}).length, 10); + t.is(cryptoRandomString({length: 100, type: 'distinguishable'}).length, 100); + t.regex(cryptoRandomString({length: 100, type: 'distinguishable'}), /^[CDEHKMPRTUWXY012458]*$/); // Sanity check, probabilistic + t.is(generatedCharacterSetSize(cryptoRandomString, {type: 'distinguishable'}, 19), 19); +})); - throws(() => { - cryptoRandomString({length: 0, type: 'hex', characters: '1234'}); - }); +runTest('ascii-printable', test.macro((t, {cryptoRandomString}) => { + t.is(cryptoRandomString({length: 0, type: 'ascii-printable'}).length, 0); + t.is(cryptoRandomString({length: 10, type: 'ascii-printable'}).length, 10); + t.is(cryptoRandomString({length: 100, type: 'ascii-printable'}).length, 100); + t.regex(cryptoRandomString({length: 100, type: 'ascii-printable'}), /^[!"#$%&'()*+,-./\w:;<=>?@[\\\]^`{|}~]*$/); // Sanity check, probabilistic +})); - throws(() => { - cryptoRandomString({length: 0, characters: 42}); - }); +runTest('alphanumeric', test.macro((t, {cryptoRandomString}) => { + t.is(cryptoRandomString({length: 0, type: 'alphanumeric'}).length, 0); + t.is(cryptoRandomString({length: 10, type: 'alphanumeric'}).length, 10); + t.is(cryptoRandomString({length: 100, type: 'alphanumeric'}).length, 100); + t.regex(cryptoRandomString({length: 100, type: 'alphanumeric'}), /^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]*$/); // Sanity check, probabilistic + t.is(generatedCharacterSetSize(cryptoRandomString, {type: 'alphanumeric'}, 19), 62); +})); - throws(() => { - cryptoRandomString({length: 0, type: 'unknown'}); - }); +runTest('characters', test.macro((t, {cryptoRandomString}) => { + t.is(cryptoRandomString({length: 0, characters: '1234'}).length, 0); + t.is(cryptoRandomString({length: 10, characters: '1234'}).length, 10); + t.is(cryptoRandomString({length: 100, characters: '1234'}).length, 100); + t.regex(cryptoRandomString({length: 100, characters: '1234'}), /^[1-4]*$/); // Sanity check, probabilistic + t.is(generatedCharacterSetSize(cryptoRandomString, {characters: '1234'}, 4), 4); + t.is(generatedCharacterSetSize(cryptoRandomString, {characters: '0123456789'}, 10), 10); +})); + +runTest('argument errors', test.macro((t, {cryptoRandomString}) => { + t.throws(() => { + cryptoRandomString({length: Number.POSITIVE_INFINITY}); }); - test.run(); -} + t.throws(() => { + cryptoRandomString({length: -1}); + }); -runTests(suite('Node.js', { - cryptoRandomString: nodeCryptoRandomString, - cryptoRandomStringAsync: nodeCryptoRandomStringAsync, -})); -runTests(suite('Browser', { - cryptoRandomString: browserCryptoRandomString, - cryptoRandomStringAsync: browserCryptoRandomStringAsync, + t.throws(() => { + cryptoRandomString({length: 0, type: 'hex', characters: '1234'}); + }); + + t.throws(() => { + cryptoRandomString({length: 0, characters: 42}); + }); + + t.throws(() => { + cryptoRandomString({length: 0, type: 'unknown'}); + }); })); From 892a397d30b942f9d04932dc3c3611beaa897428 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sat, 23 Apr 2022 22:31:55 +0700 Subject: [PATCH 12/16] Update main.yml --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a2c4c4a..d9e3e00 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,6 +11,7 @@ jobs: matrix: node-version: - 16 + - 14 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 From 5c9649c62c0a723f5908222545ff7d58f0ec7f90 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sat, 23 Apr 2022 22:33:48 +0700 Subject: [PATCH 13/16] Update main.yml --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9e3e00..a2c4c4a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,6 @@ jobs: matrix: node-version: - 16 - - 14 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 From 2a89372802910ef4e8f62ceac66a06145d4d7b51 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 10 May 2022 00:50:48 +1200 Subject: [PATCH 14/16] Synchronise --- readme.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/readme.md b/readme.md index ce9bf26..ee3ac15 100644 --- a/readme.md +++ b/readme.md @@ -58,6 +58,13 @@ For most use-cases, there's really no good reason to use this async version. Fro In general, anything async comes with some overhead on it's own. +```js +import {cryptoRandomStringAsync} from 'crypto-random-string'; + +await cryptoRandomStringAsync({length: 10}); +//=> '2cf05d94db' +``` + #### options Type: `object` From 95aa3993d74a548faeac2c9dc4a7593e95aeac4f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 10 May 2022 00:56:56 +1200 Subject: [PATCH 15/16] Update test.js --- test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.js b/test.js index 37dd76d..7a904b1 100644 --- a/test.js +++ b/test.js @@ -4,8 +4,8 @@ import test from 'ava'; import browserCryptoRandomString, {cryptoRandomStringAsync as browserCryptoRandomStringAsync} from './browser.js'; import nodeCryptoRandomString, {cryptoRandomStringAsync as nodeCryptoRandomStringAsync} from './index.js'; -if (!hasProperty(globalThis, 'crypto.getRandomValues')) { - setProperty(globalThis, 'crypto.getRandomValues', webcrypto.getRandomValues); +if (!hasProperty(globalThis, 'crypto')) { + setProperty(globalThis, 'crypto', webcrypto); } // Probabilistic, result is always less than or equal to actual set size, chance it is less is below 1e-256 for sizes up to 32656. From b852a2880a0c5d0059323885b70e0caee97cbc05 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 10 May 2022 13:09:02 +0700 Subject: [PATCH 16/16] Update main.yml --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a2c4c4a..6a82b18 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,7 @@ jobs: fail-fast: false matrix: node-version: + - 18 - 16 steps: - uses: actions/checkout@v3