ProtemKey

Methodology and test results

Everything a ProtemKey tool tells you is computed by two small JavaScript files, keys.js (generators and entropy) and strength.js (the checker). Every weekly rebuild runs those exact files in Node against the tests below and refuses to publish if one fails. Last run: 2026-10-31, Node v20.19.2.

6/6uniformity tests passed
12/12entropy formulas match hand calculations
71/90checker cases in the expected band (87 within one)

1. Where the randomness comes from

All randomness comes from crypto.getRandomValues, the Web Crypto API's cryptographically secure generator, which browsers seed from the operating system. Math.random is never used. Values are drawn as 32-bit unsigned integers, 16,384 at a time.

To pick one of n things (a word, a character, a digit), the generator needs a whole number from 0 to n−1 with every value equally likely. Taking a random 32-bit number modulo n is slightly biased unless n divides 232, so the generator uses rejection sampling:

limit = 2^32 − (2^32 mod n)      // the largest multiple of n that fits
repeat: x = next random 32-bit value
until x < limit
return x mod n

Every value below limit maps to each result exactly limit / n times, so the result is exactly uniform. The cost is an occasional redraw. In the worst case (n just over 231) about half of all draws are rejected: in this run, 1,000,000 calls with n = 231+1 used 2,000,644 draws, 2.0006 per call against 2.0000 expected. For 7,776 words the rejection chance is about 1 in 1,677,722. Modulo bias and rejection sampling explains this step by step.

Rules such as “at least one of each kind” or “avoid obvious PINs” are applied the same way: a candidate that breaks the rule is thrown away and a fresh one drawn, which keeps every allowed result equally likely.

2. Uniformity tests

Each test runs the generator functions visitors use, counts how often each outcome appears, and compares the counts with a perfectly even spread using Pearson's chi-square test. The p-value is the chance a perfectly fair generator would look at least this uneven; a test fails below p = 0.001. A fair generator fails such a test about once in a thousand runs by chance, so a failure triggers one full re-run before the build is stopped. This run needed 1 attempt.

TestWhat was sampledSamplesOutcomesFewest / most per outcomeχ²dfp
Passphrase words (EFF list)passphrase(), 6 words per phrase, 1,296,000 phrases7,776,0007,776882 / 1,1097756.877750.556pass
Password characters (94 printable)password(), length 100, 100,000 passwords10,000,00094105,460 / 107,05389.7930.578pass
Whole short passwords (3 digits)password(), every one of the 1,000 possible strings counted2,000,0001,0001,842 / 2,134982.69990.638pass
“At least one of each” stays uniformpassword(), length 2, lowercase + digits, both required: 520 distinct strings seen of 520 allowed1,040,0005201,852 / 2,112517.95190.506pass
PIN digit pairspin(), 6 digits, 1,000,000 PINs, each split into three 2-digit pairs3,000,00010029,661 / 30,35379.5990.925pass
randomBelow(7)the uniform-integer primitive, n = 77,000,0007997,913 / 1,002,12610.960.091pass
Naive byte % 94 (deliberately biased control)one random byte, then % 94: the shortcut rejection sampling avoids9,400,0009472,602 / 110,70925433793< 10⁻³⁰⁰bias detected

The last row is a control. A byte has 256 values, and 256 = 2 × 94 + 68, so the naive method makes the first 68 characters 1.5 times as likely as the other 26. The same test catches that instantly, which shows it has the power to find real bias, not just that it passes. The full output of this run is at /validation.json.

In a browser. On 2026-10-26 the same two main tests ran inside headless Chrome/124.0.6367.78, loading this site's keys.js and calling the browser's own crypto.getRandomValues: passphrase words over 7,776,000 samples, χ² = 7891.2 on 7775 df, p = 0.175; password characters over 10,000,000 samples, χ² = 72.0 on 93 df, p = 0.947. Both pass.

3. Entropy formulas, checked by hand

The strength shown by each generator is computed from its settings, never estimated from its output: it is log2 of the number of equally likely results. The cases below were worked out by hand as exact integer expressions (in the “count” column), evaluated by an independent Python script that shares no code with keys.js, and compared with what keys.js reports. They must agree to within 10−9 bits.

SettingHand calculationCountBits (hand)Bits (keys.js)
Passphrase: 6 words, lowercase, hyphens (the default)Six independent picks from 7,776 words: 7,776^6 = 2.21 × 10^23 phrases. The separator is fixed, so it adds nothing.221073… (24 digits)77.548977.5489match
Passphrase: 4 words, one word in CAPITALS, plus a digit and a symbol7,776^4 word choices × 4 places for the capitals × (10 digits × 4 words) × (8 symbols × 4 words).187195… (20 digits)64.021264.0212match
Passphrase: 5 words, each randomly capitalized or not7,776^5 × 2 choices of capitalization per word.909769… (21 digits)69.624169.6241match
Password: 20 characters, lower + upper + digits + 13 easy symbols (the default)75 possible characters, 20 independent picks: 75^20.317121… (38 digits)124.5764124.5764match
Password: 16 characters from all 94 printable ASCII26 + 26 + 10 + 32 = 94 characters; 94^16.371574… (32 digits)104.8734104.8734match
Password: 12 characters, lowercase + digits, look-alikes removedLowercase without l and o leaves 24; digits without 1 and 0 leave 8; 32^12 = 2^60, exactly 60 bits.115292… (19 digits)60.000060.0000match
Password: 3 characters, lowercase + digits, at least one of eachAll 36^3 strings, minus the 26^3 with no digit, minus the 10^3 with no letter: 46,656 − 17,576 − 1,000 = 28,080.2808014.777314.7773match
Password: 8 characters, all four classes (94), at least one of eachInclusion-exclusion over the four classes (26, 26, 10, 32): subtract strings missing one class, add back those missing two, subtract those missing three; none can miss all four. Missing one class leaves 68, 68, 84 or 62 characters; missing two leaves 42, 58, 36, 58, 36 or 52; missing three leaves 32, 10, 26 or 26. About 2.8 × 10^15 of the 6.1 × 10^15 eight-character strings qualify: the rule costs 1.1 bits.280765… (16 digits)51.318351.3183match
PIN: 4 digits10,000 PINs from 0000 to 9999.1000013.287713.2877match
PIN: 4 digits, obvious PINs avoidedRemove 10 repeats (0000…9999), 7 runs up (0123…6789), 7 runs down (9876…3210), 90 more repeated pairs (abab, not already counted), 128 more years 1900-2029 (1919 and 2020 are already pairs), and 150 further all-digit 4-character entries from the common-password list.960813.230013.2300match
PIN: 6 digits1,000,000 PINs.100000019.931619.9316match
PIN: 6 digits, obvious PINs avoided (the default)Remove 10 repeats, 5 runs up (012345…456789), 5 runs down, 90 more repeated pairs (ababab), and 89 further all-digit 6-character entries from the common-password list.99980119.931319.9313match

The formulas, for reference: k words from a list of W: k·log2W; one capitalized word of k: +log2k; random capitals: +k; a digit or symbol from a set of s on one of k words: +log2s + log2k; L characters from N: L·log2N; with every one of the chosen kinds required, inclusion-exclusion over the kinds; PINs: log2(10L − excluded).

4. Bands and attack rates

The meter colors a number of bits by these bands. They are judgment calls, tied to the fast offline attack rate: below 30 bits falls in under a few seconds there; 75 bits takes thousands of years on average.

BandBitsFast offline, 1011/s, at the band's lower edge
Very weakbelow 30–
Weak30 to 45less than a second
Fair45 to 602.9 minutes
Strong60 to 7567 days
Very strong75 and up6 thousand years
Illustrative attack rates used throughout this site. They are order-of-magnitude assumptions, not measurements; real rates vary with hardware, hash settings and defenses.
ScenarioRate (assumed)Situation
Online, rate-limited100 guesses per houra login form that slows down or locks out repeated failures
Offline, slow hash10,000 guesses per seconda stolen database hashed with a deliberately slow, salted scheme (bcrypt class)
Offline, fast hash100 billion guesses per seconda stolen database hashed with a fast unsalted hash (MD5, SHA-1 or NTLM class) on a high-end GPU rig

5. The strength checker and its labelled test set

The checker estimates guessing cost for a password a person typed: it splits the password into the cheapest pieces it can explain (common-list entries, dictionary words, keyboard walks, repeats, sequences, dates) and charges per character for the rest. The full rule table is on the checker page. Its common list is SecLists' “10k most common” (10,001 entries) and its dictionary is the EFF large wordlist (7,776 words).

It was tested on 90 passwords, each labelled with the band a realistic attacker would put it in. The labels were written before the checker was run on them and were not changed afterwards. Result: 71 in the expected band, 87 within one band, 5 rated stronger than labelled and 14 weaker. Rating a password two or more bands too strong is the dangerous mistake, and the build fails on it unless the case is listed as a known limit.

Where it disagrees with the labels

PasswordLabelCheckerBitsNote
ilovemycatvery-weakweak (+1)33.8a short common phrase
Ch@rlie2012weakvery-weak (-1)14.4a name, swaps and a year
Mustang1969!weakvery-weak (-1)17.2
MyDogRex2015!weakfair (+1)49.8
correcthorseweakvery-weak (-1)22.3two dictionary words
coffee&cakeweakvery-weak (-1)25.2
JohnSmith1975weakvery-weak (-1)25.7
london2012olympicsweakvery-weak (-1)27.5a city, an event year and a word
Kx9#mTweakvery-weak (-1)27.26 random-looking characters (39.3 bits if drawn from all 94)
h7Gq2xweakvery-weak (-1)25.46 random-looking letters and digits (35.7 bits if drawn from 62)
q7#Lm2!xfairweak (-1)35.58 random printable characters: 52.4 bits
Xk3pL9wQ2mfairweak (-1)42.910 random letters and digits: 59.5 bits
Hq8vN2xLp4Rzstrongfair (-1)52.312 random letters and digits: 71.5 bits
7rC#vL2q9Wx!pZ4every-strongstrong (-1)70.416 random printable: 104.9 bits

Known limits

All 90 labelled cases
PasswordLabelCheckerBits
passwordvery-weakvery-weak1
123456very-weakvery-weak1.6
qwertyvery-weakvery-weak2.6
iloveyouvery-weakvery-weak6.7
Password1very-weakvery-weak5.3
P@ssw0rdvery-weakvery-weak4
Passw0rd!very-weakvery-weak8
letmein!very-weakvery-weak8.6
monkey123very-weakvery-weak7.3
dragon2020very-weakvery-weak10.1
abc123very-weakvery-weak3.9
111111very-weakvery-weak4.4
7777777very-weakvery-weak6.1
qwertyuiopvery-weakvery-weak11
1qaz2wsxvery-weakvery-weak9.4
zaq12wsxvery-weakvery-weak11.7
asdfghvery-weakvery-weak6.6
1234qwervery-weakvery-weak10.3
Summer2024!very-weakvery-weak19.5
Spring2023very-weakvery-weak18
Welcome1very-weakvery-weak11.6
Football!very-weakvery-weak9.5
admin123very-weakvery-weak13.1
Admin@123very-weakvery-weak19.2
passw0rdvery-weakvery-weak2
01011990very-weakvery-weak14.8
12/25/1990very-weakvery-weak17.6
supermanvery-weakvery-weak4.6
trustno1very-weakvery-weak4.9
sunshine1very-weakvery-weak8.9
aaaaaavery-weakvery-weak6.8
abcabcabcvery-weakvery-weak5.2
987654321very-weakvery-weak5.2
princess!very-weakvery-weak11.5
Qwerty123!very-weakvery-weak12.2
Liverpool1very-weakvery-weak14
baseball7very-weakvery-weak6.6
michael1985very-weakvery-weak11.2
Jennifer01very-weakvery-weak12
drowssapvery-weakvery-weak2
!QAZ2wsxvery-weakvery-weak21.3
football4lifevery-weakvery-weak17.7
ilovemycatvery-weakweak33.8
q1w2e3r4t5very-weakvery-weak18.1
hello worldvery-weakvery-weak21.4
Ch@rlie2012weakvery-weak14.4
Mustang1969!weakvery-weak17.2
MyDogRex2015!weakfair49.8
correcthorseweakvery-weak22.3
BlueSky42very-weakvery-weak22
coffee&cakeweakvery-weak25.2
Tr0ub4dor&3weakfair47.9
Bananas!!very-weakvery-weak18.8
JohnSmith1975weakvery-weak25.7
sweetpea22very-weakvery-weak17.2
zebra-lampweakweak36
Nov-14-1988weakweak33.9
london2012olympicsweakvery-weak27.5
2580very-weakvery-weak12.7
Kx9#mTweakvery-weak27.2
h7Gq2xweakvery-weak25.4
glacier violin nutmegweakweak43.8
q7#Lm2!xfairweak35.5
correct horse battery staplevery-weakfair54.6
parrot-tiger-cactus-wobblefairfair49.1
Xk3pL9wQ2mfairweak42.9
My first car was a blue 1998 Civic!fairvery-strong97.9
Hq8vN2xLp4Rzstrongfair52.3
ladle-unbent-sprout-casing-gerbilstrongstrong72.3
rT6!vP2@kL9#very-strongfair53.3
unwind tamale exile pebble cobweb outletvery-strongvery-strong86.6
7rC#vL2q9Wx!pZ4every-strongstrong70.4
Oyster.Banjo.Wiry.Trench.Dimmer.Rebel.Haikuvery-strongvery-strong105.9
mN4$sQ8&wE2^zR7*tY5!very-strongvery-strong88.8
ilovechocolatecakeweakweak34.5
PurpleMonkeyDishwasherfairweak39.3
Iloveyou4ever!very-weakvery-weak25.1
Pa$$word2024very-weakvery-weak11.1
letmein123456very-weakvery-weak5.2
11223344very-weakvery-weak10.9
abcdefghvery-weakvery-weak5
zxcvbnm123very-weakvery-weak11.4
Winter2025!very-weakvery-weak20.8
Tigger123very-weakvery-weak9.7
loveyouvery-weakvery-weak10.2
whateververy-weakvery-weak7
starwarsvery-weakvery-weak5.9
batman1very-weakvery-weak8.6
123abc!very-weakvery-weak12.2
access14very-weakvery-weak10.2

Against secrets with known entropy

The checker was also run on 200 fresh outputs of each generator setting below, whose true entropy is known exactly. It may under-rate them, since it can't know a string was generated, but it must never rate one more than 15 bits stronger than it really is.

Generator settingTrue bitsChecker: lowestmedianhighest
Passphrase, 4 words, hyphens51.753.258.761.7pass
Passphrase, 6 words, spaces77.577.186.387.8pass
Passphrase, 5 words, Title Case, no separator64.658.269.669.8pass
Password, 12, letters + digits71.540.453.656.4pass
Password, 16, all 94 printable104.963.373.278.0pass
Password, 20, default settings124.677.790.295.7pass
Password, 10, lowercase only47.035.947.047.0pass

Random passphrases come out a few bits high, because the checker charges for the separator and capitals that the generator's figure treats as known settings. Random character strings come out low, because the checker charges each character by its own kind (4.7 bits for a letter) rather than the full set it was drawn from. Both are why the generators show their exact figure and the checker is for passwords people choose.

6. Sources and licenses

7. What the build checks