What was wrong with password.es, and no longer is

Published on by David Carrero

This blog has twelve articles explaining how things ought to be done: that length matters more than symbols, that a manager is not a convenience but the only way to keep the rule, that a percentage tells you nothing.

Writing that is easy. The uncomfortable part is holding your own site to the same standard. We did, and three things were wrong. They go here in full, with the numbers that gave them away, because a security site that only publishes its wins is worth considerably less.

The checker froze while you were typing

The checker analyses your password with zxcvbn, a library that looks for patterns instead of counting capitals. It works very well and it has a cost: it runs all at once, on the same thread that draws the page. While it runs, the browser can do nothing else.

That cost grows fast with length. Measured with the same bundle the site serves, using the kind of passwords people actually type — words, years, @ for a:

length one run
16 characters 45 ms
20 characters 121 ms
32 characters 273 ms
64 characters 843 ms
128 characters 3,621 ms

So far, normal. The problem was how many times we launched it.

The field had two separate listeners attached, input and keyup. A normal key press fires both. So every letter you typed ran the full analysis twice: 90 ms on a 16-character password, half a second on a 32-character one.

And nothing held it back: no timer, no length cap, no separate thread. Past about twenty characters, the browser took longer to analyse than you took to press the next key. Keystrokes piled up in a queue that never drained, and the page stopped responding while you typed. Pasting a long password froze it for seconds.

The fix is two small changes. We removed the keyup listener, because input already covers typing, pasting, deleting and your manager’s autofill: half the work disappears without changing a single thing you see. And we added a 150 millisecond wait: nothing is analysed while you type, and it is analysed the moment you stop. Nobody types slowly enough to notice.

The result is identical. Barcelona1992$ still says the same: that «barcelona» is in the dictionary, that 1992 looks like a year, and that it falls in under a second.

The description was cut off mid-sentence in fifteen languages

This one is dumber and more embarrassing.

The line that appears under the title in search results — the meta description — was truncated in fifteen of the checker’s eighteen languages. Only Spanish, Korean and Japanese survived. Someone trimmed it by length at some point, cut mid-word, and nobody looked afterwards.

In three languages the cut landed in the middle of a special character, so what we published to Google was this:

language what we served what it should say
Portuguese …datas e padr&o padrões
Turkish …arıyor ve ger&cced gerçekten
Vietnamese …chúng tôi tra mật kh&# mật khẩu

The striking part is where it was: on the best-performing page on the site. The checker converts far better than the generator, and it was doing it with a broken description. All fifteen are now complete.

While we were there, in Korean, Japanese and Vietnamese we moved the brand from the front of the title to the end. In search results the domain already shows separately, so opening with «password.es ·» only spent the space that counts most.

A bias of one in a hundred million

The generator picks each character with crypto.getRandomValues(), the browser’s good source of randomness. But to turn that huge number into «a position in the alphabet» we used a remainder, and there is a classic trap there: 2³² is not a multiple of 83, so the first positions in the alphabet came up very slightly more often than the last ones.

How slightly: with an alphabet of up to 89 characters, on the order of one part in a hundred million. It changes nothing. It does not make your password measurably weaker, not remotely.

We fixed it anyway, and it takes three lines: discard the 45 leftover values out of the 4,294,967,296 possible ones and draw again. On a site about passwords, «it’s negligible» is not a good reason to leave a bias in when removing it costs that.

What was missing: letting the browser enforce the promise

We have a whole article explaining that your password never leaves here. It was true, and even so it was only a sentence of ours: nothing technically stopped an injected script from doing otherwise.

Now every page carries a content security policy. The two parts that matter:

  • No script runs that wasn’t in the page when it was built. Every legitimate script is signed with its fingerprint; an injected one doesn’t match and the browser refuses to run it.
  • connect-src 'none'. The site makes no network request whatsoever from JavaScript, and now the browser enforces that. If something ever tried to send what you type anywhere, it wouldn’t get out.

The promise stopped being a statement and became a rule.

What is still wrong

Update, 31 August. All three items in this section were fixed the same day this article went up: the fifteen languages now describe the real engine, the two scales are one, and the checker’s FAQ explains the symbol count. They stay written down, because the record of what was wrong is worth more than a picture of a flawless site.

This is where this article earns its place.

Fifteen languages still describe the checker as the old engine. Their text says it recreates the classic PasswordMeter logic. Not any more: that engine was replaced because it called Password1! very strong and failed a five-word passphrase, which is exactly backwards. Spanish, Korean and Japanese are updated; the other fifteen are not.

There are two different scales in the same house. The front page decides whether a password is strong by its bits; the checker decides by how long it would take to break. Different criteria, and nowhere do we say so.

The generator and the checker don’t count the same symbols: one uses 27, the other assumes 33. The difference is small, but it’s there.

None of the three is serious. All three are debt, and they would rather be written down here than waiting for someone else to find them.

This version’s changelog

Since the repository isn’t public, the changelog goes here in full. Version 0.2.0, 31 August 2026:

Added. A per-page content security policy with the fingerprint of every script. The article on where your saved passwords are, in all eighteen languages. An editorial queue that decides what gets written, and that orders us to publish nothing when it runs empty.

Fixed. The checker description, cut off mid-sentence in fifteen languages, three of them with a character split in half. The tab freezing as you typed. The bias in the draw. And the photo missing from the previous article.

Changed. The checker title in Korean, Japanese and Vietnamese. The deployment, which now fires once the built site exists instead of a minute earlier. And the Vietnamese, Hindi and Arabic translations, which stop blocking a release even though they remain the weakest.


The figures in this article are measured, not estimated: the zxcvbn timings with the same bundle the site serves and passwords containing words and substitutions; the draw bias, computed over the 4,294,967,296 values of a 32-bit integer. The checker’s engine is zxcvbn, open source.

Photo by Ernandes Alves · Pexels

← Back to the blog