random-stringinizer
Version:
Generate Random Crypto String with ease!
147 lines (114 loc) • 5.22 kB
JavaScript
'use-strict';
const {promisify} = require('util');
const crypto = require('crypto');
const randomBytesAsync = promisify(crypto.randomBytes);
const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split('');
const numeric = '0123456789'.split('');
const distinguishable = 'CDEHKMPRTUWXY012458'.split('');
const asciiPrintable = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.split('');
const alphanumeric = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('');
const generateForCustomCharacters = (length, characters) => {
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 buffer = await randomBytesAsync(byteLength);
return buffer.toString(type).slice(0, length);
};
const allowedTypes = [
undefined,
'hexadecimal',
'base64',
'url',
'numeric',
'distinct',
'ascii',
'alphanumeric'
];
const createGenerator = (generateForCustomCharacters, generateRandomBytes) => ({len, typeOf, char}) => {
if (!(len >= 0 && Number.isFinite(len))) {
throw new TypeError('Expected a `length` to be a non-negative finite number');
}
if (typeOf !== undefined && char !== undefined) {
throw new TypeError('Expected either `type` or `characters`');
}
if (char !== undefined && typeof char !== 'string') {
throw new TypeError('Expected `characters` to be string');
}
if (!allowedTypes.includes(typeOf)) {
throw new TypeError(`Unknown type: ${type}`);
}
if (typeOf === undefined && char === undefined) {
typeOf = 'hex';
}
if (typeOf === 'hexadecimal' || (typeOf === undefined && char === undefined)) {
return generateRandomBytes(Math.ceil(len * 0.5), 'hex', len); // Need 0.5 byte entropy per character
}
if (typeOf === 'base64') {
return generateRandomBytes(Math.ceil(len * 0.75), 'base64', len); // Need 0.75 byte of entropy per character
}
if (typeOf === 'url') {
return generateForCustomCharacters(len, urlSafeCharacters);
}
if (typeOf === 'numeric') {
return generateForCustomCharacters(len, numeric);
}
if (typeOf === 'distinct') {
return generateForCustomCharacters(len, distinguishable);
}
if (typeOf === 'ascii') {
return generateForCustomCharacters(len, asciiPrintable);
}
if (typeOf === 'alphanumeric') {
return generateForCustomCharacters(len, alphanumeric);
}
if (char.length === 0) {
throw new TypeError('Expected `characters` string length to be greater than or equal to 1');
}
if (char.length > 0x10000) {
throw new TypeError('Expected `characters` string length to be less or equal to 65536');
}
return generateForCustomCharacters(len, char.split(''));
};
module.exports = createGenerator(generateForCustomCharacters, generateRandomBytes);
module.exports.async = createGenerator(generateForCustomCharactersAsync, generateRandomBytesAsync);