generate.ts

2.6 KB · raw

1import type { PasswordPolicy } from "./types";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}1213/**14 * Generate a cryptographically random password.15 */16export function generatePassword(options: Partial<GeneratorOptions> = {}): string {17  const {18    length = 20,19    includeUppercase = true,20    includeLowercase = true,21    includeNumbers = true,22    includeSymbols = true,23    forbiddenChars = "",24  } = options;2526  const forbidden = new Set(forbiddenChars);27  const filter = (pool: string): string =>28    forbidden.size === 029      ? pool30      : Array.from(pool).filter((c) => !forbidden.has(c)).join("");3132  let chars = "";33  if (includeLowercase) chars += filter("abcdefghijklmnopqrstuvwxyz");34  if (includeUppercase) chars += filter("ABCDEFGHIJKLMNOPQRSTUVWXYZ");35  if (includeNumbers) chars += filter("0123456789");36  if (includeSymbols) chars += filter("!@#$%^&*");3738  if (chars.length === 0) chars = filter("abcdefghijklmnopqrstuvwxyz") || "abcdefghijklmnopqrstuvwxyz";3940  // Rejection sampling to eliminate modulo bias: discard CSPRNG values in the41  // final partial bucket so every character is exactly uniform over `chars`42  // (audit INFO — `% chars.length` alone slightly favours low indices when the43  // alphabet size does not divide 2^32).44  const limit = Math.floor(0x1_0000_0000 / chars.length) * chars.length;45  let password = "";46  while (password.length < length) {47    const batch = crypto.getRandomValues(new Uint32Array(length - password.length));48    for (let i = 0; i < batch.length && password.length < length; i++) {49      const v = batch[i]!;50      if (v >= limit) continue; // would bias the modulo — draw again51      password += chars[v % chars.length];52    }53  }54  return password;55}5657/**58 * Generate a password that satisfies a detected site policy — clamps length59 * to maxLength, raises to minLength, honors forbidsSymbols, and excludes60 * forbiddenChars. Used by the inline popup when the user clicks "regen" on a61 * signup form with parseable requirements.62 */63export function generateSmartPassword(policy: PasswordPolicy = {}): string {64  const defaultLength = 20;65  let length = defaultLength;66  if (policy.minLength) length = Math.max(length, policy.minLength);67  if (policy.maxLength) length = Math.min(length, policy.maxLength);68  if (length < 8) length = 8;6970  return generatePassword({71    length,72    includeSymbols: !policy.forbidsSymbols,73    forbiddenChars: policy.forbiddenChars,74  });75}