generate.ts

2.9 KB · raw

1import effShortWordList from "./eff-short-wordlist.txt?raw";23export interface GeneratorOptions {4  length: number;5  includeUppercase: boolean;6  includeLowercase: boolean;7  includeNumbers: boolean;8  includeSymbols: boolean;9  /** Characters to exclude from every pool — honors site forbidden-char lists. */10  forbiddenChars?: string;11}1213export const MIN_PASSPHRASE_WORDS = 4;14export const DEFAULT_PASSPHRASE_WORDS = 5;15export const MAX_PASSPHRASE_WORDS = 12;1617export type PassphraseSeparator = "-" | " " | "." | "_";1819export interface PassphraseOptions {20  wordCount: number;21  separator: PassphraseSeparator;22}2324const PASSPHRASE_WORDS = effShortWordList.trim().split("\n");2526/**27 * Generate a cryptographically random password.28 */29export function generatePassword(options: Partial<GeneratorOptions> = {}): string {30  const {31    length = 20,32    includeUppercase = true,33    includeLowercase = true,34    includeNumbers = true,35    includeSymbols = true,36    forbiddenChars = "",37  } = options;3839  const forbidden = new Set(forbiddenChars);40  const filter = (pool: string): string =>41    forbidden.size === 042      ? pool43      : Array.from(pool).filter((c) => !forbidden.has(c)).join("");4445  let chars = "";46  if (includeLowercase) chars += filter("abcdefghijklmnopqrstuvwxyz");47  if (includeUppercase) chars += filter("ABCDEFGHIJKLMNOPQRSTUVWXYZ");48  if (includeNumbers) chars += filter("0123456789");49  if (includeSymbols) chars += filter("!@#$%^&*");5051  if (chars.length === 0) chars = filter("abcdefghijklmnopqrstuvwxyz") || "abcdefghijklmnopqrstuvwxyz";5253  // Rejection sampling to eliminate modulo bias: discard CSPRNG values in the54  // final partial bucket so every character is exactly uniform over `chars`55  // (audit INFO — `% chars.length` alone slightly favours low indices when the56  // alphabet size does not divide 2^32).57  const limit = Math.floor(0x1_0000_0000 / chars.length) * chars.length;58  let password = "";59  while (password.length < length) {60    const batch = crypto.getRandomValues(new Uint32Array(length - password.length));61    for (let i = 0; i < batch.length && password.length < length; i++) {62      const v = batch[i]!;63      if (v >= limit) continue; // would bias the modulo — draw again64      password += chars[v % chars.length];65    }66  }67  return password;68}6970/** Generate a passphrase from EFF's 1,296-word short list. */71export function generatePassphrase(options: Partial<PassphraseOptions> = {}): string {72  const wordCount = Math.max(73    MIN_PASSPHRASE_WORDS,74    Math.min(MAX_PASSPHRASE_WORDS, Math.floor(options.wordCount ?? DEFAULT_PASSPHRASE_WORDS)),75  );76  const separator = options.separator ?? "-";77  const limit = Math.floor(0x1_0000_0000 / PASSPHRASE_WORDS.length) * PASSPHRASE_WORDS.length;78  const words: string[] = [];7980  while (words.length < wordCount) {81    const value = crypto.getRandomValues(new Uint32Array(1))[0]!;82    if (value < limit) words.push(PASSPHRASE_WORDS[value % PASSPHRASE_WORDS.length]!);83  }8485  return words.join(separator);86}