UNPKG

password-generator

Version:

Memorable password generator for Node and browsers (async WebCrypto).

262 lines (259 loc) 9.83 kB
// src/random.ts var crypto = globalThis.crypto; var MAX_RANDOM_BYTES = 65536; var getRandomBytes = async (length) => { if (!Number.isFinite(length) || length < 0) { throw new RangeError("length must be a non-negative finite number"); } const buffer = new Uint8Array(length); for (let offset = 0;offset < length; offset += MAX_RANDOM_BYTES) { const end = Math.min(offset + MAX_RANDOM_BYTES, length); crypto.getRandomValues(buffer.subarray(offset, end)); } return buffer; }; var createDeterministicRandomBytes = async (entropy, cryptoSource = crypto) => { if (entropy.length === 0) { throw new RangeError("entropy must not be empty"); } if (!cryptoSource.subtle) { throw new Error("WebCrypto subtle is required for deterministic entropy"); } const keyData = new Uint8Array(entropy).buffer; const key = await cryptoSource.subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); let counter = 0n; const counterBytes = new Uint8Array(8); const counterView = new DataView(counterBytes.buffer); const deterministicRandomBytes = async (length) => { if (!Number.isFinite(length) || length < 0) { throw new RangeError("length must be a non-negative finite number"); } const buffer = new Uint8Array(length); let offset = 0; while (offset < length) { counterView.setBigUint64(0, counter, false); counter += 1n; const block = new Uint8Array(await cryptoSource.subtle.sign("HMAC", key, counterBytes)); const take = Math.min(block.length, length - offset); buffer.set(block.subarray(0, take), offset); offset += take; } return buffer; }; return deterministicRandomBytes; }; var randomInt = async (min, max, randomBytes = getRandomBytes) => { if (!Number.isFinite(min) || !Number.isFinite(max)) { throw new RangeError("min and max must be finite numbers"); } if (max <= min) { throw new RangeError("max must be greater than min"); } const range = max - min; if (range === 1) { return min; } const bytesNeeded = Math.ceil(Math.log2(range) / 8); const maxValue = 256 ** bytesNeeded; const limit = maxValue - maxValue % range; let value = limit; while (value >= limit) { const bytes = await randomBytes(bytesNeeded); value = 0; for (const byte of bytes) { value = value * 256 + byte; } } return min + value % range; }; // src/index.ts var VOWELS = "aeiou"; var CONSONANTS = "bcdfghjklmnpqrstvwxyz"; var CONSONANT = new RegExp(`[${CONSONANTS}]$`, "i"); var DEFAULT_PATTERN = /\w/; var DEFAULT_LENGTH = 12; var MIN_ENTROPY_BITS = 64; var MIN_WORD_LENGTH = 3; var MAX_WORD_LENGTH = 7; var textEncoder = new TextEncoder; var MIN_MEMORABLE_LENGTH = (() => { let bits = 0; let len = 0; let vowel = false; while (bits < MIN_ENTROPY_BITS) { bits += Math.log2(vowel ? VOWELS.length : CONSONANTS.length); vowel = !vowel; len += 1; } return len; })(); var buildValidChars = (pattern) => { const chars = []; for (let i = 33;i <= 126; i += 1) { const char = String.fromCharCode(i); if (pattern.test(char)) { chars.push(char); } } if (chars.length === 0) { throw new Error(`Could not find characters that match the password pattern ${pattern}. Patterns must match individual characters, not the password as a whole.`); } return chars; }; var estimatePatternEntropy = (alphabetSize, length, prefixLength) => { const bitsPerChar = alphabetSize > 1 ? Math.log2(alphabetSize) : 0; return { entropyBits: bitsPerChar * Math.max(0, length - prefixLength), recommendedLength: bitsPerChar > 0 ? prefixLength + Math.ceil(MIN_ENTROPY_BITS / bitsPerChar) : null }; }; var estimateMemorableEntropy = (length, prefix) => { const effectiveLength = Math.max(0, length - prefix.length); let entropyBits = 0; let expectsVowel = CONSONANT.test(prefix); for (let i = 0;i < effectiveLength; i += 1) { entropyBits += Math.log2(expectsVowel ? VOWELS.length : CONSONANTS.length); expectsVowel = !expectsVowel; } let recommendedLength = prefix.length; let bits = 0; expectsVowel = CONSONANT.test(prefix); while (bits < MIN_ENTROPY_BITS) { bits += Math.log2(expectsVowel ? VOWELS.length : CONSONANTS.length); expectsVowel = !expectsVowel; recommendedLength += 1; } return { entropyBits, recommendedLength }; }; var buildMemorable = async (length, startsWithVowel, nextInt) => { let expectsVowel = startsWithVowel; let result = ""; for (let i = 0;i < length; i += 1) { const alphabet = expectsVowel ? VOWELS : CONSONANTS; result += alphabet[await nextInt(0, alphabet.length)]; expectsVowel = !expectsVowel; } return result; }; var buildWordLengths = async (count, nextInt, targetLength) => { const lengths = []; let total = 0; for (let i = 0;i < count; i += 1) { const len = await nextInt(MIN_WORD_LENGTH, MAX_WORD_LENGTH + 1); lengths.push(len); total += len; } if (targetLength !== undefined && total < targetLength) { const adjustable = []; for (let i = 0;i < count; i += 1) { if (lengths[i] < MAX_WORD_LENGTH) adjustable.push(i); } let remaining = targetLength - total; while (remaining > 0 && adjustable.length > 0) { const pick = await nextInt(0, adjustable.length); const wordIdx = adjustable[pick]; lengths[wordIdx] = lengths[wordIdx] + 1; remaining -= 1; if (lengths[wordIdx] >= MAX_WORD_LENGTH) { adjustable.splice(pick, 1); } } } return lengths; }; var generatePassword = async (length, memorable, pattern, prefix) => { const opts = {}; if (length !== undefined) opts.length = length; if (memorable !== undefined) opts.memorable = memorable; if (pattern !== undefined) opts.pattern = pattern; if (prefix !== undefined) opts.prefix = prefix; return generatePasswordWithOptions(opts); }; var generatePasswordWithOptions = async (options) => { const length = options?.length ?? DEFAULT_LENGTH; const memorable = options?.memorable ?? false; const pattern = options?.pattern ?? DEFAULT_PATTERN; const prefix = String(options?.prefix ?? ""); const ignoreSecurityRecommendations = options?.ignoreSecurityRecommendations ?? false; const entropy = options?.entropy; const words = options?.words; if (!Number.isSafeInteger(length)) { throw new RangeError("length must be a safe integer"); } if (length < 0) { throw new RangeError("length must be a non-negative integer"); } if (!(pattern instanceof RegExp)) { throw new TypeError("pattern must be a RegExp"); } if (words !== undefined) { if (!Number.isSafeInteger(words)) { throw new RangeError("words must be a safe integer"); } if (words <= 0) { throw new RangeError("words must be a positive integer"); } } if (words !== undefined && prefix !== "") { throw new Error("prefix is not supported when words are enabled"); } let entropyBytes; if (entropy !== undefined) { if (typeof entropy === "string") { entropyBytes = textEncoder.encode(entropy); } else if (entropy instanceof Uint8Array) { entropyBytes = entropy; } else { throw new TypeError("entropy must be a Uint8Array or string"); } } const randomBytes = entropyBytes ? await createDeterministicRandomBytes(entropyBytes) : getRandomBytes; const nextInt = (min, max) => randomInt(min, max, randomBytes); if (words !== undefined) { if (!ignoreSecurityRecommendations && words * MAX_WORD_LENGTH < MIN_MEMORABLE_LENGTH) { const recommendedWords = Math.ceil(MIN_MEMORABLE_LENGTH / MAX_WORD_LENGTH); throw new Error(`Security recommendation: word count ${words} cannot reach ${MIN_ENTROPY_BITS} bits with ${MIN_WORD_LENGTH}-${MAX_WORD_LENGTH} letter words. Use words >= ${recommendedWords}. To override, pass { ignoreSecurityRecommendations: true }.`); } const targetLength = ignoreSecurityRecommendations ? undefined : MIN_MEMORABLE_LENGTH; const lengths = await buildWordLengths(words, nextInt, targetLength); const wordsList = []; for (const wordLength of lengths) { wordsList.push(await buildMemorable(wordLength, false, nextInt)); } return wordsList.join(" "); } if (memorable) { if (!ignoreSecurityRecommendations) { const estimate = estimateMemorableEntropy(length, prefix); if (estimate.entropyBits < MIN_ENTROPY_BITS) { throw new Error(`Security recommendation: estimated entropy ${estimate.entropyBits.toFixed(1)} bits is below ${MIN_ENTROPY_BITS} bits. Use length >= ${estimate.recommendedLength} or set memorable: false. To override, pass { ignoreSecurityRecommendations: true }.`); } } const charCount = Math.max(0, length - prefix.length); return prefix + await buildMemorable(charCount, CONSONANT.test(prefix), nextInt); } const validChars = buildValidChars(pattern); if (!ignoreSecurityRecommendations) { const estimate = estimatePatternEntropy(validChars.length, length, prefix.length); if (estimate.entropyBits < MIN_ENTROPY_BITS) { const recommendation = estimate.recommendedLength === null ? "Use a broader pattern to increase the character set." : `Use length >= ${estimate.recommendedLength} or broaden the pattern.`; throw new Error(`Security recommendation: estimated entropy ${estimate.entropyBits.toFixed(1)} bits is below ${MIN_ENTROPY_BITS} bits. ${recommendation} To override, pass { ignoreSecurityRecommendations: true }.`); } } let result = prefix; while (result.length < length) { result += validChars[await nextInt(0, validChars.length)]; } return result; }; export { generatePasswordWithOptions, generatePassword }; //# debugId=8BE329A7E8A624DB64756E2164756E21 //# sourceMappingURL=index.js.map