Security Pitfalls in Password Generators: From Math.random to crypto.getRandomValues#

I was building a password generator tool recently and discovered that many online implementations have serious security flaws. What seems like a simple password generation task actually hides numerous pitfalls.

The Fatal Problem with Math.random#

The simplest implementation looks like this:

function generatePassword(length) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
  let password = ''
  for (let i = 0; i < length; i++) {
    password += chars.charAt(Math.floor(Math.random() * chars.length))
  }
  return password
}

Looks fine? Actually, Math.random is NOT cryptographically secure.

Predictability of Pseudo-Random Numbers#

Math.random is a pseudo-random number generator (PRNG). Its output is entirely determined by a seed value. If an attacker knows the seed, they can predict all subsequent random numbers.

V8’s implementation is based on the XorShift128+ algorithm. While statistically random, it has weaknesses:

// An attacker can infer internal state by observing historical outputs
const outputs = []
for (let i = 0; i < 10; i++) {
  outputs.push(Math.random())
}
// Theoretically possible to reverse-engineer XorShift128+ state
// Then predict all future outputs

In 2015, someone exploited this vulnerability to crack certain online gambling websites.

The Correct Approach: crypto.getRandomValues#

Browsers provide a cryptographically secure random number generator:

function generateSecurePassword(length: number, charset: string): string {
  const values = new Uint32Array(length)
  crypto.getRandomValues(values)
  
  let password = ''
  for (let i = 0; i < length; i++) {
    // Map to charset using modulo
    password += charset[values[i] % charset.length]
  }
  return password
}

crypto.getRandomValues uses the operating system’s entropy source (Linux’s /dev/urandom, Windows’ CryptGenRandom), providing true cryptographic randomness.

The Modulo Bias Problem#

The implementation above has a hidden issue: values[i] % charset.length introduces bias.

Assume charset.length = 62 (uppercase + lowercase + numbers), Uint32 ranges from 0 to 4,294,967,295:

4,294,967,295 ÷ 62 = 69,273,666.05...

Not evenly divisible! This means some characters will appear one more time than others.

Rejection Sampling Algorithm#

The standard method to eliminate bias is rejection sampling:

function secureRandomInt(max: number): number {
  const limit = Math.floor((2 ** 32) / max) * max
  const values = new Uint32Array(1)
  
  do {
    crypto.getRandomValues(values)
  } while (values[0] >= limit)
  
  return values[0] % max
}

function generateSecurePassword(length: number, charset: string): string {
  let password = ''
  for (let i = 0; i < length; i++) {
    password += charset[secureRandomInt(charset.length)]
  }
  return password
}

Rejection sampling ensures every character has an exactly equal probability of being selected.

Security Considerations for Charset Design#

Avoid Visually Similar Characters#

Some characters are visually confusing:

const AMBIGUOUS_CHARS = {
  similar: 'Il1O0',        // I/l/1, O/0 easily confused
  special: '"\'`\\',       // Quotes and backslash cause issues
  unambiguous: 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'
}

If passwords might be manually entered (like WiFi passwords), exclude these characters.

Special Character Compatibility#

Some systems restrict special characters:

const SAFE_SPECIAL = '!@#$%^&*-_+='  // Most systems support these
const RISKY_SPECIAL = '"\'<>{}[]()`\\|'  // May cause SQL injection, XSS, or command injection

Choose appropriate character sets based on usage context.

Correct Password Strength Assessment#

Entropy Calculation#

The essence of password strength is entropy:

function calculateEntropy(password: string, charsetSize: number): number {
  return password.length * Math.log2(charsetSize)
}

// 16-digit numeric password
console.log(calculateEntropy('1234567890123456', 10))  // 53.15 bits

// 16-character mixed case + numbers + symbols
console.log(calculateEntropy('aB3$xK9@mL2#pQ7!', 72))  // 98.06 bits

NIST recommendations:

  • 80 bits: General security
  • 112 bits: High-security scenarios
  • 128 bits: Military/government level

Entropy vs Cracking Difficulty#

// Assume attacker tries 1 trillion times per second (modern GPU cluster)
const attemptsPerSecond = 1e12
const entropy = 53  // 16-digit numeric

const secondsToCrack = 2 ** entropy / attemptsPerSecond
console.log(secondsToCrack / 3600 / 24 / 365)  // ~0.04 years (about 2 weeks)

53 bits of entropy is no longer secure against modern hardware.

Batch Generation Implementation Details#

Many users want to generate multiple passwords at once:

interface PasswordOptions {
  length: number
  count: number
  uppercase: boolean
  lowercase: boolean
  numbers: boolean
  symbols: boolean
  excludeAmbiguous?: boolean
}

function generatePasswords(options: PasswordOptions): string[] {
  let charset = ''
  if (options.uppercase) charset += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  if (options.lowercase) charset += 'abcdefghijklmnopqrstuvwxyz'
  if (options.numbers) charset += '0123456789'
  if (options.symbols) charset += '!@#$%^&*()_+-=[]{}|;:,.<>?'
  
  if (options.excludeAmbiguous) {
    charset = charset.replace(/[Il1O0]/g, '')
  }
  
  if (!charset) charset = 'abcdefghijklmnopqrstuvwxyz'
  
  const passwords: string[] = []
  for (let i = 0; i < options.count; i++) {
    passwords.push(generateSecurePassword(options.length, charset))
  }
  
  return passwords
}

Performance Optimization#

When batch generating, fetch enough random numbers at once:

function generatePasswordsOptimized(length: number, count: number, charset: string): string[] {
  // Get all random numbers at once
  const values = new Uint32Array(length * count)
  crypto.getRandomValues(values)
  
  const passwords: string[] = []
  for (let i = 0; i < count; i++) {
    let password = ''
    for (let j = 0; j < length; j++) {
      const idx = values[i * length + j] % charset.length
      password += charset[idx]
    }
    passwords.push(password)
  }
  
  return passwords
}

Reducing crypto.getRandomValues calls improves performance.

Frontend Security Boundaries#

Passwords in Memory#

Generated passwords are stored in JavaScript variables, theoretically accessible through memory inspection:

// Bad practice: Password stored in state long-term
const [password, setPassword] = useState('')

// Better approach: Use immediately after generation, then clear
const generateAndCopy = async () => {
  const pwd = generateSecurePassword(16, charset)
  await navigator.clipboard.writeText(pwd)
  // Don't store in state, return directly
  return pwd
}

Preventing Side-Channel Attacks#

While frontend code can’t completely prevent side-channel attacks, risks can be minimized:

// Avoid leaking passwords through console.log
const generatePassword = () => {
  const pwd = generateSecurePassword(16, charset)
  // console.log('Generated:', pwd)  // NEVER do this!
  return pwd
}

// Avoid sending passwords via network requests
// const sendPassword = async (pwd) => {
//   await fetch('/api/passwords', { body: pwd })  // Dangerous!
// }

Practical Recommendations#

Based on the above analysis, I implemented an online password generator: Password Generator

Key features:

  • Uses crypto.getRandomValues for cryptographic security
  • Rejection sampling to eliminate modulo bias
  • Customizable character sets and lengths
  • Batch generation up to 20 passwords
  • Optional exclusion of ambiguous characters

Recommended settings for different scenarios:

Scenario Length Charset Entropy
Regular websites 12 Upper+lower+numbers 71 bits
Important accounts 16 Upper+lower+numbers+symbols 98 bits
WiFi passwords 16 Exclude ambiguous chars 90 bits
Financial accounts 20 Full charset 123 bits

Conclusion#

Password generation seems simple but hides complexity:

  1. Never use Math.random - it’s not cryptographically secure
  2. Use crypto.getRandomValues and handle modulo bias properly
  3. Calculate entropy for strength assessment rather than just looking at length
  4. Mind frontend security boundaries to avoid memory leaks and side-channel attacks

Security is no small matter - password generators should be built to the highest standards.


Related tools: Password Strength Checker | Hash Generator