Security Pitfalls in Password Generators: From Math.random to crypto.getRandomValues
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.getRandomValuesfor 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 Configurations#
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:
- Never use Math.random - it’s not cryptographically secure
- Use crypto.getRandomValues and handle modulo bias properly
- Calculate entropy for strength assessment rather than just looking at length
- 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