password-generator
Version:
Memorable password generator for Node and browsers (async WebCrypto).
351 lines (346 loc) • 13.6 kB
JavaScript
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