Back to Blog

The Generator Never Saw the Password

September 4, 2026
Share:
The Generator Never Saw the Password

I was about to ship a password generator the obvious way: a form, a button, a string comes back. Then I sat with where that string would exist for a second.

If the generator runs on a server, the password has to go there to come back. Even if the FAQ says nothing is stored. The bytes were on a machine I don't own, and that machine writes logs.

So I didn't build that. The password generator on Softery runs in your tab. I never get the password. I can't get it. After the page has loaded, nothing of yours comes back to me at all.

The Password Has to Live Somewhere

Every password you use exists at the site that accepted it, and in the manager you pasted it into. It also existed for a moment when it was created. People obsess over the first two. The creation step is often a text box on a website with ads down the side, and the page that drew the password also received it.

That would be fine if "received it" meant "drew it on your screen." A lot of generators mean "posted it to a backend, rendered the response." You can tell which kind you have. Open the network panel. Press Generate. If a request fires, the password just took a trip.

I wanted the other kind, because I was about to paste the result into a bank and a password manager, and I did not want a stranger's access log sitting in the middle of that.

The Web Crypto API already does this job. crypto.getRandomValues fills a typed array with bytes from the browser's own CSPRNG, the same source that creates session tokens. It runs in the tab. It does not call home.


What the Page Does

Two modes and a copy button.

Random mode starts at 20 characters from lowercase, uppercase, digits and symbols. That alphabet is 80 symbols, so 20 characters is about 126 bits. Passphrase mode starts at seven words from the EFF's public 7,776-word list, joined with hyphens. That is 90.5 bits.

The number sits next to the password. "Strong" is a mood. Bits are a count. I got tired of generators that paint a green bar and call it a day.

Copy is one tap. Generate another is next to it, because the useful move after you paste a password into a manager is to throw the one on the screen away.

There is no account. There is no extension. The page keeps working with the network unplugged.


Random Starts at Twenty Characters

A lot of generators default to 12. Twelve from a 62-character alphabet (letters and digits, no symbols) is 71 bits. Fine for a throwaway. Thin for anything you will still be logging into in five years.

Twenty from 80 symbols is the other direction on purpose. You can shorten it. You can drop a set if a form will not take symbols. You can hide the lookalikes (i, l, 1, L, o, 0, O) if you are typing the thing onto a TV. Each of those choices moves the number, which is the point of putting the number there.

The generator also puts at least one character from every set you left on into the result. A 20-character password that is accidentally all lowercase is a real outcome of naive sampling, and it is a dumb way to fail a "must include a number" rule. The fill happens first, then any missing set is written into a random unused slot.


Seven Words From a Public List

The passphrase is the mode I actually wanted.

Randall Munroe's comic about correct horse battery staple is the thing people send around. Four common English words, memorable, long enough that a guessing program has a bad day. The comic is right about the idea and light on the list. Four words from "words you can think of" is not the same as four words from a 7,776-word list picked with a CSPRNG.

The EFF published that list in 2016 and put it in the public domain. 7,776 entries, which is six dice. Each word is log2(7776) bits, which is 12.92. Seven of them is 90.47, which formats as 90.5.

Four from the same list is 52 bits. That is a different product. I defaulted to seven so the number on load already clears the bar a Show HN post used: over 90 bits of entropy.

You can drop to six, or five, or add a digit on the end for the forms that demand one. Capitalizing each word is there for the same kind of rule, and because some people remember the title-case version more easily.


90 Bits Is a Count

Each extra bit doubles the search space. Here is the same rate, a billion guesses a second, against a few sizes I actually computed while building the page.

What you haveBitsAt a billion guesses a second
8 characters, letters and digits47.6about 61 hours
12 characters, letters and digits71.5about 100,000 years
4 EFF words51.7about 42 days
6 EFF words77.5about 7 million years
7 EFF words90.5about 54 billion years
20 characters from 80 symbols126.4not this universe

The 8-character row is why "must include a symbol" on an 8-character field does not save you. The 4-word row is why I did not default to the comic: at a billion guesses a second those four words last about six weeks. The 7-word row is why the page opens there.

A well funded attacker with a GPU farm can go faster than a billion a second against a fast hash. They cannot go 54 billion times faster. Use a password manager's slow KDF and the table gets even kinder. The bits still have to be there first.


Four Dictionary Words Are Not the Same Thing

People hear "passphrase" and pick four words they like. sunset coffee river piano. That is a sentence they will remember, and it is also a sentence a wordlist-aware guesser will try early, because those are common words in common patterns.

The EFF list is not "English." It is 7,776 tokens chosen to be distinct, easy to type, and evenly usable as dice faces. abacus is in it. zoom is in it. password is not. The generator picks with getRandomValues, not with a person thinking of animals.

Over 90 bits of entropy.

That line is what I took from a Show HN post as the number to beat. Seven words from this list is how this page gets there without making you type twenty random characters by hand.


Math.random Is the Wrong Call

Math.random is fine for a shuffle animation. It is not a CSPRNG. Engines do not promise cryptographic quality, they promise "good enough to pick a banner ad." Some of them have been predictable from a handful of outputs.

crypto.getRandomValues is the one the spec points at when the bytes have to be unguessable. It is in every browser this page will run in. Node has it too, which is how the tests call the same function.

The page never calls Math.random. There is a test that spies on it and fails if it fires. I also do not do Math.floor(Math.random() * n), which is the version you find in the first Stack Overflow answer and in half the gists. That one has the remainder problem and the weak RNG stacked on top of each other.


The Remainder Trick

A classic bug in homemade generators is this:

const i = randomByte % 26; // pick a letter

A byte is 0 to 255. 256 is not a multiple of 26. The first 10 letters of the alphabet get one extra slot. Over a million passwords that bias is visible. Over a billion it is a gift.

The fix is rejection sampling. Draw a 32-bit value. If it sits above the largest multiple of n that fits in 32 bits, throw it out and draw again. The values that remain are uniform.

I wrote a test that feeds 0xffffffff first, which sits above the cutoff for n = 10, and asserts the function rejects it and takes the next draw. A test that only checks "returns a number between 0 and 9" would have accepted the biased version.


I Had to Inject the RNG to Trust It

You cannot assert on a random password. The next one is different.

So the generator takes an optional source of bytes. Production passes crypto.getRandomValues. Tests pass a function that returns zeros, or a scripted sequence. With zeros, a lowercase password of length 8 is aaaaaaaa. With zeros and all four sets on, a length 20 password is A0!aaaaaaaaaaaaaaaaa, because the fill hits a and then the "at least one of each set" pass writes A, 0 and ! into the first three slots.

That looks ugly. It is also the only way to know the ensure-one-of-each logic is doing what I think it is doing, instead of hoping a million runs get lucky.

The library is 187 lines. The tests next to it are 306. The wordlist is 7,776 entries in its own file. The page has another 6 tests for copy, length and the passphrase switch. I watched them fail before the module existed.


Settings Stay, the Password Doesn't

Length, the character boxes, random vs passphrase, the separator: those go in localStorage so you do not have to click them again tomorrow.

The password does not. A refresh draws a new one. Close the tab and it is gone. There is no draft to steal from a shared computer, because there is no draft.

If you want the old password, it is in the manager you pasted it into. That is the only place it should still be.


Forms That Want a Digit

Some sites will not take a password that looks like a sentence. Some want a number. Some want a symbol. Some reject ' and ". Random mode can require a character from every set you left on. Passphrase mode can append a digit, alpha-bravo-charlie-7, which is enough to clear a "must include a number" check without turning the phrase into soup.

Exclude similar is a typing aid. It shrinks the alphabet, so the bits drop, and the password gets easier to read off a screen onto a phone. The number next to it will tell you which way you went. I left it off by default because I would rather you see 126 bits and turn it on than see 118 and wonder why.


What I Left Off the Page

A crack-time estimate that says "centuries" based on an offline hash nobody told me you are using. That is a later tool, and it is a guess unless it knows the site's KDF.

A strength library that flags password1 and then green-lights P@ssw0rd1. Pattern matching is useful and also how people end up with 28 bits they think is 80.

An account. A "save this password" button that would mean I am now in the password business.

The page generates and copies. That is the job.


Unplug It

Open the network panel. Load the page. Generate five passwords. The list stays empty after the first load.

Then pull the cable, or flip on airplane mode, and press Generate another. It still works, because the only thing it needed from the network was the JavaScript, and that already arrived.

I wrote the same sentence on the JSON formatter. Same reason. If a tool has to see the secret to help you with the secret, it should see it in your process, not mine.


What To Do With It

  1. Open the password generator.
  2. Leave it on passphrase if you have to type the result. Leave it on random if a manager will store it.
  3. Copy. Paste into the manager or the form. Press Generate another so the tab is holding a different string.
  4. Close the tab if you like. There is nothing of yours on a server to delete.

It's free. I never see the password. That is not a policy. It is the shape of the code.


Last updated: September 4, 2026 | Reading time: 10 minutes

Written at Softery.io. I'd rather count bits than call a password strong.