Most password generators you find in tutorials share the same flaw. They call Math.random(), pick characters from a string, and call it a day. The passwords look random. They are not random enough.
This matters because a predictable generator is barely better than no generator at all. If someone can guess the internal state of your random source, they can reproduce every "random" password it ever produced.
Why Math.random() is not safe
Math.random() is a pseudorandom number generator. It starts from a seed and produces a long but fully deterministic sequence. In V8 (the engine behind Chrome and Node.js) the algorithm is xorshift128+. It is fast and fine for games, shuffling a playlist, or picking a random quote.
It is not fine for security, for three reasons:
- It is predictable. Given enough consecutive outputs, the internal state can be reconstructed, and every future value becomes known. Researchers have demonstrated this repeatedly against real-world token generators.
- It is not seeded by you. You cannot give it a strong seed, and you cannot verify the quality of the seed the engine chose.
-
MDN says so directly. The documentation warns that
Math.random()does not provide cryptographically secure random numbers and must not be used for anything security related.
Passwords, reset tokens, session IDs, and API keys are all security related.
What to use instead
Browsers and Node.js ship a cryptographic random source: the Web Crypto API.
const array = new Uint32Array(4);
crypto.getRandomValues(array);
console.log(array); // four unpredictable 32-bit numbers
crypto.getRandomValues() pulls from the operating system's entropy pool, the same source used for TLS keys. It is available in every modern browser and in Node.js 19+ as a global (earlier via require('crypto').webcrypto).
For single IDs, crypto.randomUUID() gives you a v4 UUID in one call:
const id = crypto.randomUUID();
// "3f6d4a2e-9b1c-4d5f-8a2e-7c9d1b3f5a6e"
The math that actually matters: entropy
A password's strength is measured in bits of entropy. The formula is simple:
entropy = length x log2(charset size)
Some concrete numbers:
- 8 lowercase letters: 8 x 4.7 = ~38 bits. Crackable in hours on a single GPU.
- 12 characters from letters, digits, and symbols (94 chars): 12 x 6.55 = ~79 bits. Out of reach for brute force.
- 16 characters from the same set: ~105 bits. Comfortable margin for years.
Length beats cleverness. A 16-character password from a plain generator is stronger than an 8-character one full of symbols that you struggle to remember. This is also why passphrases work: four random words from a 7,776-word list give about 51 bits, and they are easy to type.
Building a secure generator
Here is a compact generator that does it right:
function securePassword(length = 16) {
const charset =
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789" +
"!@#$%^&*()-_=+[]{};:,.<>?";
const randomValues = new Uint32Array(length);
crypto.getRandomValues(randomValues);
let password = "";
for (let i = 0; i < length; i++) {
// modulo bias is negligible here for typical lengths,
// but rejection sampling removes it entirely
password += charset[randomValues[i] % charset.length];
}
return password;
}
console.log(securePassword()); // e.g. "kQ7$mZ2@pL9#vX4!"
console.log(securePassword(20));
A few notes on this code:
-
crypto.getRandomValues()fills the whole array in one call, which is faster than calling it per character. - The
% charset.lengthintroduces a tiny modulo bias because 2^32 is not a multiple of 91. For password generation this bias is negligible, but if you want it gone, discard values above the largest multiple of the charset size and draw again. - The function runs entirely client-side. The password never leaves the browser, which is exactly how a generator should behave.
If you want to go further, add options users actually ask for: exclude ambiguous characters (0, O, l, 1), guarantee at least one character from each group, or generate a memorable passphrase from a word list instead.
Mistakes to avoid
-
Seeding with
Date.now(). Timestamps are guessable to the millisecond. An attacker who knows roughly when a password was created can brute force the seed. -
Using
Math.random()for reset tokens or API keys. Same predictability problem, worse consequences, because tokens are often transmitted and logged. - Short lengths with big charsets. Eight characters is short no matter how exotic the symbols are. Default to 16 or more.
- Rolling your own crypto. You do not need a custom shuffling algorithm. The platform already gives you a vetted one.
The one-line version
If you just need a strong password right now and do not want to write code, there is a free client-side password generator at https://handynest.live/tools/password-generator.html that uses this same approach. Nothing is sent to a server.
Takeaway
Random-looking is not random. Math.random() is a fast PRNG for non-security tasks. For passwords, tokens, and keys, reach for crypto.getRandomValues(), make the output long enough to carry real entropy, and keep the whole thing client-side. It is about ten lines of code, and it is the difference between a password that looks strong and one that actually is.
Top comments (0)