UNPKG

password-generator

Version:

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

351 lines (346 loc) 13.6 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, set: (newValue) => all[name] = () => newValue }); }; var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); // src/random.ts var crypto, MAX_RANDOM_BYTES = 65536, 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; }, 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; }, 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; }; var init_random = __esm(() => { crypto = globalThis.crypto; }); // src/index.ts var exports_src = {}; __export(exports_src, { generatePasswordWithOptions: () => generatePasswordWithOptions, generatePassword: () => generatePassword }); var VOWELS = "aeiou", CONSONANTS = "bcdfghjklmnpqrstvwxyz", CONSONANT, DEFAULT_PATTERN, DEFAULT_LENGTH = 12, MIN_ENTROPY_BITS = 64, MIN_WORD_LENGTH = 3, MAX_WORD_LENGTH = 7, textEncoder, MIN_MEMORABLE_LENGTH, 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; }, 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 }; }, 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 }; }, 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; }, 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; }, 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); }, 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; }; var init_src = __esm(() => { init_random(); CONSONANT = new RegExp(`[${CONSONANTS}]$`, "i"); DEFAULT_PATTERN = /\w/; textEncoder = new TextEncoder; 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; })(); }); // src/cli.ts import { parseArgs } from "node:util"; var DEFAULT_LENGTH2 = 16; var DEFAULT_MEMORABLE_LENGTH = 20; var DEFAULT_WORDS = 3; var showHelp = () => { console.log(`Generates a secure password\r `); console.log("Options:"); console.log(` -l, --length <n>: Password length [default: ${DEFAULT_LENGTH2}, or ${DEFAULT_MEMORABLE_LENGTH} with --memorable]`); console.log(" -m, --memorable: Generates a memorable password"); console.log(" -c, --non-memorable: Generates a non memorable password [default]"); console.log(" -p, --pattern <regex>: Pattern to match for the generated password"); console.log(" -i, --ignore-security-recommendations: Ignore security recommendations"); console.log(` -s, -sN, --words <n>: Generate N memorable words (3-7 letters) separated by spaces [default: ${DEFAULT_WORDS}]`); console.log(" -h, --help: Displays this help"); }; var expandWordsArg = (argv) => { const result = []; for (let i = 0;i < argv.length; i += 1) { const arg = argv[i]; const match = arg.match(/^-s(\d+)$/); if (match) { result.push("--words", match[1]); } else if (arg === "-s") { const next = argv[i + 1]; if (next !== undefined && /^\d+$/.test(next)) { result.push("--words", next); i += 1; } else { result.push("--words", String(DEFAULT_WORDS)); } } else { result.push(arg); } } return result; }; var runCli = async (argv = process.argv.slice(2)) => { const { values } = parseArgs({ args: expandWordsArg(argv), options: { length: { type: "string", short: "l" }, memorable: { type: "boolean", short: "m" }, "non-memorable": { type: "boolean", short: "c" }, pattern: { type: "string", short: "p" }, "ignore-security-recommendations": { type: "boolean", short: "i" }, words: { type: "string" }, help: { type: "boolean", short: "h" } }, strict: false, allowPositionals: true }); if (values.help) { showHelp(); return; } const { generatePasswordWithOptions: generatePasswordWithOptions2 } = await Promise.resolve().then(() => (init_src(), exports_src)); let memorable = values.memorable === true; if (values["non-memorable"]) memorable = false; const patternRaw = values.pattern; const pattern = typeof patternRaw === "string" ? new RegExp(patternRaw) : undefined; if (pattern) memorable = false; const words = values.words !== undefined ? Number(values.words) : undefined; const lengthRaw = values.length; const lengthValue = typeof lengthRaw === "string" ? Number(lengthRaw) : undefined; const length = lengthValue !== undefined ? lengthValue : words !== undefined ? undefined : memorable ? DEFAULT_MEMORABLE_LENGTH : DEFAULT_LENGTH2; const ignoreSecurityRecommendations = values["ignore-security-recommendations"] === true; const options = { memorable, ignoreSecurityRecommendations }; if (pattern) options.pattern = pattern; if (typeof length === "number" && Number.isFinite(length)) options.length = length; if (words !== undefined) options.words = words; console.log(await generatePasswordWithOptions2(options)); }; export { runCli }; //# debugId=50A204555605F5BB64756E2164756E21 //# sourceMappingURL=cli.js.map