-
-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
197 additions
and
144 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* 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)); | ||
|
||
// `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; | ||
|
||
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)); // eslint-disable-line unicorn/no-array-reduce | ||
let currentIndex = 0; | ||
|
||
for (const bytes of generatedBytes) { | ||
result.set(bytes, currentIndex); | ||
currentIndex += bytes.byteLength; | ||
} | ||
|
||
return result; | ||
} | ||
|
||
function specialRandomBytes(byteLength, type, length) { | ||
const generatedBytes = getRandomValues(byteLength); | ||
const convert = type === 'hex' ? toHex : toBase64; | ||
|
||
return convert(generatedBytes).slice(0, length); | ||
} | ||
|
||
const cryptoRandomString = createStringGenerator(specialRandomBytes, getRandomValues); | ||
cryptoRandomString.async = createAsyncStringGenerator(specialRandomBytes, getRandomValues); | ||
|
||
export default cryptoRandomString; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split(''); | ||
const numericCharacters = '0123456789'.split(''); | ||
const distinguishableCharacters = 'CDEHKMPRTUWXY012458'.split(''); | ||
const asciiPrintableCharacters = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.split(''); | ||
const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split(''); | ||
|
||
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 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; | ||
|
||
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; | ||
} | ||
|
||
string += characters[entropyValue % characterCount]; | ||
stringLength++; | ||
} | ||
} | ||
|
||
return string; | ||
}; | ||
|
||
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 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 = await randomBytesAsync(entropyLength); // eslint-disable-line no-await-in-loop | ||
let entropyPosition = 0; | ||
|
||
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; | ||
} | ||
|
||
string += characters[entropyValue % characterCount]; | ||
stringLength++; | ||
} | ||
} | ||
|
||
return string; | ||
}; | ||
|
||
const allowedTypes = new Set([ | ||
undefined, | ||
'hex', | ||
'base64', | ||
'url-safe', | ||
'numeric', | ||
'distinguishable', | ||
'ascii-printable', | ||
'alphanumeric' | ||
]); | ||
|
||
const createGenerator = (generateForCustomCharacters, specialRandomBytes, randomBytes) => ({length, type, characters}) => { | ||
if (!(length >= 0 && Number.isFinite(length))) { | ||
throw new TypeError('Expected a `length` to be a non-negative finite number'); | ||
} | ||
|
||
if (type !== undefined && characters !== undefined) { | ||
throw new TypeError('Expected either `type` or `characters`'); | ||
} | ||
|
||
if (characters !== undefined && typeof characters !== 'string') { | ||
throw new TypeError('Expected `characters` to be string'); | ||
} | ||
|
||
if (!allowedTypes.has(type)) { | ||
throw new TypeError(`Unknown type: ${type}`); | ||
} | ||
|
||
if (type === undefined && characters === undefined) { | ||
type = 'hex'; | ||
} | ||
|
||
if (type === 'hex' || (type === undefined && characters === undefined)) { | ||
return specialRandomBytes(Math.ceil(length * 0.5), 'hex', length); // Needs 0.5 bytes of entropy per character | ||
} | ||
|
||
if (type === 'base64') { | ||
return specialRandomBytes(Math.ceil(length * 0.75), 'base64', length); // Needs 0.75 bytes of entropy per character | ||
} | ||
|
||
if (type === 'url-safe') { | ||
return generateForCustomCharacters(length, urlSafeCharacters, randomBytes); | ||
} | ||
|
||
if (type === 'numeric') { | ||
return generateForCustomCharacters(length, numericCharacters, randomBytes); | ||
} | ||
|
||
if (type === 'distinguishable') { | ||
return generateForCustomCharacters(length, distinguishableCharacters, randomBytes); | ||
} | ||
|
||
if (type === 'ascii-printable') { | ||
return generateForCustomCharacters(length, asciiPrintableCharacters, randomBytes); | ||
} | ||
|
||
if (type === 'alphanumeric') { | ||
return generateForCustomCharacters(length, alphanumericCharacters, randomBytes); | ||
} | ||
|
||
if (characters.length === 0) { | ||
throw new TypeError('Expected `characters` string length to be greater than or equal to 1'); | ||
} | ||
|
||
if (characters.length > 0x10000) { | ||
throw new TypeError('Expected `characters` string length to be less or equal to 65536'); | ||
} | ||
|
||
return generateForCustomCharacters(length, characters.split(''), randomBytes); | ||
}; | ||
|
||
export function createStringGenerator(specialRandomBytes, randomBytes) { | ||
return createGenerator(generateForCustomCharacters, specialRandomBytes, randomBytes); | ||
} | ||
|
||
export function createAsyncStringGenerator(specialRandomBytesAsync, randomBytesAsync) { | ||
return createGenerator(generateForCustomCharactersAsync, specialRandomBytesAsync, randomBytesAsync); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,148 +1,13 @@ | ||
import {promisify} from 'util'; | ||
import crypto from 'crypto'; | ||
import {promisify} from 'node:util'; | ||
import crypto from 'node:crypto'; | ||
import {createStringGenerator, createAsyncStringGenerator} from './core.js'; | ||
|
||
const randomBytesAsync = promisify(crypto.randomBytes); | ||
|
||
const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split(''); | ||
const numericCharacters = '0123456789'.split(''); | ||
const distinguishableCharacters = 'CDEHKMPRTUWXY012458'.split(''); | ||
const asciiPrintableCharacters = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.split(''); | ||
const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split(''); | ||
|
||
const generateForCustomCharacters = (length, characters) => { | ||
// 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 = crypto.randomBytes(entropyLength); | ||
let entropyPosition = 0; | ||
|
||
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; | ||
} | ||
|
||
string += characters[entropyValue % characterCount]; | ||
stringLength++; | ||
} | ||
} | ||
|
||
return string; | ||
}; | ||
|
||
const generateForCustomCharactersAsync = async (length, characters) => { | ||
// 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 = await randomBytesAsync(entropyLength); // eslint-disable-line no-await-in-loop | ||
let entropyPosition = 0; | ||
|
||
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; | ||
} | ||
|
||
string += characters[entropyValue % characterCount]; | ||
stringLength++; | ||
} | ||
} | ||
|
||
return string; | ||
}; | ||
|
||
const generateRandomBytes = (byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length); | ||
|
||
const generateRandomBytesAsync = async (byteLength, type, length) => { | ||
const cryptoRandomString = createStringGenerator((byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length), crypto.randomBytes); | ||
cryptoRandomString.async = createAsyncStringGenerator(async (byteLength, type, length) => { | ||
const buffer = await randomBytesAsync(byteLength); | ||
return buffer.toString(type).slice(0, length); | ||
}; | ||
|
||
const allowedTypes = new Set([ | ||
undefined, | ||
'hex', | ||
'base64', | ||
'url-safe', | ||
'numeric', | ||
'distinguishable', | ||
'ascii-printable', | ||
'alphanumeric' | ||
]); | ||
|
||
const createGenerator = (generateForCustomCharacters, generateRandomBytes) => ({length, type, characters}) => { | ||
if (!(length >= 0 && Number.isFinite(length))) { | ||
throw new TypeError('Expected a `length` to be a non-negative finite number'); | ||
} | ||
|
||
if (type !== undefined && characters !== undefined) { | ||
throw new TypeError('Expected either `type` or `characters`'); | ||
} | ||
|
||
if (characters !== undefined && typeof characters !== 'string') { | ||
throw new TypeError('Expected `characters` to be string'); | ||
} | ||
|
||
if (!allowedTypes.has(type)) { | ||
throw new TypeError(`Unknown type: ${type}`); | ||
} | ||
|
||
if (type === undefined && characters === undefined) { | ||
type = 'hex'; | ||
} | ||
|
||
if (type === 'hex' || (type === undefined && characters === undefined)) { | ||
return generateRandomBytes(Math.ceil(length * 0.5), 'hex', length); // Need 0.5 byte entropy per character | ||
} | ||
|
||
if (type === 'base64') { | ||
return generateRandomBytes(Math.ceil(length * 0.75), 'base64', length); // Need 0.75 byte of entropy per character | ||
} | ||
|
||
if (type === 'url-safe') { | ||
return generateForCustomCharacters(length, urlSafeCharacters); | ||
} | ||
|
||
if (type === 'numeric') { | ||
return generateForCustomCharacters(length, numericCharacters); | ||
} | ||
|
||
if (type === 'distinguishable') { | ||
return generateForCustomCharacters(length, distinguishableCharacters); | ||
} | ||
|
||
if (type === 'ascii-printable') { | ||
return generateForCustomCharacters(length, asciiPrintableCharacters); | ||
} | ||
|
||
if (type === 'alphanumeric') { | ||
return generateForCustomCharacters(length, alphanumericCharacters); | ||
} | ||
|
||
if (characters.length === 0) { | ||
throw new TypeError('Expected `characters` string length to be greater than or equal to 1'); | ||
} | ||
|
||
if (characters.length > 0x10000) { | ||
throw new TypeError('Expected `characters` string length to be less or equal to 65536'); | ||
} | ||
|
||
return generateForCustomCharacters(length, characters.split('')); | ||
}; | ||
|
||
const cryptoRandomString = createGenerator(generateForCustomCharacters, generateRandomBytes); | ||
|
||
cryptoRandomString.async = createGenerator(generateForCustomCharactersAsync, generateRandomBytesAsync); | ||
}, randomBytesAsync); | ||
|
||
export default cryptoRandomString; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters