Why a password generator must run in your browser, not on a server

A password generated on a server is a password you've already shared. Here's why client-side generation with a cryptographic RNG is the only design that makes sense.

By Muhammad Tahir6 min readsecurityprivacyexplainer

A password is a secret. The whole point is that exactly one party knows it: you. So the moment you ask a website to generate a password for you, a quiet but important question appears — where does the generation actually happen? If the answer is "on the server," then the secret was created on a machine you don't control, and it had to travel back to you across the network. You're being handed a key by a stranger who, for a moment, also held a copy.

This sounds pedantic until you trace the threat model carefully. Then it becomes the difference between a tool you can trust and one you can't.

The two places code can run

Any web tool runs its logic in one of two places:

  • On the server — your browser sends a request, the server computes a result, and sends the result back. The server sees the inputs and the outputs.
  • In your browser (client-side) — JavaScript downloaded with the page does the work locally. The server sends code once, then sees nothing about what you do with it.

For most features the choice is about performance or convenience. For a password generator it's about secrecy, and the two options are not remotely equivalent.

What a server-side generator actually means

Imagine a generator where you click "Generate" and the browser quietly fetches https://example.com/api/password?length=20. The server picks a random string and returns it. To you it looks identical to a local tool. Under the hood, three things just happened that should bother you:

  1. The server created your secret. Whatever process generated it had the plaintext in memory. You're trusting that it wasn't logged, cached, copied, or weakly generated.
  2. The secret crossed the network. Even over HTTPS, it existed as plaintext at both endpoints. TLS protects it in transit; it does nothing about what either end does with it.
  3. The server can log it forever. Request logs, error logs, an APM tool capturing response bodies, a proxy in front of the app — any of these can persist your password without anyone acting maliciously. It only takes one breach of those logs to expose every password the service ever minted.

None of this requires bad intent. A well-meaning team with sloppy logging is enough. The architecture itself is the problem: a password that another machine generated is a password you have, at best, shared.

How a client-side generator works instead

A client-side generator never makes that request. All the logic ships with the page and runs in your tab:

  1. You choose a length and character set (lowercase, uppercase, digits, symbols).
  2. JavaScript asks the browser's built-in cryptographic random number generator for raw random bytes.
  3. Those bytes are mapped onto your chosen character set.
  4. The result is displayed. It never touches the network.

You can verify this yourself. Open your browser's developer tools, switch to the Network tab, and click generate. With a properly built client-side tool, you'll see zero network requests. That's the proof: if nothing left your machine, nothing could have been intercepted or logged. Our Password Generator is built exactly this way.

The part that matters most: cryptographic randomness

Running in the browser is necessary but not sufficient. The quality of the randomness matters just as much, because a password is only as unguessable as the source of randomness behind it. This is where two functions that look interchangeable are wildly different.

Math.random is not for secrets

Math.random() returns a float between 0 and 1, and it's the obvious tool — except it was never designed for security. It's a pseudo-random number generator (PRNG) tuned for speed and statistical evenness, not unpredictability. Most browser engines implement it with an algorithm like xorshift128+. The output looks random to a human, but it is fully deterministic given the internal state, and that state is small.

The practical consequence: researchers have demonstrated that by observing a handful of consecutive Math.random() outputs, you can recover the generator's internal state and then predict every future value. For a password generator that's catastrophic — an attacker who learns your generator's state can reproduce the exact password it will hand out. Math.random() also carries no guarantee of being seeded with real entropy. Never use it for anything secret.

crypto.getRandomValues is

The right tool is the Web Crypto API:

// Generate 16 cryptographically secure random bytes
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
// bytes is now filled with unpredictable values, 0-255 each

crypto.getRandomValues() is a CSPRNG — a cryptographically secure pseudo-random number generator. The browser feeds it from the operating system's entropy pool (getrandom//dev/urandom on Linux, BCryptGenRandom on Windows, the Secure Enclave-backed RNG on Apple platforms). These pools gather genuine physical unpredictability: hardware RNGs, interrupt timing, and other noise the OS collects.

What makes it cryptographic comes down to a few properties:

  • Unpredictability. Even seeing every previous output gives you no computational advantage in guessing the next one.
  • State that can't be reverse-engineered. You can't observe outputs and recover the internal state, so you can't run the generator forward like you can with Math.random().
  • Adequate seeding. It's seeded from a high-entropy OS source, not a timestamp.

A correctly built generator uses crypto.getRandomValues() and then maps bytes to characters carefully, avoiding a subtle trap.

The modulo bias trap

The naive way to pick a character is bytes[i] % alphabet.length. If your alphabet doesn't evenly divide 256, this skews toward the lower characters, because some residues occur slightly more often. For a 62-character alphabet, characters at the start become marginally more likely. A small bias, but in security small biases compound. The fix is rejection sampling: discard any byte that falls in the biased tail and draw again, so every character is exactly equally likely. Good generators do this; it's invisible in the output but it's why the randomness stays uniform.

The honest threat model

To be clear about what client-side generation does and doesn't protect against:

  • It eliminates the server seeing, logging, transmitting, or weakly generating your password. That's a real, common risk, and it's gone entirely.
  • It does not protect you from malware on your own device, a malicious browser extension reading the page, or a compromised version of the site's own JavaScript. Client-side code can only be trusted if the site that served it is trusted and your machine is clean.

That second point is why open, inspectable, no-network tools matter: you can watch the Network tab and confirm nothing leaves. You can't audit a server you can't see.

Generate, then verify

Once you've generated a password locally, it's worth confirming it's actually strong rather than just long-looking. A Password Strength Checker that also runs in your browser estimates how resistant the password is to guessing — without, of course, transmitting it anywhere. (If a strength checker sent your password to a server to "analyze" it, you'd be right back to the original problem.)

The principle underneath all of this is simple: a secret should be born where it lives. A password generator that runs in your browser, seeded by a cryptographic RNG, creates your secret on your machine and leaves it there. Any design that generates it elsewhere has already, by construction, let someone else hold your key.