A random password generator builds a string by selecting characters from a defined set using a cryptographically secure random number generator (CSPRNG). The strength of the result depends on the size of that character set and the length of the string, measured in bits of entropy. Higher entropy means more possible combinations, making the password harder to guess or brute-force.
The Basics of Random Generation
Most password generators follow a simple loop: pick a random number, map that number to a character from your allowed set, append it to the string, repeat until you hit the desired length. The quality of the result hinges entirely on the random number generator. If the generator is predictable, the password is weak regardless of length.
A common mistake is using a standard pseudo-random number generator (PRNG) like JavaScript’s Math.random(). These generators use a simple algorithm with a limited internal state, meaning they can repeat sequences or be predicted if you know the starting seed. For security-sensitive tasks, you need a CSPRNG. This type of generator pulls entropy from the operating system’s hardware noise sources, ensuring each character selection is statistically independent and unpredictable.
The process is deterministic only in the sense that it follows strict mathematical rules for distribution. It does not mean the output is predictable. Each character is chosen independently from the previous one, which is why longer passwords exponentially increase the search space for anyone trying to guess them.
Why crypto.getRandomValues() Matters
The browser API crypto.getRandomValues() is the standard for generating secure random numbers in modern web applications. Unlike Math.random(), which is optimized for speed and UI animations, crypto.getRandomValues() is designed for security tasks like generating session keys, tokens, and passwords. It draws from the OS entropy pool, which mixes hardware noise, interrupt timings, and other unpredictable system events.
Using Math.random() for passwords can create vulnerabilities because its state is smaller and easier to reconstruct. If an attacker observes a few outputs, they might predict future outputs. crypto.getRandomValues() avoids this by using a more complex mixing function that ensures the output remains unpredictable even if parts of the internal state are known.
Here is how you implement a basic generator using this API:
function generatePassword(length, charset) {
const array = new Uint32Array(length);
crypto.getRandomValues(array);
let password = '';
for (let i = 0; i < length; i++) {
// Modulo bias correction for perfect uniformity
const max = Math.floor(0xFFFFFFFF / charset.length) * charset.length;
let val = array[i];
while (val >= max) {
crypto.getRandomValues(new Uint32Array([val]));
}
password += charset[val % charset.length];
}
return password;
}
This code ensures that every character in the charset has an equal probability of being chosen, avoiding "modulo bias," where some characters appear more frequently than others due to uneven division of the random number range.
Calculating True Entropy in Bits
Entropy measures the unpredictability of a password. It is calculated using the formula: $Entropy = \log_2(\text{charset\_size}^{\text{length}})$. This simplifies to $Entropy = \text{length} \times \log_2(\text{charset\_size})$. Each bit of entropy doubles the number of possible combinations.
Consider a standard charset containing lowercase letters, uppercase letters, digits, and common symbols. This typically includes 26 lowercase + 26 uppercase + 10 digits + ~10 symbols = 72 characters. If you generate a 16-character password from this set, the entropy calculation is:
$$Entropy = 16 \times \log_2(72) \approx 16 \times 6.17 = 98.7 \text{ bits}$$
Now compare this to a Diceware passphrase. Diceware uses a wordlist to build phrases. The EFF wordlist has 7,776 words. If you choose 4 words, the entropy is calculated differently because each word is an independent choice from the list:
$$Entropy = 4 \times \log_2(7776) \approx 4 \times 12.93 = 51.7 \text{ bits}$$
This comparison highlights a key insight: a 16-character random password has nearly double the entropy of a 4-word Diceware passphrase, despite the passphrase being easier to read and remember. If you increase the Diceware passphrase to 6 words, the entropy becomes $6 \times 12.93 = 77.6$ bits, still lower than the random password but often sufficient for most use cases due to better memorability.
Tools like PasswordForge calculate this automatically, showing the exact bit count for each option so you can choose between maximum strength and usability based on real numbers rather than guesses.
Diceware vs. Random Characters
Choosing between random characters and Diceware depends on how you interact with the password. Random character strings (like aB3$kL9#mN2@pQ5!) are high-entropy and compact but hard to remember and type accurately. They are ideal for machine-generated credentials, API keys, or database passwords where a human rarely types them manually.
Diceware passphrases (like correct horse battery staple) are lower entropy per character but easier for humans to remember and type. The words are distinct, reducing typos. For a user who logs in manually multiple times a day, a passphrase is often more practical. However, for a server-to-server authentication token, the compactness and higher entropy of random characters are superior.
When generating Diceware phrases, ensure your wordlist is large enough. A small wordlist (e.g., 1,000 words) yields only $\log_2(1000) \approx 10$ bits per word. Four words give 40 bits, which is weak. The EFF wordlist’s 7,776 words provide ~13 bits per word, making 4-6 words a strong baseline.
Handling Bulk Generation Efficiently
Bulk generation is necessary when rotating credentials for multiple servers, databases, or services. Doing this one-by-one is slow and prone to human error. Efficient bulk generation involves creating a single large buffer of random values and parsing them into multiple strings, or looping through the generation logic efficiently.
When generating hundreds of passwords, performance matters. Creating a new Uint32Array for each password is inefficient. Instead, create a large array once and slice it for each password. Here is an optimized approach:
function bulkGenerate(count, length, charset) {
const totalLength = count * length;
const array = new Uint32Array(totalLength);
crypto.getRandomValues(array);
const results = [];
const charsetLen = charset.length;
const max = Math.floor(0xFFFFFFFF / charsetLen) * charsetLen;
for (let i = 0; i < count; i++) {
let password = '';
const startIdx = i * length;
for (let j = 0; j < length; j++) {
let val = array[startIdx + j];
// Simple bias correction for bulk speed
while (val >= max) {
val = crypto.getRandomValues(new Uint32Array(1))[0];
}
password += charset[val % charsetLen];
}
results.push(password);
}
return results;
}
This method minimizes calls to the random number generator, which is the bottleneck. It generates all randomness in one go, then processes it. For credential rotation, you can export these results as CSV or JSON to import into your secrets manager. This ensures each service gets a unique, high-entropy credential without manual intervention. Always verify that your bulk generation logic does not reuse seeds or states, which could correlate passwords across services.