filetity

Half the characters in your password are more likely than the rest

Here is how almost every password generator picks a character. A random byte, then a remainder.

const byte = crypto.getRandomValues(new Uint8Array(1))[0];
password += alphabet[byte % alphabet.length];

A byte is 0 to 255, which is 256 values. Our alphabet with everything switched on is 90 characters. 256 divided by 90 is 2 with 76 left over, so the first 76 characters can be reached three ways and the remaining 14 only two.

Those 76 characters come up 1.172% of the time each. The other 14 come up 0.781%. Exactly half as likely again, on every character of every password the generator has ever produced.

How much does that actually cost?

A uniform 90 character alphabet gives 6.4919 bits per character. The biased one gives 6.4790. The loss is 0.0128 bits per character, so a twenty character password loses about a quarter of one bit out of 130.

A lowercase-only alphabet is even less interesting: 26 goes into 256 nine times with 22 left over, so 22 letters run 11% ahead of the other four and the cost is 0.00099 bits a character. Nobody is losing an account over this.

The bias is worth removing because it is free to remove, and because a generator carrying it usually carries something worse.

The fix is a rejection loop

Throw away the bytes that land in the uneven tail and draw again.

const limit = 256 - (256 % size);   // 180 for a 90 character alphabet
let byte = draw();
while (byte >= limit) byte = draw();
return byte % size;

Below the limit, every character is reachable by the same number of byte values, so every character is equally likely. For a 90 character alphabet the loop throws away 29.7% of its draws, which sounds expensive and costs nothing you can perceive: it is a few extra reads from an in-memory buffer for a twenty character password.

A rejection loop looks unbounded and is not. For any alphabet smaller than 128, each draw has at least a 50% chance of landing under the limit, so it running even ten times is under one chance in a thousand.

The bug that does matter is one line up

Math.random(). It is not seeded to resist anybody, its algorithm is public, and its internal state can be recovered from a modest run of outputs. A generator built on it produces passwords that look exactly as random as the good ones and are not, and no amount of care about the remainder afterwards repairs that.

The correct source is crypto.getRandomValues, which is the operating system's own randomness. The reason the modulo bias is worth checking for is that it is visible from outside: a generator that did not think about the remainder probably did not think about the source either.

And the thing that decides it: bits, not badges

Every generator on the internet prints a word. Weak, Fair, Strong. That word describes the SHAPE of a password: does it have a capital, a digit, a symbol. It is not what an attacker is up against.

What they are up against is the number of possibilities, and it is arithmetic. Length times the log of the alphabet size, in bits, with each bit doubling the work. A twelve character password of mixed letters and digits is 71.5 bits and passes every badge on the internet. Twenty characters from our full 90 character set is 129.8 bits.

Put a rate against it and the numbers become a decision. At a hundred billion guesses a second, which is a realistic offline attack on a fast hash, 48 bits falls in about twenty three minutes and 80 bits takes around 190,000 years. No badge distinguishes those two.

Which is why our passphrase default is ten words

A passphrase's strength depends on the size of the list the words came from and how many were drawn, and on nothing else. Not on how long the words are, and certainly not on how clever the phrase sounds.

Our list holds exactly 256 words, so each one is exactly 8 bits and you can check the total in your head. That is a deliberate trade and it has a real cost: Diceware's list of 7,776 words is 12.9 bits a word, so six Diceware words are 77.5 bits where six of ours are 48. Six is the default everywhere, and on a 256 word list six is twenty three minutes.

So ours defaults to ten, which is 80 bits, and the page shows the number while you change it. A short list does not make a passphrase weak. It makes it longer for the same strength, and the honest thing is to say which of those you are getting.

The tool this came out of

Passwords and passphrases generated in your browser, from the operating system's randomness, with the strength in bits and the attacker rate stated.

filetity is built by Adarsh Mishra. Every figure here was computed from the definitions rather than quoted, and the code that does it is a dozen lines you can read. If any of the arithmetic is wrong, that is worth knowing: support@filetity.com.