generate.ts

1.9 KB · raw

1export interface GeneratorOptions {2  length: number;3  includeUppercase: boolean;4  includeLowercase: boolean;5  includeNumbers: boolean;6  includeSymbols: boolean;7  /** Characters to exclude from every pool — honors site forbidden-char lists. */8  forbiddenChars?: string;9}1011/**12 * Generate a cryptographically random password.13 */14export function generatePassword(options: Partial<GeneratorOptions> = {}): string {15  const {16    length = 20,17    includeUppercase = true,18    includeLowercase = true,19    includeNumbers = true,20    includeSymbols = true,21    forbiddenChars = "",22  } = options;2324  const forbidden = new Set(forbiddenChars);25  const filter = (pool: string): string =>26    forbidden.size === 027      ? pool28      : Array.from(pool).filter((c) => !forbidden.has(c)).join("");2930  let chars = "";31  if (includeLowercase) chars += filter("abcdefghijklmnopqrstuvwxyz");32  if (includeUppercase) chars += filter("ABCDEFGHIJKLMNOPQRSTUVWXYZ");33  if (includeNumbers) chars += filter("0123456789");34  if (includeSymbols) chars += filter("!@#$%^&*");3536  if (chars.length === 0) chars = filter("abcdefghijklmnopqrstuvwxyz") || "abcdefghijklmnopqrstuvwxyz";3738  // Rejection sampling to eliminate modulo bias: discard CSPRNG values in the39  // final partial bucket so every character is exactly uniform over `chars`40  // (audit INFO — `% chars.length` alone slightly favours low indices when the41  // alphabet size does not divide 2^32).42  const limit = Math.floor(0x1_0000_0000 / chars.length) * chars.length;43  let password = "";44  while (password.length < length) {45    const batch = crypto.getRandomValues(new Uint32Array(length - password.length));46    for (let i = 0; i < batch.length && password.length < length; i++) {47      const v = batch[i]!;48      if (v >= limit) continue; // would bias the modulo — draw again49      password += chars[v % chars.length];50    }51  }52  return password;53}