Password Strength Analyser — JavaScript source
Estimate password strength with zxcvbn - realistic dictionary and pattern cracking with crack-time estimates and improvement suggestions. Runs entirely in your browser.
This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
/**
* password-strength-analyser - password strength scoring & feedback.
*
* Language: JavaScript (ES2020+, runs unmodified in Node 16+ and modern browsers)
* Source: CosmoDev polyglot showcase port of the Password Strength Analyser
* tool, ported from cli/password-strength-analyser/password-strength-
* analyser.go (the live Go twin, which wraps zxcvbn) and
* src/lib/password-strength.ts (the canonical TS lib).
* License: display source - part of CosmoDev's polyglot tool pages.
*
* Design goals:
* - Pure + deterministic; never throws on odd input.
* - Self-contained: stdlib only (no npm dependencies - no `zxcvbn`).
* - Mirrors the Go twin's public API and result shape: `analyse` returns an
* object with `score` (0-4), `label`, `entropy` (bits), and a single
* crack-time display string.
*
* Parity note: the live Go and TS twins both delegate scoring to `zxcvbn`, a
* dictionary + pattern estimator whose ranking data is far too large to vendor
* in a stdlib display port. This snippet reproduces the SAME 0-4 scale and the
* SAME public contract with a self-contained heuristic estimator:
* (a) a curated common-password blocklist - forces the obvious weak passwords
* (password, 123456, qwerty, ...) to score 0, exactly as zxcvbn's
* dictionary does;
* (b) a character-pool entropy estimate (length × log2(pool));
* (c) penalties for low symbol variety, keyboard / numeric sequences, and
* user-supplied related tokens (the `userInputs` seed zxcvbn accepts).
* It agrees with zxcvbn on the clear cases - common passwords score 0, long
* diverse passphrases score 3-4 - and is a reasonable approximation in between.
* This is a deliberate, documented trade-off to keep the port dependency-free;
* for exact scores use the live Go/TS twin.
*/
'use strict';
/**
* Strength breakdown shape.
* @typedef {Object} Analysis
* @property {0|1|2|3|4} score 0 (worst) … 4 (best), identical scale to zxcvbn.
* @property {string} label Human-readable strength label.
* @property {number} entropy Estimated guess entropy, in bits.
* @property {string} crackTimeDisplay Single human-readable crack-time string (offline-fast model).
*/
/**
* Strength labels indexed by score. Identical to the Go/TS twins.
*/
const LABELS = ['Very weak', 'Weak', 'Fair', 'Good', 'Strong'];
/**
* Curated common passwords (lowercased). An exact match forces score 0 - the
* observable effect of zxcvbn's dictionary for the most leaked passwords. A
* small, hand-picked set rather than zxcvbn's ~30k-entry list: enough to make
* the showcase behaviour meaningful without vendoring a data file.
*/
const COMMON = new Set([
'1234', '12345', '123123', '123456', '12345678', '123456789', '1234567890',
'000000', '111111', '654321', '666666', 'abc123', 'admin', 'baseball',
'batman', 'dragon', 'football', 'iloveyou', 'letmein', 'login', 'master',
'monkey', 'passw0rd', 'password', 'password1', 'princess', 'qwerty', 'root',
'shadow', 'superman', 'sunshine', 'trustno1', 'welcome',
]);
/**
* Keyboard rows and numeric / alphabetic runs. Any length-4 window of these
* appearing inside the password signals an easy-to-guess sequence and incurs
* an entropy penalty.
*/
const SEQUENCES = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm', '1234567890', '0987654321', 'abcdefg', 'gfedcba'];
/** Min length for a sequence window to count as a match. */
const SEQ_MIN_LEN = 4;
/**
* Entropy thresholds mapping a bit count to the 0-4 score. Shared across the
* whole polyglot showcase so every port lands on the same bucket.
*/
const THRESHOLDS = [28.0, 36.0, 60.0, 128.0];
/**
* Tally the distinct character-class buckets present and return the combined
* guess-pool size (lower, upper, digit, ASCII symbols, other). Iterates UTF-16
* code units via spread; for the BMP-only inputs this tool targets, code units
* and code points coincide.
*
* @param {string} password
* @returns {number}
*/
function poolSize(password) {
let lower = false;
let upper = false;
let digit = false;
let sym = false;
let other = false;
for (const c of password) {
if (c >= 'a' && c <= 'z') {
lower = true;
} else if (c >= 'A' && c <= 'Z') {
upper = true;
} else if (c >= '0' && c <= '9') {
digit = true;
} else if (c.charCodeAt(0) <= 0x7f) {
sym = true; // ASCII punctuation or whitespace (not alphanumeric)
} else {
other = true; // any non-ASCII code point
}
}
let pool = 0;
if (lower) pool += 26;
if (upper) pool += 26;
if (digit) pool += 10;
if (sym) pool += 33; // printable ASCII non-alnum: 32 punctuation + space
if (other) pool += 128; // broad bucket for the rest of Unicode
return pool;
}
/**
* Report whether `lower` contains any length-`minLen` window drawn from `seq` -
* i.e. a keyboard/numeric run of consecutive symbols.
*
* @param {string} lower
* @param {string} seq
* @param {number} [minLen=SEQ_MIN_LEN]
* @returns {boolean}
*/
function containsSequence(lower, seq, minLen = SEQ_MIN_LEN) {
if (seq.length < minLen) return false;
for (let i = 0; i <= seq.length - minLen; i++) {
if (lower.includes(seq.slice(i, i + minLen))) return true;
}
return false;
}
/**
* Human-readable crack-time for an offline fast attack (1e10 guesses/sec),
* mirroring zxcvbn's `offline_fast_hashing_1e10_per_second` display. Works in
* log-space so large entropies (2^200 ≈ 1e60 guesses) stay finite.
*
* @param {number} entropy
* @returns {string}
*/
function crackTimeDisplay(entropy) {
if (entropy <= 0.0) return 'instant';
const log10Seconds = entropy * Math.LOG10E * Math.LN2 - 10.0; // entropy·log10(2) − 10
if (log10Seconds < 0.0) return 'instant';
const seconds = 10 ** log10Seconds;
if (seconds < 1.0) return 'instant';
const minute = 60.0;
const hour = 3600.0;
const day = 86400.0;
const month = 2592000.0; // 30 days
const year = 31536000.0; // 365 days
if (seconds < minute) return `${Math.round(seconds)} seconds`;
if (seconds < hour) return `${Math.round(seconds / minute)} minutes`;
if (seconds < day) return `${Math.round(seconds / hour)} hours`;
if (seconds < month) return `${Math.round(seconds / day)} days`;
if (seconds < year) return `${Math.round(seconds / month)} months`;
const years = seconds / year;
if (years < 1000.0) return `${Math.round(years)} years`;
if (years < 1e6) return `${Math.round(years / 1e3)} thousand years`;
if (years < 1e9) return `${Math.round(years / 1e6)} million years`;
return 'centuries';
}
/**
* Score a password's strength. `userInputs` seeds the estimator with related
* tokens (username, site name, ...) - a substring match weakens the score,
* mirroring the `userInputs` parameter of the Go/TS twins.
*
* @param {string} password
* @param {string[]} [userInputs=[]]
* @returns {Analysis}
*/
function analyse(password, userInputs = []) {
// Empty → trivially weak.
if (!password) {
return { score: 0, label: LABELS[0], entropy: 0.0, crackTimeDisplay: 'instant' };
}
const length = [...password].length; // code-point count via the iterator
const pool = poolSize(password);
const logPool = pool > 1 ? Math.log2(pool) : 0.0;
// Base entropy: length × log2(pool), capped by symbol variety so heavy
// repetition ("aaaaaaaa") collapses instead of accruing length credit.
const raw = length * logPool;
const unique = new Set(password).size;
const variety = unique < length && pool > 1
// Repeats earn only a quarter credit: U full picks + 0.25× the repeats.
? (unique + (length - unique) * 0.25) * logPool
: raw;
let entropy = pool > 1 ? Math.min(raw, variety) : 0.0;
// Lowercased view for dictionary / sequence / user-input matching.
const lower = password.toLowerCase();
// (a) common-password blocklist forces score 0 and zero entropy, the way
// zxcvbn's dictionary collapses a known leak.
const common = COMMON.has(lower);
if (common) {
entropy = 0.0;
} else {
// (b) keyboard / numeric sequence penalty.
for (const seq of SEQUENCES) {
if (containsSequence(lower, seq)) entropy -= 12.0;
}
// (c) user-input relatedness penalty.
for (const ui of userInputs) {
const l = String(ui).toLowerCase();
if (l.length >= 3 && lower.includes(l)) entropy -= 10.0;
}
if (entropy < 0.0) entropy = 0.0;
}
// Map entropy (and a minimum-length floor) to the 0-4 bucket.
let score;
if (length < 4 || entropy < THRESHOLDS[0]) {
score = 0;
} else if (entropy < THRESHOLDS[1]) {
score = 1;
} else if (entropy < THRESHOLDS[2]) {
score = 2;
} else if (entropy < THRESHOLDS[3]) {
score = 3;
} else {
score = 4;
}
return {
score,
label: LABELS[score],
entropy,
crackTimeDisplay: crackTimeDisplay(entropy),
};
}
// CommonJS export so the file is consumable from Node without a build step,
// while staying dependency-free and framework-agnostic.
module.exports = { analyse, poolSize, containsSequence, crackTimeDisplay, LABELS, COMMON, SEQUENCES };
// ---------- showcase tests (run with: node js.js) ----------
if (require.main === module) {
const assert = require('assert');
let r = analyse('password');
assert.strictEqual(r.score, 0);
assert.strictEqual(r.label, 'Very weak');
const strong = analyse('correct horse battery staple');
assert.ok(strong.score >= 3, `expected >= 3, got ${strong.score}`);
assert.ok(analyse('123456').score <= strong.score);
assert.ok(analyse('aB3$xK9p').entropy > analyse('aaaaaaaa').entropy);
for (const pw of ['password', '12345678', 'monkey', 'Tr0ub4dour&3', 'correct horse battery staple', 'u2#9Xq!Lp$7wZ']) {
const row = analyse(pw);
assert.ok(row.score >= 0 && row.score <= 4, `${pw} scored ${row.score}`);
assert.ok(row.label.length > 0);
assert.ok(row.crackTimeDisplay.length > 0);
}
assert.ok(analyse('cosmolabs2024', ['cosmolabs']).score <= analyse('cosmolabs2024').score);
console.log('all showcase tests passed');
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →