Use crypto.getRandomValues() to generate passwords because it provides cryptographically secure randomness, unlike Math.random() which is predictable. Calculate entropy using log2(charset_size^length) to determine the actual strength of your generated string in bits. This ensures your passwords are both random enough and sufficiently complex for modern security standards.
Why Standard Random Functions Are Not Enough
Most JavaScript environments offer two primary ways to generate random numbers: Math.random() and crypto.getRandomValues(). While Math.random() is fast and sufficient for shuffling a UI list or picking a random avatar color, it is not suitable for security-sensitive tasks. It uses a deterministic algorithm that can be predicted if you know the previous states or the seed. An attacker observing a few outputs could potentially predict future passwords generated by the same session.
crypto.getRandomValues() fills a typed array with random values from the operating system’s cryptographically secure pseudo-random number generator (CSPRNG). This is the same underlying mechanism browsers use to generate TLS keys. For password generation, unpredictability is the primary goal. If an attacker can predict your next password, the strength of the length or complexity matters less. Using the CSPRNG ensures that each character selection is independent and unpredictable.
Choosing the Right RNG for Security
The choice between RNG types affects how many bits of entropy you actually get. Math.random() typically provides around 32 bits of entropy in its internal state, though this varies by engine. In contrast, crypto.getRandomValues() draws from the OS entropy pool, which is designed to be high-entropy and non-deterministic.
When building a generator, you should avoid modulo bias. Simple modulo operations (value % charsetLength) can skew distribution if the maximum value of the RNG is not evenly divisible by the charset length. While this bias is often negligible for short passwords, it is best practice to use rejection sampling or ensure your charset size divides evenly into the RNG range. For most web-based generators, using crypto.getRandomValues() with a standard alphanumeric charset provides sufficient distribution without complex bias correction logic.
Calculating Entropy: Bits vs. Characters
Entropy measures the unpredictability of your password. It is not simply the number of characters. It is calculated by determining how many possible combinations exist for a given length and charset size. The formula is:
Entropy (bits) = log2(charset_size ^ length)
This simplifies to length * log2(charset_size).
Consider a standard alphanumeric charset consisting of uppercase letters, lowercase letters, and digits. This is 26 + 26 + 10 = 62 characters. If you generate a password of length 16, the calculation is:
16 * log2(62) ≈ 16 * 5.954 ≈ 95.27 bits
A 95-bit password is considered strong for most modern applications. If you used only lowercase letters (26 characters), the entropy for the same length would be 16 * log2(26) ≈ 75 bits. This demonstrates that expanding the character set adds significant entropy per character. Always display the calculated bits to the user so they understand the actual strength of their choice.
Building the Core Generation Logic
The core logic involves filling a buffer with random bytes and mapping those bytes to your chosen charset. Here is a complete, working JavaScript implementation that generates a 16-character alphanumeric password and calculates its entropy.
function generateSecurePassword(length = 16) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charsetSize = charset.length; // 62
// Create a buffer for random bytes
const buffer = new Uint8Array(length);
// Fill buffer with cryptographically secure random values
crypto.getRandomValues(buffer);
let password = '';
for (let i = 0; i < length; i++) {
// Map byte to charset index
// Note: For perfect uniformity with non-power-of-2 charsets,
// rejection sampling is ideal, but modulo is acceptable
// for most web contexts with 62 chars.
const index = buffer[i] % charsetSize;
password += charset[index];
}
// Calculate entropy
const entropyBits = Math.log2(Math.pow(charsetSize, length));
return { password, entropyBits };
}
// Example Usage
const result = generateSecurePassword(16);
console.log(`Generated Password: ${result.password}`);
console.log(`Entropy: ${result.entropyBits.toFixed(2)} bits`);
Running this code produces output similar to:
Generated Password: aB3kL9mN2pQ7rS4t
Entropy: 95.27 bits
The modulo operation (%) maps the byte value (0–255) to the charset index (0–61). Since 256 is not perfectly divisible by 62, there is a very slight bias toward the first few characters in the charset. For most practical purposes, this bias is negligible. If you require perfect uniformity, you would implement rejection sampling, discarding values above floor(256 / charsetSize) * charsetSize. However, for a 62-character set, the bias is minimal and the simpler code is often preferred for readability and speed.
Implementing Bulk Generation for Efficiency
When rotating credentials for multiple accounts, generating passwords one by one can be slow due to DOM updates or function call overhead. Bulk generation allows you to create many passwords in a single pass. This is particularly useful for exporting to CSV or JSON for import into password managers.
To implement bulk generation efficiently, reuse the buffer allocation strategy. Instead of creating a new Uint8Array for every password, you can allocate a larger buffer and slice it, or simply loop through the generation function. The bottleneck is rarely the RNG itself but the string concatenation. Using an array join method is faster than string concatenation for large batches.
function generateBulkPasswords(count, length = 16) {
const passwords = [];
for (let i = 0; i < count; i++) {
passwords.push(generateSecurePassword(length).password);
}
return passwords;
}
// Generate 5 passwords at once
const batch = generateBulkPasswords(5);
console.log(batch);
This approach minimizes overhead. When integrating with a UI, ensure you render the results efficiently. If you are building a tool like PasswordForge, bulk mode allows users to generate up to 1000 passwords instantly, showing entropy for each. This is critical for credential rotation where consistency matters. You can export these lists directly to clipboard formats without leaving the browser environment.
Testing and Validating Your Generator
Validation ensures your generator behaves as expected. You should test for three things: uniqueness, distribution, and entropy accuracy.
First, check uniqueness. Generate 100 passwords and ensure no duplicates occur. With 95 bits of entropy, collisions should be extremely rare. If duplicates appear frequently, your RNG might be falling back to a less secure method or your buffer size is too small.
Second, check distribution. Generate 1,000 passwords and count the frequency of each character. Each character in the charset should appear roughly equally often. If certain characters appear significantly more often, your mapping logic (modulo bias) needs adjustment.
Third, verify entropy calculation. Use known values to check your math. For a length of 1 with a charset of 2 (binary), entropy should be 1 bit. For length 10 with charset 2, entropy should be 10 bits. Your formula length * log2(charsetSize) should match these expectations.
If you are building a web-based tool, consider adding a visual indicator for entropy. A simple bar or numerical display helps users understand why a longer password is stronger. PasswordForge, for example, displays entropy in bits for every generated option, allowing users to see exactly how strong each choice is. This transparency helps users make informed decisions rather than guessing.
For a complete solution, combine the CSPRNG logic, entropy calculation, and bulk generation into a single module. Ensure the code runs entirely client-side to maintain privacy and speed. By focusing on the correct RNG and accurate entropy metrics, you create a generator that is both secure and understandable.