diff --git a/README.md b/README.md index c12ae60..56ab302 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ Deno module based on [crypto-random-string](https://github.com/sindresorhus/cryp ## Import Module ```typescript -import { cryptoRandomString, cryptoRandomStringAsync } from "https://deno.land/x/crypto_random_string@1.0.0/mod.ts" +import { cryptoRandomString } from "https://deno.land/x/crypto_random_string@1.0.0/mod.ts" // or -import { cryptoRandomString, cryptoRandomStringAsync } from "https://github.com/piyush-bhatt/crypto-random-string/raw/main/mod.ts" +import { cryptoRandomString } from "https://github.com/piyush-bhatt/crypto-random-string/raw/main/mod.ts" ``` ## Usage @@ -21,36 +21,20 @@ import { cryptoRandomString, cryptoRandomStringAsync } from "https://github.com/ cryptoRandomString({length: 10}); // '0696cb9e70' -await cryptoRandomStringAsync({length: 10}); // 'c8d4b0140d' - cryptoRandomString({length: 10, type: 'base64'}); // 'dw3mgWC5uO' -await cryptoRandomStringAsync({length: 10, type: 'base64'}); // 'k6ALljZx+E' - cryptoRandomString({length: 10, type: 'url-safe'}); // '0pN1Y2Jz.X' -await cryptoRandomStringAsync({length: 10, type: 'url-safe'}); // '7.F5oBY9Qy' - cryptoRandomString({length: 10, type: 'numeric'}); // '1639380067' -await cryptoRandomStringAsync({length: 10, type: 'numeric'}); // '0923903115' - cryptoRandomString({length: 6, type: 'distinguishable'}); // 'H4HH5D' -await cryptoRandomStringAsync({length: 6, type: 'distinguishable'}); // 'D2Y254' - cryptoRandomString({length: 10, type: 'ascii-printable'}); // '#I&J.GP./9' -await cryptoRandomStringAsync({length: 10, type: 'ascii-printable'}); // '7t%FxZkyL(' - cryptoRandomString({length: 10, type: 'alphanumeric'}); // 'ZtgC2J6aU5' -await cryptoRandomStringAsync({length: 10, type: 'alphanumeric'}); // 'FELQVN9S8H' - cryptoRandomString({length: 10, characters: 'abc'}); // 'abcabccbcc' -await cryptoRandomStringAsync({length: 10, characters: 'abc'}); // 'abcbbbacbb' - ``` ## API @@ -59,9 +43,6 @@ await cryptoRandomStringAsync({length: 10, characters: 'abc'}); // 'abcbbbacbb' Returns a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default. -### cryptoRandomStringAsync(options) - -Returns a promise which resolves to a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default. #### options diff --git a/cryptoRandomString.ts b/cryptoRandomString.ts index 93deede..a88d8e9 100644 --- a/cryptoRandomString.ts +++ b/cryptoRandomString.ts @@ -1,5 +1,3 @@ -import { randomBytes } from "./deps.ts"; -import { promisify } from "./deps.ts"; import { ALLOWED_TYPES, ALPHANUMERIC_CHARACTERS, @@ -8,40 +6,49 @@ import { NUMERIC_CHARACTERS, URL_SAFE_CHARACTERS, } from "./constants.ts"; +import { encodeToBase64, encodeToHex } from "./deps.ts"; -const randomBytesAsync = promisify(randomBytes); - -const generateForCustomCharacters = (length: number, characters: string[]) => { - // 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 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; - - while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it - const entropy = randomBytes(entropyLength); - let entropyPosition = 0; +export interface GenerateRandomBytes { + ( + byteLength: number, + type: "hex" | "base64", + length: number, + ): string; +} + +export interface GenerateForCustomCharacters { + (length: number, characters: string[]): string; +} + +export const MAX_RANDOM_VALUES = 65536; +export const MAX_SIZE = 4294967295; + +function randomBytes(size: number) { + if (size > MAX_SIZE) { + throw new RangeError( + `The value of "size" is out of range. It must be >= 0 && <= ${MAX_SIZE}. Received ${size}`, + ); + } - while (entropyPosition < entropyLength && stringLength < length) { - const entropyValue = entropy.readUInt16LE(entropyPosition); - entropyPosition += 2; - if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division - continue; - } + const bytes = new Uint8Array(size); - string += characters[entropyValue % characterCount]; - stringLength++; + //Work around for getRandomValues max generation + if (size > MAX_RANDOM_VALUES) { + for (let generated = 0; generated < size; generated += MAX_RANDOM_VALUES) { + crypto.getRandomValues( + bytes.subarray(generated, generated + MAX_RANDOM_VALUES), + ); } + } else { + crypto.getRandomValues(bytes); } - return string; -}; + return bytes; +} -const generateForCustomCharactersAsync = async ( - length: number, - characters: string[], +const generateForCustomCharacters: GenerateForCustomCharacters = ( + length, + characters, ) => { // Generating entropy is faster than complex math operations, so we use the simplest way const characterCount = characters.length; @@ -52,11 +59,16 @@ const generateForCustomCharactersAsync = async ( let stringLength = 0; while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it - const entropy = await randomBytesAsync(entropyLength); // eslint-disable-line no-await-in-loop + const entropy = randomBytes(entropyLength); let entropyPosition = 0; while (entropyPosition < entropyLength && stringLength < length) { - const entropyValue = entropy.readUInt16LE(entropyPosition); + const entropyValue = new DataView( + entropy.buffer, + entropy.byteOffset, + entropy.byteLength, + ) + .getUint16(entropyPosition, true); entropyPosition += 2; if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division continue; @@ -70,24 +82,15 @@ const generateForCustomCharactersAsync = async ( return string; }; -const generateRandomBytes = ( - byteLength: number, - type: string, - length: number, -) => randomBytes(byteLength).toString(type).slice(0, length); - -const generateRandomBytesAsync = async ( - byteLength: number, - type: string, - length: number, -) => { - const buffer = await randomBytesAsync(byteLength); - return buffer.toString(type).slice(0, length); +const generateRandomBytes: GenerateRandomBytes = (byteLength, type, length) => { + const bytes = randomBytes(byteLength); + const str = type === "base64" ? encodeToBase64(bytes) : encodeToHex(bytes); + return str.slice(0, length); }; const createGenerator = ( - generateForCustomCharacters: Function, - generateRandomBytes: Function, + generateForCustomCharacters: GenerateForCustomCharacters, + generateRandomBytes: GenerateRandomBytes, ) => ( { length, type, characters }: { @@ -165,9 +168,5 @@ const cryptoRandomString = createGenerator( generateForCustomCharacters, generateRandomBytes, ); -const cryptoRandomStringAsync = createGenerator( - generateForCustomCharactersAsync, - generateRandomBytesAsync, -); -export { cryptoRandomString, cryptoRandomStringAsync }; +export { cryptoRandomString }; diff --git a/deps.ts b/deps.ts index 34d2286..b081789 100644 --- a/deps.ts +++ b/deps.ts @@ -1,2 +1,2 @@ -export { randomBytes } from "https://deno.land/std@0.83.0/node/crypto.ts"; -export { promisify } from "https://deno.land/std@0.83.0/node/util.ts"; +export { encodeToString as encodeToHex } from "https://deno.land/std@0.99.0/encoding/hex.ts"; +export { encode as encodeToBase64 } from "https://deno.land/std@0.99.0/encoding/base64.ts"; diff --git a/mod_test.ts b/mod_test.ts index 4c5507a..39a9f94 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -1,5 +1,5 @@ import { assertEquals, assertMatch, assertThrows } from "./test_deps.ts"; -import { cryptoRandomString, cryptoRandomStringAsync } from "./mod.ts"; +import { cryptoRandomString } from "./mod.ts"; // 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 = ( @@ -25,13 +25,6 @@ Deno.test("main", () => { assertEquals(generatedCharacterSetSize({}, 16), 16); }); -Deno.test("async", async () => { - assertEquals((await cryptoRandomStringAsync({ length: 0 })).length, 0); - assertEquals((await cryptoRandomStringAsync({ length: 10 })).length, 10); - assertEquals((await cryptoRandomStringAsync({ length: 100 })).length, 100); - assertMatch(await cryptoRandomStringAsync({ length: 100 }), /^[a-f\d]*$/); -}); - Deno.test("hex", () => { assertEquals(cryptoRandomString({ length: 0, type: "hex" }).length, 0); assertEquals(cryptoRandomString({ length: 10, type: "hex" }).length, 10);