import effShortWordList from "./eff-short-wordlist.txt?raw"; export interface GeneratorOptions { length: number; includeUppercase: boolean; includeLowercase: boolean; includeNumbers: boolean; includeSymbols: boolean; /** Characters to exclude from every pool — honors site forbidden-char lists. */ forbiddenChars?: string; } export const MIN_PASSPHRASE_WORDS = 4; export const DEFAULT_PASSPHRASE_WORDS = 5; export const MAX_PASSPHRASE_WORDS = 12; export type PassphraseSeparator = "-" | " " | "." | "_"; export interface PassphraseOptions { wordCount: number; separator: PassphraseSeparator; } const PASSPHRASE_WORDS = effShortWordList.trim().split("\n").filter((word) => !word.includes("-")); /** * Generate a cryptographically random password. */ export function generatePassword(options: Partial = {}): string { const { length = 20, includeUppercase = true, includeLowercase = true, includeNumbers = true, includeSymbols = true, forbiddenChars = "", } = options; const forbidden = new Set(forbiddenChars); const filter = (pool: string): string => forbidden.size === 0 ? pool : Array.from(pool).filter((c) => !forbidden.has(c)).join(""); let chars = ""; if (includeLowercase) chars += filter("abcdefghijklmnopqrstuvwxyz"); if (includeUppercase) chars += filter("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); if (includeNumbers) chars += filter("0123456789"); if (includeSymbols) chars += filter("!@#$%^&*"); if (chars.length === 0) chars = filter("abcdefghijklmnopqrstuvwxyz") || "abcdefghijklmnopqrstuvwxyz"; // Rejection sampling to eliminate modulo bias: discard CSPRNG values in the // final partial bucket so every character is exactly uniform over `chars` // (audit INFO — `% chars.length` alone slightly favours low indices when the // alphabet size does not divide 2^32). const limit = Math.floor(0x1_0000_0000 / chars.length) * chars.length; let password = ""; while (password.length < length) { const batch = crypto.getRandomValues(new Uint32Array(length - password.length)); for (let i = 0; i < batch.length && password.length < length; i++) { const v = batch[i]!; if (v >= limit) continue; // would bias the modulo — draw again password += chars[v % chars.length]; } } return password; } /** Generate a passphrase from EFF's 1,296-word short list. */ export function generatePassphrase(options: Partial = {}): string { const wordCount = Math.max( MIN_PASSPHRASE_WORDS, Math.min(MAX_PASSPHRASE_WORDS, Math.floor(options.wordCount ?? DEFAULT_PASSPHRASE_WORDS)), ); const separator = options.separator ?? "-"; const limit = Math.floor(0x1_0000_0000 / PASSPHRASE_WORDS.length) * PASSPHRASE_WORDS.length; const words: string[] = []; while (words.length < wordCount) { const value = crypto.getRandomValues(new Uint32Array(1))[0]!; if (value < limit) words.push(PASSPHRASE_WORDS[value % PASSPHRASE_WORDS.length]!); } return words.join(separator); }