add option to randomize keys
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
||||
import {uint8ArrayToBase64, uint8ArrayToHex} from 'uint8array-extras';
|
||||
|
||||
// `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 maxBytesPerRequest = 65_536;
|
||||
|
||||
// Each character is picked with a single `Uint16` selector value, so a character set cannot be larger than the range of one.
|
||||
const maxCharacterSetSize = 0x1_00_00;
|
||||
|
||||
function fillWithRandomValues(typedArray) {
|
||||
const maxElementsPerRequest = maxBytesPerRequest / typedArray.BYTES_PER_ELEMENT;
|
||||
|
||||
for (let offset = 0; offset < typedArray.length; offset += maxElementsPerRequest) {
|
||||
// `TypedArray#subarray` clamps the end index and shares the underlying buffer, so this fills the array in place without any copying.
|
||||
crypto.getRandomValues(typedArray.subarray(offset, offset + maxElementsPerRequest));
|
||||
}
|
||||
|
||||
return typedArray;
|
||||
}
|
||||
|
||||
const randomBytes = byteLength => fillWithRandomValues(new Uint8Array(byteLength));
|
||||
|
||||
const generateForCustomCharacters = (length, characters) => {
|
||||
// Generating entropy is faster than complex math operations, so we use the simplest way
|
||||
const characterCount = characters.length;
|
||||
const validSelectorCount = Math.floor(maxCharacterSetSize / characterCount) * characterCount; // Using values at or above this will ruin distribution when using modular division
|
||||
// Generating a bit more than required, adjusted for how many values we expect to discard, so we usually only need one pass
|
||||
const entropyLength = Math.ceil(1.1 * length * (maxCharacterSetSize / validSelectorCount));
|
||||
let string = '';
|
||||
let stringLength = 0;
|
||||
|
||||
while (stringLength < length) {
|
||||
const entropy = fillWithRandomValues(new Uint16Array(entropyLength));
|
||||
|
||||
for (let index = 0; index < entropyLength; index++) {
|
||||
const entropyValue = entropy[index];
|
||||
|
||||
if (entropyValue < validSelectorCount) { // Skip values which will ruin distribution when using modular division
|
||||
string += characters[entropyValue % characterCount];
|
||||
stringLength++;
|
||||
|
||||
if (stringLength === length) {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string;
|
||||
};
|
||||
|
||||
const characterSets = new Map([
|
||||
['url-safe', [...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~']],
|
||||
['numeric', [...'0123456789']],
|
||||
['distinguishable', [...'CDEHKMPRTUWXY012458']],
|
||||
['ascii-printable', [...'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~']],
|
||||
['alphanumeric', [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789']],
|
||||
]);
|
||||
|
||||
const allowedTypes = new Set(['hex', 'base64', ...characterSets.keys()]);
|
||||
|
||||
export default function cryptoRandomString({length, type, characters}) {
|
||||
if (!Number.isSafeInteger(length) || length < 0) {
|
||||
throw new TypeError('Expected `length` to be a non-negative integer');
|
||||
}
|
||||
|
||||
if (type !== undefined && characters !== undefined) {
|
||||
throw new TypeError('Expected either `type` or `characters`');
|
||||
}
|
||||
|
||||
if (characters !== undefined) {
|
||||
if (typeof characters !== 'string') {
|
||||
throw new TypeError('Expected `characters` to be a string');
|
||||
}
|
||||
|
||||
// Spread to keep characters outside the Basic Multilingual Plane, like emoji, intact.
|
||||
const customCharacterSet = [...characters];
|
||||
|
||||
if (customCharacterSet.length === 0) {
|
||||
throw new TypeError('Expected `characters` to contain at least 1 character');
|
||||
}
|
||||
|
||||
if (customCharacterSet.length > maxCharacterSetSize) {
|
||||
throw new TypeError(`Expected \`characters\` to contain at most ${maxCharacterSetSize} characters, got ${customCharacterSet.length}`);
|
||||
}
|
||||
|
||||
return generateForCustomCharacters(length, customCharacterSet);
|
||||
}
|
||||
|
||||
if (type !== undefined && !allowedTypes.has(type)) {
|
||||
throw new TypeError(`Unknown type: ${type}`);
|
||||
}
|
||||
|
||||
const characterSet = characterSets.get(type);
|
||||
|
||||
if (characterSet !== undefined) {
|
||||
return generateForCustomCharacters(length, characterSet);
|
||||
}
|
||||
|
||||
if (type === 'base64') {
|
||||
return uint8ArrayToBase64(randomBytes(Math.ceil(length * 0.75))).slice(0, length); // Needs 0.75 bytes of entropy per character
|
||||
}
|
||||
|
||||
return uint8ArrayToHex(randomBytes(Math.ceil(length * 0.5))).slice(0, length); // Needs 0.5 bytes of entropy per character
|
||||
}
|
||||
Reference in New Issue
Block a user