0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
Most strength meters count character classes and hand out a green bar for a capital letter; this one tries to guess your passphrase the way a cracking tool would, searching every way of chopping the text into known patterns — a few hundred common words, dates, counting runs, repeats, keyboard walks, with lookalike swaps like @→a priced at what they actually cost — and reporting the cheapest split it finds. It opens on Summer2019! at 26 bits, gone instantly, and the breakdown underneath says exactly why: one word, one year, one exclamation mark. Try Tr0ub4dor&3, which lands near 25 bits for precisely the reason the famous comic gives, then correct horse battery staple at 47 — then type a real sentence and watch it get caught as prose and priced at barely more than a bit per character, while the same number of unrelated words keeps its full value. The one to press is roll one for me — it draws eight words with real randomness and prints the true entropy beside the meter's own guess, which is usually the more flattering of the two; nothing is stored, nothing is sent, and the page makes no network requests at all.
Source — the code you see is the code that just ran 1087 lines 50.9 KB index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Passphrase Strength Meter — sloprun.dev</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
Identity: instrument-panel. Machine facts in mono; human words in sans.
Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ---- pattern palette (per match type). Green stays out of here — green is earned. ---- */
:root {
--c-word:#2B5CA8; --c-word-bg:#E1EAF8;
--c-date:#6D3FA6; --c-date-bg:#EDE3FA;
--c-seq:#0E6C7C; --c-seq-bg:#DCEEF2;
--c-rep:#A63F6B; --c-rep-bg:#FAE3EE;
--c-key:#8A5A00; --c-key-bg:#F7EDD8;
--c-sep:#5A6068; --c-sep-bg:#EFEEEA;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--c-word:#8FB6F5; --c-word-bg:#1C2E4C;
--c-date:#C4A3F1; --c-date-bg:#2C2148;
--c-seq:#6FD3E0; --c-seq-bg:#14323A;
--c-rep:#F0A0C0; --c-rep-bg:#3A1C2A;
--c-key:#E0B060; --c-key-bg:#33291A;
--c-sep:#9AA0A8; --c-sep-bg:#23262D;
}
}
:root[data-theme="dark"] {
--c-word:#8FB6F5; --c-word-bg:#1C2E4C;
--c-date:#C4A3F1; --c-date-bg:#2C2148;
--c-seq:#6FD3E0; --c-seq-bg:#14323A;
--c-rep:#F0A0C0; --c-rep-bg:#3A1C2A;
--c-key:#E0B060; --c-key-bg:#33291A;
--c-sep:#9AA0A8; --c-sep-bg:#23262D;
}
/* ---- shell ---- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-size: 16px; line-height: 1.55; -webkit-text-size-adjust: 100%; overflow-x: hidden; }
.wrap { max-width: 1000px; margin: 0 auto; padding: 26px 18px 34px; }
h1 { font-size: 1.5rem; line-height: 1.2; margin: 0 0 7px; letter-spacing: -.015em; }
h2 { font-size: .95rem; margin: 0; }
.lede { margin: 0; color: var(--muted); font-size: .95rem; max-width: 68ch; }
.lede b { color: var(--ink); font-weight: 600; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.mono { font-family: var(--font-mono); }
.k-lab {
font-family: var(--font-mono); font-size: .66rem; letter-spacing: .12em;
text-transform: uppercase; color: var(--muted);
}
/* ---- cards ---- */
.card {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 14px; margin-top: 16px;
}
.cardhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; margin-bottom: 11px; }
.cardhead .note { font-size: .78rem; color: var(--muted); margin-left: auto; }
/* ---- buttons ---- */
.btn {
font-family: var(--font-mono); font-size: .72rem; letter-spacing: .02em;
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
border-radius: 999px; padding: 6px 11px; min-height: 32px; cursor: pointer;
white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.btn.solid { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* ---- input ---- */
.field { position: relative; display: flex; align-items: stretch; gap: 8px; }
.field .btn { flex: 0 0 auto; }
input.pw {
flex: 1 1 auto; min-width: 0; font-family: var(--font-mono); font-size: 1.02rem;
padding: 12px 13px; border-radius: 9px; border: 1px solid var(--line);
background: var(--bg); color: var(--ink); letter-spacing: .02em;
}
input.pw::placeholder { color: var(--muted); opacity: 1; }
input.pw:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
/* the soft ring is nearly invisible on white, so keyboard focus gets a real outline */
input.pw:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.privacy {
margin: 9px 0 0; font-family: var(--font-mono); font-size: .68rem; color: var(--muted);
display: flex; gap: 7px; align-items: flex-start; line-height: 1.5;
}
.privacy svg { flex: 0 0 auto; margin-top: 2px; }
.presets { margin-top: 11px; }
/* ---- readout grid ---- */
.grid { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1.08fr); gap: 16px; }
@media (max-width: 760px) { .grid { grid-template-columns: minmax(0,1fr); } }
.bitsline { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.bits {
font-family: var(--font-mono); font-size: clamp(2.3rem, 10vw, 3.2rem); line-height: .95;
letter-spacing: -.03em; font-variant-numeric: tabular-nums; color: var(--ink);
}
.bits.b0, .bits.b1 { color: var(--danger); }
.bits.b2 { color: var(--accent); }
.bits.b3, .bits.b4 { color: var(--run); }
.bitsunit { font-family: var(--font-mono); font-size: .78rem; color: var(--muted); }
.band {
font-family: var(--font-mono); font-size: .68rem; letter-spacing: .1em; text-transform: uppercase;
border: 1px solid var(--line); border-radius: 999px; padding: 4px 10px; display: inline-block;
}
/* The pill keeps a coloured border and sits on --surface: accent-on-accent-soft is
only 4.2:1 in the light theme, which this 11px uppercase label would fail. */
.band.b0, .band.b1 { color: var(--danger); border-color: var(--danger); }
.band.b2 { color: var(--accent); border-color: var(--accent); }
.band.b3, .band.b4 { color: var(--run); border-color: var(--run); }
.meter { margin-top: 13px; }
.track {
position: relative; height: 15px; border-radius: 999px; background: var(--bg);
border: 1px solid var(--line); overflow: hidden;
}
.fill { height: 100%; width: 0; border-radius: 999px; background: var(--muted); transition: width .32s ease, background-color .32s ease; }
.fill.b0, .fill.b1 { background: var(--danger); }
.fill.b2 { background: var(--accent); }
.fill.b3, .fill.b4 { background: var(--run); }
.tick { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--line); }
.scale { position: relative; height: 1.2em; margin-top: 5px; font-family: var(--font-mono); font-size: .64rem; color: var(--muted); }
.scale span { position: absolute; top: 0; transform: translateX(-50%); white-space: nowrap; }
.scale span:first-child { transform: none; }
.scale span:last-child { transform: translateX(-100%); }
.guesses { margin-top: 10px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); word-break: break-word; }
.guesses b { color: var(--ink); font-weight: 600; }
.ruler { margin: 12px 0 0; font-size: .82rem; color: var(--muted); max-width: 46ch; }
.ruler b { font-family: var(--font-mono); color: var(--ink); font-weight: 600; }
/* ---- rates ---- */
table.rates { width: 100%; border-collapse: collapse; }
table.rates td { padding: 8px 0; border-top: 1px solid var(--line); vertical-align: baseline; }
table.rates tr:first-child td { border-top: 0; }
table.rates td.who { font-size: .84rem; padding-right: 10px; }
table.rates td.who small { display: block; font-family: var(--font-mono); font-size: .66rem; color: var(--muted); letter-spacing: .03em; }
table.rates td.t { font-family: var(--font-mono); font-size: .82rem; text-align: right; white-space: nowrap; color: var(--ink); }
table.rates td.t.bad { color: var(--danger); }
table.rates td.t.good { color: var(--run); }
/* ---- chips / breakdown ---- */
.chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: flex-start; }
.chip {
border: 1px solid var(--line); border-radius: 8px; padding: 5px 7px 4px;
background: var(--bg); min-width: 0; max-width: 100%;
}
.chip .tok {
font-family: var(--font-mono); font-size: .84rem; display: block;
overflow-wrap: anywhere; word-break: break-word; color: var(--ink);
}
.chip .lab {
font-family: var(--font-mono); font-size: .62rem; letter-spacing: .08em; text-transform: uppercase;
display: block; margin-top: 3px; color: var(--muted);
}
.chip.t-word { border-color: var(--c-word); background: var(--c-word-bg); }
.chip.t-word .tok, .chip.t-word .lab { color: var(--c-word); }
.chip.t-date { border-color: var(--c-date); background: var(--c-date-bg); }
.chip.t-date .tok, .chip.t-date .lab { color: var(--c-date); }
.chip.t-seq { border-color: var(--c-seq); background: var(--c-seq-bg); }
.chip.t-seq .tok, .chip.t-seq .lab { color: var(--c-seq); }
.chip.t-rep { border-color: var(--c-rep); background: var(--c-rep-bg); }
.chip.t-rep .tok, .chip.t-rep .lab { color: var(--c-rep); }
.chip.t-key { border-color: var(--c-key); background: var(--c-key-bg); }
.chip.t-key .tok, .chip.t-key .lab { color: var(--c-key); }
.chip.t-sep { border-color: var(--line); background: var(--c-sep-bg); padding-left: 6px; padding-right: 6px; }
.chip.t-sep .tok, .chip.t-sep .lab { color: var(--c-sep); }
.chip.t-free { border-style: dashed; }
.sp { color: var(--muted); }
.legend { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 11px; }
.legend span { font-family: var(--font-mono); font-size: .64rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); display: inline-flex; align-items: center; gap: 5px; }
.legend i { width: 9px; height: 9px; border-radius: 3px; display: inline-block; border: 1px solid currentColor; }
/* ---- findings ---- */
ul.tips { list-style: none; margin: 0; padding: 0; }
ul.tips li { display: flex; gap: 9px; padding: 9px 0; border-top: 1px solid var(--line); font-size: .88rem; }
ul.tips li:first-child { border-top: 0; }
ul.tips .dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 2px; margin-top: 6px; background: var(--muted); }
ul.tips .d-word { background: var(--c-word); }
ul.tips .d-date { background: var(--c-date); }
ul.tips .d-seq { background: var(--c-seq); }
ul.tips .d-rep { background: var(--c-rep); }
ul.tips .d-key { background: var(--c-key); }
ul.tips .d-shape{ background: var(--danger); }
ul.tips .d-good { background: var(--run); }
ul.tips code { font-family: var(--font-mono); font-size: .84em; background: var(--bg); border: 1px solid var(--line); border-radius: 5px; padding: 0 4px; overflow-wrap: anywhere; }
ul.tips li.good { color: var(--run); }
ul.tips li.good code { border-color: var(--run); }
.rolled {
margin-top: 11px; border: 1px solid var(--run); background: var(--run-soft);
border-radius: 9px; padding: 10px 12px; font-size: .84rem;
}
.rolled .k-lab { color: var(--run); }
.rolled b { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
details.method { margin-top: 16px; }
details.method summary {
cursor: pointer; font-family: var(--font-mono); font-size: .7rem; letter-spacing: .06em;
text-transform: uppercase; color: var(--muted); padding: 4px 0;
}
details.method summary:hover { color: var(--accent); }
details.method summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
details.method .body { font-size: .86rem; color: var(--muted); max-width: 74ch; }
details.method .body p { margin: 9px 0; }
details.method .body b { color: var(--ink); }
details.method .body code { font-family: var(--font-mono); font-size: .85em; color: var(--ink); }
footer { margin-top: 24px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); }
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; }
}
</style>
</head>
<body>
<div class="wrap">
<h1>Passphrase Strength Meter</h1>
<p class="lede">Type a passphrase and watch it get taken apart the way a cracking tool would take it apart: <b>words, dates, runs and repeats</b> get named, and what is left over is the only part anyone has to actually guess.</p>
<section class="card" aria-labelledby="h-in">
<div class="cardhead">
<h2 id="h-in">Your passphrase</h2>
<span class="note" id="lenNote"></span>
</div>
<div class="field">
<label class="sr-only" for="pw">Passphrase to measure</label>
<input class="pw" id="pw" type="text" maxlength="128" spellcheck="false" autocomplete="off"
autocapitalize="off" autocorrect="off" placeholder="type something…">
<button class="btn" id="toggle" type="button" aria-pressed="false" title="Hide the characters on screen">HIDE</button>
</div>
<p class="privacy">
<svg width="12" height="14" viewBox="0 0 12 14" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4">
<rect x="1.2" y="5.8" width="9.6" height="7.2" rx="1.4"/><path d="M3.4 5.8V4a2.6 2.6 0 0 1 5.2 0v1.8"/>
</svg>
<span>Everything happens in this tab. Nothing is stored, nothing is remembered, and this page makes no network requests of any kind — you can watch the network panel while you type.</span>
</p>
<div class="presets">
<div class="k-lab" style="margin-bottom:6px">Try one</div>
<div class="row" id="presets"></div>
</div>
</section>
<section class="card" aria-labelledby="h-out">
<div class="cardhead">
<h2 id="h-out">How hard is it to guess?</h2>
<span class="note">estimate, not a promise</span>
</div>
<div class="grid">
<div>
<div class="bitsline">
<span class="bits mono" id="bits">0</span>
<span class="bitsunit">bits of<br>guesswork</span>
<span class="band" id="band" style="margin-left:auto">EMPTY</span>
</div>
<div class="meter">
<div class="track" id="track" role="progressbar" aria-labelledby="h-out" aria-valuemin="0" aria-valuemax="90" aria-valuenow="0" aria-valuetext="empty">
<div class="fill" id="fill"></div>
<div class="tick" style="left:33.33%"></div>
<div class="tick" style="left:50%"></div>
<div class="tick" style="left:66.67%"></div>
<div class="tick" style="left:83.33%"></div>
</div>
<div class="scale" aria-hidden="true">
<span style="left:0">0</span><span style="left:33.33%">30</span><span style="left:50%">45</span><span style="left:66.67%">60</span><span style="left:83.33%">75</span><span style="left:100%">90+</span>
</div>
</div>
<p class="guesses mono" id="guesses"></p>
<p class="ruler">Every extra bit doubles the work. <b>30 bits</b> is a coffee break,
<b>45</b> is an afternoon, <b>60</b> is a real wall, and past <b>75</b> nobody is getting
in by guessing — they will phone you and ask instead.</p>
<div id="rolled"></div>
</div>
<div>
<div class="k-lab" style="margin-bottom:4px">Time to guess it, on average</div>
<table class="rates"><tbody id="rates"></tbody></table>
</div>
</div>
<p class="sr-only" id="live" role="status" aria-live="polite"></p>
</section>
<section class="card" aria-labelledby="h-parts">
<div class="cardhead">
<h2 id="h-parts">What a cracker sees</h2>
<span class="note">cheapest split found, left to right</span>
</div>
<div class="chips" id="chips"></div>
<div class="legend" aria-hidden="true">
<span style="color:var(--c-word)"><i></i>word</span>
<span style="color:var(--c-date)"><i></i>date</span>
<span style="color:var(--c-seq)"><i></i>run</span>
<span style="color:var(--c-rep)"><i></i>repeat</span>
<span style="color:var(--c-key)"><i></i>keyboard</span>
<span style="color:var(--muted)"><i style="border-style:dashed"></i>nothing to say</span>
</div>
</section>
<section class="card" aria-labelledby="h-tips">
<div class="cardhead"><h2 id="h-tips">What to do about it</h2></div>
<ul class="tips" id="tips"></ul>
</section>
<details class="method">
<summary>How this number is worked out</summary>
<div class="body">
<p>The meter tries every way of chopping your passphrase into known patterns — a <b>word list</b> of about 450 entries baked into this page, <b>dates and years</b>, <b>counting runs</b> like <code>4567</code> or <code>wxyz</code>, <b>repeats</b> like <code>lolololol</code>, and <b>keyboard walks</b> like <code>asdfg</code> — and then picks the split that would cost an attacker the <em>least</em>. Anything the meter can't name is priced as raw brute force over the characters you actually used.</p>
<p>Substitutions are priced honestly: <code>@</code> for <code>a</code> and <code>0</code> for <code>o</code> are tried by every real tool, so they buy a bit or two, not a category. Splitting a passphrase into several parts does cost the attacker something (they have to guess the shape too), so the meter adds a penalty for each extra piece. And if the text is stitched together with function words — <code>the</code>, <code>is</code>, <code>my</code>, <code>for</code> — it is treated as running English and capped at about <b>1.2 bits per character</b>, because a sentence you composed is far weaker than the same letters drawn at random. Four <em>unrelated</em> words are not a sentence and keep their full price.</p>
<p><b>Bits</b> is just the base-2 logarithm of that guess count. Each extra bit doubles the work. Times assume the attacker gets the answer halfway through, on average.</p>
<p><b>Where it lies to you:</b> the word list here is tiny — real attack dictionaries carry hundreds of millions of leaked passwords, names and phrases. If your passphrase leans on a word this page has never heard of, the meter will price it as random and flatter you. Treat the number as a ceiling, never a floor.</p>
</div>
</details>
<footer>demo · sloprun.dev</footer>
</div>
<script>
(function () {
"use strict";
/* ============================================================
1. WORD LIST — small, everyday, roughly ordered by how often
it turns up in leaked-password lists. Rank matters: the
earlier a word sits, the cheaper it is to guess.
============================================================ */
var RAW = (
"password qwerty letmein welcome monkey dragon master shadow sunshine princess " +
"football baseball basketball soccer hockey superman batman spiderman iloveyou " +
"trustno1 whatever freedom hunter ninja computer starwars pokemon admin login " +
"secret access flower hello charlie michael jennifer jessica ashley michelle " +
"daniel matthew joshua andrew robert thomas william richard jordan taylor hannah " +
"samantha amanda nicole brandon justin ryan kevin jason tyler austin dakota " +
"harley maggie buster tigger chelsea cookie pepper ginger killer phoenix dolphin " +
"mustang corvette ferrari yamaha guitar banana chocolate orange purple silver " +
"golden diamond crystal troubador rainbow butterfly summer winter spring autumn " +
"january february march april june july august september october november december " +
"monday tuesday wednesday thursday friday saturday sunday " +
"love life home house water music money happy lucky magic dream night morning " +
"sunset sunrise midnight evening weekend holiday birthday " +
"red blue green black white yellow pink brown grey violet " +
"star moon sky cloud rain storm thunder lightning snow wind fire ocean river " +
"lake mountain forest garden beach island desert valley meadow canyon " +
"tree leaf rose daisy tulip clover cactus bamboo willow maple " +
"apple cherry peach grape lemon melon mango berry peanut walnut " +
"coffee sugar honey butter cheese bread pizza pasta burger noodle candy cake " +
"soup salad pickle waffle pancake muffin biscuit " +
"horse mouse rabbit bear wolf tiger lion eagle shark whale panda koala turtle " +
"snake frog spider monkey donkey parrot penguin falcon otter badger beaver " +
"book page story poem song dance movie game puzzle riddle " +
"paint brush pencil paper canvas marble " +
"phone laptop screen keyboard printer camera speaker battery " +
"table chair window door floor kitchen bedroom garage attic basement " +
"train plane boat truck wagon rocket bridge tunnel " +
"city town village street market school office library museum station " +
"friend family mother father sister brother uncle cousin " +
"king queen prince wizard castle crown scroll jester pirate robot alien " +
"planet galaxy comet meteor orbit " +
"correct staple bucket ladder hammer anvil kettle teapot lantern candle " +
"mirror pillow blanket carpet basket bottle barrel copper bronze " +
"happy angry quiet brave clever funny silly crazy sweet bitter salty spicy " +
"fresh clean dirty young tiny giant short narrow quick lazy busy " +
"heavy empty bright shiny sharp smooth rough gentle " +
"walk jump swim sleep laugh smile think learn teach write draw build break " +
"climb ride drive cook wander whisper wonder " +
"cat dog sun ice fox owl bee ant sea key box hat cup pen car map " +
"gold rust salt milk mint sage moss fern reed dust ember frost " +
"the and for you are was not but all can her out his has had one two six ten " +
"its our who why how now warm cold cool dark soft hard loud brick sock rope " +
"wire nail glue tape rock sand clay iron steel glass wood nest crumb"
).split(/\s+/);
var DICT = Object.create(null), WORDS = [];
for (var wi = 0; wi < RAW.length; wi++) {
var w = RAW[wi];
if (w && DICT[w] === undefined) { DICT[w] = WORDS.length + 1; WORDS.push(w); }
}
var MAXWORD = 0;
for (var wj = 0; wj < WORDS.length; wj++) if (WORDS[wj].length > MAXWORD) MAXWORD = WORDS[wj].length;
/* pool used by the "roll one for me" button — typeable lengths only */
var POOL = WORDS.filter(function (x) { return x.length >= 4 && x.length <= 7; });
/* ============================================================
2. Small helpers. All guess counts live in log2 space so that
nothing ever overflows to Infinity.
============================================================ */
var L2 = Math.log2 || function (x) { return Math.log(x) / Math.LN2; };
var LOG10_2 = Math.log(2) / Math.log(10);
var LEET = {
"4": "a", "@": "a", "3": "e", "0": "o", "$": "s", "5": "s", "7": "t",
"+": "t", "8": "b", "9": "g", "(": "c", "{": "c", "6": "b"
};
var LEET2 = { "1": "il", "!": "il", "|": "il" };
function lettersAt(ch) {
var lo = ch.toLowerCase();
if (lo >= "a" && lo <= "z") return { set: lo, sub: false };
if (LEET[ch]) return { set: LEET[ch], sub: true };
if (LEET2[ch]) return { set: LEET2[ch], sub: true };
return null;
}
function isHighSurr(c) { var x = c.charCodeAt(0); return x >= 0xD800 && x <= 0xDBFF; }
function isLowSurr(c) { var x = c.charCodeAt(0); return x >= 0xDC00 && x <= 0xDFFF; }
/* true when cutting the string just before index i would split a surrogate pair */
function splitsPair(s, i) {
return i > 0 && i < s.length && isHighSurr(s[i - 1]) && isLowSurr(s[i]);
}
function nCk(n, k) {
if (k > n) return 0;
if (k === 0) return 1;
var r = 1;
for (var d = 1; d <= k; d++) { r *= n; r /= d; n -= 1; }
return r;
}
function upperVariations(tok) {
if (tok === tok.toLowerCase()) return 1;
if (/^[A-Z][^A-Z]*$/.test(tok)) return 2; /* Capitalised */
if (/^[^a-z]*$/.test(tok)) return 2; /* ALL CAPS */
if (/^[^A-Z]*[A-Z]$/.test(tok)) return 2; /* trailinG */
var U = 0, L = 0, i;
for (i = 0; i < tok.length; i++) {
if (tok[i] >= "A" && tok[i] <= "Z") U++;
else if (tok[i] >= "a" && tok[i] <= "z") L++;
}
var v = 0;
for (i = 1; i <= Math.min(U, L); i++) v += nCk(U + L, i);
return Math.max(v, 2);
}
var SYMBOLS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
function cardinality(s) {
var lo = false, up = false, di = false, sp = false, sy = false, ot = false;
for (var i = 0; i < s.length; i++) {
var c = s[i];
if (c >= "a" && c <= "z") lo = true;
else if (c >= "A" && c <= "Z") up = true;
else if (c >= "0" && c <= "9") di = true;
else if (c === " ") sp = true;
else if (SYMBOLS.indexOf(c) >= 0) sy = true;
else ot = true;
}
var n = 0;
if (lo) n += 26; if (up) n += 26; if (di) n += 10;
if (sp) n += 1; if (sy) n += 33; if (ot) n += 100;
return Math.max(n, 10);
}
/* per-match floor: nothing is ever free */
function floorLg(tok, lg) {
var min = tok.length === 1 ? L2(10) : L2(50);
return Math.max(lg, min);
}
/* ============================================================
3. MATCHERS — each returns {i, j, type, tok, lg, why}
============================================================ */
function dictMatches(pw) {
var out = [], n = pw.length, i, k;
for (i = 0; i < n; i++) {
var cands = [{ s: "", sub: 0 }];
var lim = Math.min(MAXWORD, n - i);
for (k = 0; k < lim; k++) {
var opt = lettersAt(pw[i + k]);
if (!opt) break;
var next = [];
for (var c = 0; c < cands.length && next.length < 24; c++) {
for (var m = 0; m < opt.set.length; m++) {
next.push({ s: cands[c].s + opt.set[m], sub: cands[c].sub + (opt.sub ? 1 : 0) });
}
}
cands = next;
if (k + 1 < 3) continue;
for (var q = 0; q < cands.length; q++) {
var rank = DICT[cands[q].s];
if (rank === undefined) continue;
var tok = pw.slice(i, i + k + 1);
var subs = cands[q].sub;
var lg = L2(rank) + L2(upperVariations(tok)) + (subs ? Math.min(subs, 8) : 0);
out.push({
i: i, j: i + k, type: "word", tok: tok, lg: floorLg(tok, lg),
why: { word: cands[q].s, rank: rank, subs: subs,
caps: tok !== tok.toLowerCase(), rev: false }
});
}
}
/* backwards spelling — a standard rule in every cracking tool */
for (k = 3; k <= Math.min(MAXWORD, n - i); k++) {
var seg = pw.slice(i, i + k).toLowerCase();
var rv = seg.split("").reverse().join("");
if (rv === seg) continue;
var r2 = DICT[rv];
if (r2 === undefined) continue;
var tok2 = pw.slice(i, i + k);
out.push({
i: i, j: i + k - 1, type: "word", tok: tok2,
lg: floorLg(tok2, L2(r2) + L2(upperVariations(tok2)) + 1),
why: { word: rv, rank: r2, subs: 0, caps: false, rev: true }
});
}
}
return out;
}
function seqMatches(pw) {
var out = [], n = pw.length;
function cls(c) {
if (c >= "0" && c <= "9") return { sp: 10, first: (c === "0" || c === "1") };
if (c >= "a" && c <= "z") return { sp: 26, first: (c === "a" || c === "z") };
if (c >= "A" && c <= "Z") return { sp: 26, first: (c === "A" || c === "Z") };
return null;
}
var i = 0;
while (i < n - 1) {
var a = cls(pw[i]);
if (!a) { i++; continue; }
var d = pw.charCodeAt(i + 1) - pw.charCodeAt(i);
if (d !== 1 && d !== -1 || !cls(pw[i + 1]) || cls(pw[i + 1]).sp !== a.sp) { i++; continue; }
var j = i + 1;
while (j + 1 < n && cls(pw[j + 1]) && cls(pw[j + 1]).sp === a.sp &&
pw.charCodeAt(j + 1) - pw.charCodeAt(j) === d) j++;
if (j - i + 1 >= 3) {
var tok = pw.slice(i, j + 1);
var base = a.first ? 4 : a.sp;
out.push({
i: i, j: j, type: "seq", tok: tok,
lg: floorLg(tok, L2(base) + L2(tok.length) + (d < 0 ? 1 : 0)),
why: { down: d < 0, digits: a.sp === 10 }
});
}
i = j;
}
return out;
}
var ROWS = ["`1234567890-=", "qwertyuiop[]\\", "asdfghjkl;'", "zxcvbnm,./"];
function keyMatches(pw) {
var out = [], n = pw.length, low = pw.toLowerCase();
function pos(c) {
for (var r = 0; r < ROWS.length; r++) { var k = ROWS[r].indexOf(c); if (k >= 0) return { r: r, k: k }; }
return null;
}
var i = 0;
while (i < n - 1) {
var p = pos(low[i]), q = pos(low[i + 1]);
if (!p || !q || p.r !== q.r || Math.abs(p.k - q.k) !== 1) { i++; continue; }
var d = q.k - p.k, j = i + 1;
while (j + 1 < n) {
var a = pos(low[j]), b = pos(low[j + 1]);
if (!a || !b || a.r !== b.r || b.k - a.k !== d) break;
j++;
}
if (j - i + 1 >= 3) {
var tok = pw.slice(i, j + 1);
out.push({ i: i, j: j, type: "key", tok: tok, lg: floorLg(tok, L2(45) + L2(tok.length)), why: {} });
}
i = j;
}
return out;
}
function repMatches(pw, card) {
var out = [], n = pw.length;
for (var i = 0; i < n; i++) {
for (var b = 1; b <= Math.min(10, Math.floor((n - i) / 2)); b++) {
var base = pw.slice(i, i + b), rep = 1;
while (pw.substr(i + rep * b, b) === base) rep++;
if (rep < 2 || b * rep < 3) continue;
var tok = pw.slice(i, i + b * rep);
var lo = base.toLowerCase();
var baseLg = DICT[lo] !== undefined ? L2(DICT[lo]) : base.length * L2(card);
out.push({
i: i, j: i + b * rep - 1, type: "rep", tok: tok,
lg: floorLg(tok, baseLg + L2(rep)),
why: { base: base, rep: rep }
});
}
}
return out;
}
function dateMatches(pw) {
var out = [], m, re;
re = /(19[0-9][0-9]|20[0-4][0-9])/g;
while ((m = re.exec(pw)) !== null) {
out.push({ i: m.index, j: m.index + 3, type: "date", tok: m[0], lg: floorLg(m[0], L2(150)), why: { kind: "year", year: m[0] } });
re.lastIndex = m.index + 1;
}
re = /([0-9]{1,2})([\/\-\.])([0-9]{1,2})\2([0-9]{2,4})/g;
while ((m = re.exec(pw)) !== null) {
var a = +m[1], b = +m[3];
if (a >= 1 && a <= 31 && b >= 1 && b <= 31 && (a <= 12 || b <= 12)) {
var span = m[4].length === 4 ? 150 : 100;
out.push({ i: m.index, j: m.index + m[0].length - 1, type: "date", tok: m[0],
lg: floorLg(m[0], L2(372 * span * 4)), why: { kind: "full" } });
}
re.lastIndex = m.index + 1;
}
re = /([0-9]{8})/g;
while ((m = re.exec(pw)) !== null) {
var s = m[0];
var ok = (+s.slice(0, 4) >= 1900 && +s.slice(0, 4) <= 2049 && +s.slice(4, 6) >= 1 && +s.slice(4, 6) <= 12 && +s.slice(6) >= 1 && +s.slice(6) <= 31) ||
(+s.slice(4) >= 1900 && +s.slice(4) <= 2049 && +s.slice(0, 2) >= 1 && +s.slice(0, 2) <= 31 && +s.slice(2, 4) >= 1 && +s.slice(2, 4) <= 31);
if (ok) out.push({ i: m.index, j: m.index + 7, type: "date", tok: s, lg: floorLg(s, L2(372 * 150)), why: { kind: "full" } });
re.lastIndex = m.index + 1;
}
return out;
}
/* Separators are all but free: an attacker picks one scheme (space, dash,
underscore, dot) for the whole passphrase, so the cost is amortised, not per gap. */
function sepMatches(pw) {
var out = [];
for (var i = 0; i < pw.length; i++) {
if (" -_.".indexOf(pw[i]) >= 0) {
out.push({ i: i, j: i, type: "sep", tok: pw[i], lg: 0.35, why: {} });
}
}
return out;
}
/* ============================================================
4. SCORER — find the cheapest split (dynamic programming over
every position and every number of pieces), then price the
shape itself: k pieces can be ordered k! ways and the
attacker has to guess the shape too.
============================================================ */
var LGFACT = [0];
(function () { for (var i = 1; i <= 200; i++) LGFACT[i] = LGFACT[i - 1] + L2(i); })();
var SHAPE_COST = 1; /* bits per extra piece, on top of the k! term */
var SHAPE_MAX = 14; /* ...but layouts get enumerated too. Don't hand out infinite credit. */
function shapeCost(k) { return Math.min(LGFACT[k] + (k - 1) * SHAPE_COST, SHAPE_MAX); }
/* Shannon put running English at 0.6–1.3 bits per character; 1.2 is the generous end. */
var ENGLISH_BITS_PER_CHAR = 1.2;
/* Grammar glue. A string only counts as running English if it is stitched together
with high-frequency function words. This is the whole test: without it, "correct
horse battery staple" and every rolled passphrase look like sentences (letters and
spaces, nothing else) and get capped at prose rates — which would be badly wrong,
because four UNRELATED words carry their full independent price. */
var GLUE = Object.create(null);
("the a an and or but if of to in on at for from by with as is are was were be been am " +
"i me my mine you your yours he him his she her hers it its we us our ours they them their " +
"this that these those there here what when where who whom why how which " +
"not no yes do does did done have has had will would can could shall should may might must " +
"so than then too very much more most just like about into over under after before again " +
"all any some each every own same other out up down off").split(/\s+/)
.forEach(function (w) { if (w) GLUE[w] = 1; });
function sentenceLike(pw) {
if (pw.length < 8) return false;
var toks = pw.trim().split(/\s+/);
if (toks.length < 3) return false;
var letters = 0, total = 0, glue = 0, i, c, ch, t;
for (i = 0; i < toks.length; i++) {
t = toks[i].replace(/^[!?.,;:'"()\[\]-]+/, "").replace(/[!?.,;:'"()\[\]-]+$/, "");
if (!t) return false;
if (GLUE[t.toLowerCase()]) glue++;
for (c = 0; c < t.length; c++) {
ch = t[c]; total++;
if ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z")) letters++;
else if (!LEET[ch] && !LEET2[ch] && ch !== "'" && ch !== "-") return false;
}
}
if (!total || letters / total < 0.7) return false;
return glue / toks.length >= 0.3;
}
function analyze(pw) {
var n = pw.length, i, j, k;
if (!n) return { bits: 0, seq: [], n: 0, card: 0 };
var card = cardinality(pw), lgc = L2(card);
var ms = dictMatches(pw)
.concat(seqMatches(pw), keyMatches(pw), repMatches(pw, card), dateMatches(pw), sepMatches(pw));
/* never let a chip cut an astral character in half (emoji are two code units) */
ms = ms.filter(function (mm) { return !splitsPair(pw, mm.i) && !splitsPair(pw, mm.j + 1); });
for (i = 0; i < n; i++) {
if (splitsPair(pw, i)) continue;
for (j = i; j < n; j++) {
if (splitsPair(pw, j + 1)) continue;
var tk = pw.slice(i, j + 1);
ms.push({ i: i, j: j, type: "free", tok: tk, lg: floorLg(tk, (j - i + 1) * lgc), why: {} });
}
}
var K = Math.min(n, 48);
var byEnd = [], dp = [], bp = [];
for (i = 0; i <= n; i++) {
byEnd.push([]);
var row = new Float64Array(K + 1);
for (k = 0; k <= K; k++) row[k] = Infinity;
dp.push(row); bp.push(new Array(K + 1));
}
dp[0][0] = 0;
for (i = 0; i < ms.length; i++) byEnd[ms[i].j + 1].push(ms[i]);
for (var e = 1; e <= n; e++) {
var list = byEnd[e];
for (var t = 0; t < list.length; t++) {
var m = list[t], src = dp[m.i], dst = dp[e];
for (k = 0; k < K; k++) {
var v = src[k];
if (v === Infinity) continue;
var nv = v + m.lg;
if (nv < dst[k + 1]) { dst[k + 1] = nv; bp[e][k + 1] = { from: m.i, fk: k, m: m }; }
}
}
}
var best = Infinity, bestK = 1;
for (k = 1; k <= K; k++) {
if (dp[n][k] === Infinity) continue;
var tot = dp[n][k] + shapeCost(k);
if (tot < best) { best = tot; bestK = k; }
}
var capped = false;
if (sentenceLike(pw)) {
var cap = ENGLISH_BITS_PER_CHAR * n;
if (cap < best) { best = cap; capped = true; }
}
var seq = [], pos = n, kk = bestK, guard = 0;
while (pos > 0 && guard++ < 300) {
var node = bp[pos][kk];
if (!node) break;
seq.unshift(node.m);
pos = node.from; kk = node.fk;
}
return { bits: best, seq: seq, n: n, card: card, pieces: bestK, capped: capped };
}
/* ============================================================
5. Bands, times, formatting
============================================================ */
var BANDS = [
{ at: 0, name: "very weak" },
{ at: 30, name: "weak" },
{ at: 45, name: "fair" },
{ at: 60, name: "strong" },
{ at: 75, name: "very strong" }
];
function bandOf(bits) { var b = 0; for (var i = 0; i < BANDS.length; i++) if (bits >= BANDS[i].at) b = i; return b; }
var RATES = [
{ who: "Guessing at a login screen", note: "throttled · 10 tries/sec", lg10: 1 },
{ who: "Stolen database, slow hashing", note: "20 thousand tries/sec", lg10: 4.3 },
{ who: "Stolen database, fast hashing", note: "100 billion tries/sec", lg10: 11 },
{ who: "Rented rack of graphics cards", note: "10 trillion tries/sec", lg10: 13 }
];
function fmtCount(x) {
if (x >= 100) return String(Math.round(x));
if (x >= 10) return x.toFixed(0);
return x.toFixed(1).replace(/\.0$/, "");
}
function unit(v, word) { var t = fmtCount(v); return t + " " + word + (t === "1" ? "" : "s"); }
function fmtTime(l10) {
if (l10 < -0.5) return "instantly";
if (l10 < 0.5) return "about a second";
var s = Math.pow(10, Math.min(l10, 12));
if (l10 < 12) {
if (s < 60) return unit(s, "second");
if (s < 3600) return unit(s / 60, "minute");
if (s < 86400) return unit(s / 3600, "hour");
if (s < 2629746) return unit(s / 86400, "day");
if (s < 31556952) return unit(s / 2629746, "month");
}
var y = l10 - 7.4989; /* log10 of years */
if (y < 3) return unit(Math.pow(10, y), "year");
var big = [[3, "thousand"], [6, "million"], [9, "billion"], [12, "trillion"], [15, "quadrillion"]];
for (var i = 0; i < big.length; i++) {
if (y < big[i][0] + 3) return fmtCount(Math.pow(10, y - big[i][0])) + " " + big[i][1] + " years";
}
return "10^" + Math.round(y) + " years";
}
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function showTok(tok) {
if (hidden) return "•".repeat(Array.from(tok).length);
return esc(tok).replace(/ /g, '<span class="sp">␣</span>');
}
/* ============================================================
6. Plain-language findings
============================================================ */
var TYPENAME = { word: "word", date: "date", seq: "run", rep: "repeat", key: "keyboard", sep: "separator", free: "nothing to say" };
function findings(pw, res) {
var t = [], seen = {}, i, m;
var covered = {}, worst = null;
for (i = 0; i < res.seq.length; i++) {
m = res.seq[i];
if (m.type !== "free" && m.type !== "sep") covered[m.type] = (covered[m.type] || 0) + m.tok.length;
if (m.type === "word" && (!worst || m.why.rank < worst.why.rank)) worst = m;
}
function add(cls, html) { if (!seen[html]) { seen[html] = 1; t.push({ cls: cls, html: html }); } }
if (worst) {
add("d-word", "<span><code>" + showTok(worst.tok) + "</code> is word number <b>" + worst.why.rank +
"</b> on this page's little list, and a real attack list holds millions more. They start at the top and work down." +
(worst.tok.length * 2 >= pw.length ? " One word plus decoration is still one word." : "") + "</span>");
if (worst.why.subs > 0) {
add("d-word", "<span>Swapping letters for lookalikes doesn't buy what people think. Every cracking tool tries <code>@</code>→<code>a</code>, <code>0</code>→<code>o</code>, <code>3</code>→<code>e</code> automatically; it costs them a rule, not a search.</span>");
}
if (worst.why.caps) {
add("d-word", "<span>A capital on the first letter is the most predictable capital there is — that's rule one in the rulebook.</span>");
}
if (worst.why.rev) {
add("d-word", "<span>Spelling it backwards is also a standard rule. It doubles the work, once.</span>");
}
}
if (covered.date) add("d-date", "<span>There are only a few thousand plausible birthdays and about a hundred plausible years, so a date adds roughly <b>7 to 17 bits</b> — less than three random characters.</span>");
if (covered.seq) add("d-seq", "<span>Counting runs like <code>1234</code> or <code>wxyz</code> are generated before anything else, forwards and backwards.</span>");
if (covered.rep) add("d-rep", "<span>A repeat is priced as the thing being repeated plus the number of times. <code>abcabcabc</code> costs barely more than <code>abc</code>.</span>");
if (covered.key) add("d-key", "<span>Walks across the keyboard — <code>qwerty</code>, <code>asdfg</code>, <code>zxcvb</code> — are in every wordlist, because they feel random and aren't.</span>");
if (pw.length && pw.length < 12) {
add("d-shape", "<span>It is <b>" + pw.length + " characters</b>. Length is the cheapest strength you can buy: every extra character multiplies the attacker's work, while extra punctuation only adds to it.</span>");
}
if (/^[A-Z][^A-Z]*[0-9]*[!@#$%^&*?.]?$/.test(pw) && /[0-9!@#$%^&*?.]/.test(pw) && pw.length > 5) {
add("d-shape", "<span>Capital in front, digits and a punctuation mark at the end — that is the shape almost everyone produces when told to “add a number and a symbol”, and cracking tools generate it first.</span>");
} else if (/[0-9]{1,4}[!@#$%^&*?.]?$/.test(pw) && /[a-zA-Z]/.test(pw) && pw.length > 5) {
add("d-shape", "<span>Digits bolted onto the end are the first thing tried after the bare word. They add a couple of bits, not a category.</span>");
}
if (res.capped) {
add("d-shape", "<span>This reads like an ordinary English sentence, so the meter has held the estimate down to <b>" +
res.bits.toFixed(1) + " bits</b>. Running English carries only about <b>1.2 bits per character</b> no matter how long it runs — once you have the first few words, the rest is half-guessable. A sentence you thought up is far weaker than the same number of words drawn at random.</span>");
}
var freeBits = 0, freeChars = 0;
for (i = 0; i < res.seq.length; i++) if (res.seq[i].type === "free") { freeBits += res.seq[i].lg; freeChars += res.seq[i].tok.length; }
var band = bandOf(res.bits);
if (band >= 3) {
t.unshift({ cls: "d-good", html: "<span>Nothing cheap left to find. " + (freeChars
? "The " + freeChars + " unnamed character" + (freeChars === 1 ? "" : "s") + " carry <b>" + freeBits.toFixed(1) + " bits</b> on their own — that part has to be brute-forced."
: "The pieces are numerous and unrelated enough that guessing the arrangement is most of the work.") + "</span>" });
} else {
add("", "<span><b>What actually works:</b> four or more unrelated everyday words you can picture — a badger, a kettle, a bridge, some frost. Long and boring beats short and clever, every time. Hit <b>roll one for me</b> below the field to see the arithmetic.</span>");
}
return t;
}
/* ============================================================
7. UI
============================================================ */
var $ = function (id) { return document.getElementById(id); };
var elPw = $("pw"), elToggle = $("toggle"), elBits = $("bits"), elBand = $("band"),
elFill = $("fill"), elTrack = $("track"), elGuess = $("guesses"), elRates = $("rates"),
elChips = $("chips"), elTips = $("tips"), elPresets = $("presets"), elLive = $("live"),
elLen = $("lenNote"), elRolled = $("rolled");
var hidden = false;
var rolledInfo = null;
var MAXLEN = parseInt(elPw.getAttribute("maxlength"), 10) || 128;
var PRESETS = [
{ label: "Summer2019!", value: "Summer2019!" },
{ label: "Tr0ub4dor&3", value: "Tr0ub4dor&3" },
{ label: "correct horse battery staple", value: "correct horse battery staple" },
{ label: "qwerty123456", value: "qwerty123456" },
{ label: "P@ssw0rd!", value: "P@ssw0rd!" },
{ label: "xK7#mQp2vRt9", value: "xK7#mQp2vRt9" }
];
function randInt(max) {
try {
if (window.crypto && window.crypto.getRandomValues) {
var lim = Math.floor(4294967296 / max) * max, a = new Uint32Array(1);
do { window.crypto.getRandomValues(a); } while (a[0] >= lim);
return a[0] % max;
}
} catch (e) { /* no secure source here — fall through */ }
return Math.floor(Math.random() * max);
}
function roll() {
var perWord = L2(POOL.length);
var need = Math.ceil(60 / perWord);
var picked = [];
for (var i = 0; i < need; i++) picked.push(POOL[randInt(POOL.length)]);
rolledInfo = { words: need, perWord: perWord, real: need * perWord };
elPw.value = picked.join(" ");
render();
elPw.focus();
}
function buildPresets() {
var frag = document.createDocumentFragment();
PRESETS.forEach(function (p) {
var b = document.createElement("button");
b.type = "button"; b.className = "btn"; b.textContent = p.label;
b.addEventListener("click", function () { rolledInfo = null; elPw.value = p.value; render(); elPw.focus(); });
frag.appendChild(b);
});
var r = document.createElement("button");
r.type = "button"; r.className = "btn solid"; r.textContent = "↻ roll one for me";
r.addEventListener("click", roll);
frag.appendChild(r);
elPresets.appendChild(frag);
}
var liveTimer = null;
function announce(text) {
if (liveTimer) clearTimeout(liveTimer);
liveTimer = setTimeout(function () { elLive.textContent = text; }, 550);
}
function render() {
var pw = elPw.value;
var res = analyze(pw);
var bits = res.bits, band = bandOf(bits), bn = BANDS[band].name;
var i;
elLen.innerHTML = pw.length
? '<span class="mono">' + pw.length + " chars · " + res.pieces + " piece" + (res.pieces === 1 ? "" : "s") +
(pw.length >= MAXLEN ? " · at the " + MAXLEN + "-character limit" : "") + "</span>"
: "";
/* headline */
elBits.textContent = pw.length ? bits.toFixed(1) : "0";
elBits.className = pw.length ? "bits mono b" + band : "bits mono";
elBand.textContent = pw.length ? bn : "empty";
elBand.className = pw.length ? "band b" + band : "band";
var pct = Math.max(0, Math.min(1, bits / 90)) * 100;
elFill.style.width = (pw.length ? pct : 0) + "%";
elFill.className = pw.length ? "fill b" + band : "fill";
elTrack.setAttribute("aria-valuenow", pw.length ? Math.min(bits, 90).toFixed(1) : "0");
elTrack.setAttribute("aria-valuetext", pw.length ? bits.toFixed(1) + " bits, " + bn : "nothing typed");
/* guess count */
if (pw.length) {
var l10 = bits * LOG10_2;
var expo = Math.floor(l10), mant = Math.pow(10, l10 - expo);
var approx = expo < 6 ? Math.round(Math.pow(10, l10)).toLocaleString("en-US")
: mant.toFixed(1) + " × 10^" + expo;
elGuess.innerHTML = "2^" + bits.toFixed(1) + " guesses ≈ <b>" + approx + "</b> " +
(approx === "1" ? "try" : "tries") + " to be sure";
} else {
elGuess.textContent = "";
}
/* rolled-passphrase truth panel */
if (rolledInfo && pw.length) {
var gap = bits - rolledInfo.real, verdict;
if (gap > 1.5) {
verdict = "The meter guessed <b>" + bits.toFixed(1) + "</b> — too generous. It has to allow that you " +
"arranged those words yourself, and you didn't: a die did.";
} else if (gap < -1.5) {
verdict = "The meter guessed <b>" + bits.toFixed(1) + "</b> — lower, because the draw happened to land on " +
"words it prices cheaply. The arithmetic above is still the number that counts.";
} else {
verdict = "The meter guessed <b>" + bits.toFixed(1) + "</b>, close enough this time — but only the line above " +
"is a fact; the meter is always guessing.";
}
elRolled.innerHTML =
'<div class="rolled"><div class="k-lab">rolled, so we know the real answer</div>' +
"<b>" + rolledInfo.words + "</b> words drawn at random from this page's <b>" + POOL.length +
"</b>-word pool is exactly <b>" + rolledInfo.real.toFixed(1) + " bits</b> — not an estimate, arithmetic. " +
verdict + " Knowing where your randomness came from beats any score. A proper <b>7,776</b>-word list clears " +
"60 bits in <b>" + Math.ceil(60 / L2(7776)) + "</b> words.</div>";
} else {
elRolled.innerHTML = "";
}
/* crack times */
var rows = "";
for (i = 0; i < RATES.length; i++) {
var r = RATES[i];
var lt = pw.length ? (bits - 1) * LOG10_2 - r.lg10 : -9;
var cls = "";
if (pw.length) {
if (lt < 4.94) cls = " bad"; /* under a day */
else if (band >= 3 && lt > 9.5) cls = " good"; /* over a century, and earned */
}
rows += "<tr><td class=\"who\">" + r.who + "<small>" + r.note + "</small></td>" +
"<td class=\"t" + cls + "\">" + (pw.length ? fmtTime(lt) : "—") + "</td></tr>";
}
elRates.innerHTML = rows;
/* the split */
if (!pw.length) {
elChips.innerHTML = '<span class="k-lab">nothing typed yet</span>';
} else {
var c = "";
for (i = 0; i < res.seq.length; i++) {
var m = res.seq[i];
var lab = m.type === "sep" ? "free" : TYPENAME[m.type] + " · " + m.lg.toFixed(1) + " bits";
c += '<span class="chip t-' + m.type + '"><span class="tok">' + showTok(m.tok) +
'</span><span class="lab">' + lab + "</span></span>";
}
elChips.innerHTML = c;
}
/* tips */
if (!pw.length) {
elTips.innerHTML = '<li><span class="dot"></span><span>Type something in the field above, or pick one of the examples, and the advice appears here.</span></li>';
} else {
var f = findings(pw, res), h = "";
for (i = 0; i < f.length; i++) {
h += '<li' + (f[i].cls === "d-good" ? ' class="good"' : "") + '><span class="dot ' + f[i].cls + '"></span>' + f[i].html + "</li>";
}
elTips.innerHTML = h;
}
announce(pw.length ? bits.toFixed(0) + " bits, " + bn : "empty");
}
/* ---- events ---- */
elPw.addEventListener("input", function () { rolledInfo = null; render(); });
elToggle.addEventListener("click", function () {
hidden = !hidden;
elPw.type = hidden ? "password" : "text";
elToggle.textContent = hidden ? "SHOW" : "HIDE";
elToggle.setAttribute("aria-pressed", hidden ? "true" : "false");
elToggle.title = hidden ? "Show the characters on screen" : "Hide the characters on screen";
render();
elPw.focus();
});
/* platform theme */
window.addEventListener("message", function (e) {
var d = e.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
document.documentElement.setAttribute("data-theme", d.theme);
}
});
/* ---- boot: never an empty form ---- */
buildPresets();
elPw.value = "Summer2019!";
render();
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.