Python 3 and a terminal. No password of yours is typed, stored or sent anywhere.
macOS and Linux include Python; on Windows install it from python.org with
“Add python.exe to PATH” ticked, then check with
python3 --version.
Do not put a real password of yours into any of these files. Everything here works on examples, and the point is to measure the shape of a password rather than test a particular one. Step 3 takes about ten seconds to run because it is deliberately doing expensive work; that is the finding, not a fault.
Why Passwords Matter
Your password is the first and often the only barrier between an attacker and your personal data. Email accounts, bank accounts, social media profiles, cloud storage -- all of it is protected by passwords. If an attacker obtains your password, they gain the same level of access that you have, and they can act as you.
The consequences of a compromised password go far beyond a single account. Attackers use stolen email credentials to reset passwords on other services, access private conversations, impersonate you to your contacts, and launch further attacks. A single weak or reused password can unravel your entire digital life.
Billions of username and password combinations have been leaked in data breaches over the past decade. Attackers compile these into massive databases and use automated tools to try them against thousands of websites simultaneously. This technique, called credential stuffing, is why reusing passwords is so dangerous.
What Makes a Strong Password
A strong password resists both automated guessing attacks and human intuition. The two primary factors that determine password strength are length and unpredictability. A password that is long but predictable (such as "aaaaaaaaaaaaaaa") is weak. A password that is random but short (such as "k9$Qz") is also weak. You need both qualities working together.
Length Is More Important Than Complexity
A 20-character password made of lowercase letters is significantly harder to crack than an 8-character password with uppercase, lowercase, numbers, and symbols. This is because each additional character multiplies the number of possible combinations exponentially. Modern password cracking hardware can test billions of short passwords per second, but a sufficiently long password takes centuries to brute-force even with unlimited computing power.
Weak passwords (easily cracked):
password123 -- Common word + simple numbers
Summer2024! -- Predictable pattern (Season + Year + Symbol)
qwerty -- Keyboard pattern
john.smith1985 -- Personal information
Strong passwords (resist cracking):
correct-horse-battery-staple -- Long passphrase (4+ random words)
tK8$mP2!vR4@nL9&wQ6 -- Long random string
glacier.notebook.railway.cactus -- Random words with separator
Names, birthdays, pet names, addresses, and phone numbers are the first things attackers try. This information is often publicly available on social media, public records, or previous data breaches. A password built from personal details is not a password -- it is a puzzle with publicly available pieces.
The Passphrase Technique
A passphrase is a password made of multiple random words strung together. Passphrases are both stronger and easier to remember than traditional complex passwords. The key is that the words must be truly random -- not a meaningful sentence, not a quote, and not something you would naturally say.
How to Create a Good Passphrase
The most secure method is to use a word list and a source of randomness. The Diceware method, for example, uses physical dice rolls to select words from a numbered list. Each word adds approximately 12.9 bits of entropy (randomness). A four-word passphrase provides about 51 bits of entropy, and a six-word passphrase provides about 77 bits -- strong enough for almost any purpose.
Generating a Diceware passphrase:
Roll five dice for each word. Look up the result in the Diceware word list.
Roll: 4-1-6-2-3 --> Word: "mutual"
Roll: 2-5-3-1-6 --> Word: "floss"
Roll: 1-3-4-2-5 --> Word: "anvil"
Roll: 6-2-1-4-3 --> Word: "timber"
Roll: 3-5-1-6-2 --> Word: "kayak"
Passphrase: mutual-floss-anvil-timber-kayak
This is 64.6 bits of entropy -- strong enough for most accounts.
Natural language has patterns. If you use a grammatically correct sentence as a passphrase, attackers can exploit the predictable structure of language to dramatically reduce the number of guesses needed. "The cat sat on the mat" feels long but is far weaker than four truly random words because each word in a sentence constrains what can follow it.
Password Managers
A password manager is software that generates, stores, and fills in strong, unique passwords for every account you have. You remember one master password to unlock your password vault, and the manager handles everything else. This is the single most impactful step you can take to improve your password security.
Why You Need One
The average person has over 100 online accounts. It is humanly impossible to memorize a unique, strong password for each one. Without a password manager, people inevitably reuse passwords or use weak, memorable ones. A password manager eliminates this tradeoff entirely by removing the need to remember any password other than one master password.
Recommended Password Managers
- KeePassXC -- Free, open-source, offline. Your vault is a local encrypted file that you control. No cloud sync unless you set it up yourself. Best for users who want full control.
- Bitwarden -- Free tier available, open-source, cloud-synced. Can also be self-hosted. Good balance of convenience and transparency.
- 1Password -- Paid, proprietary, cloud-synced. Polished user experience and good family/team sharing features.
How to Get Started
- Install a password manager and create a strong master password (use the passphrase technique above)
- Install the browser extension so it can auto-fill login forms
- Start with your most critical accounts -- email, banking, and cloud storage
- Generate a new unique password for each account as you log in to it
- Over time, replace every reused password with a unique generated one
Make it a strong passphrase of at least five random words. Memorize it thoroughly. Never write it down in a place that could be discovered. If you forget your master password, you may lose access to your entire vault, so consider storing an emergency copy in a sealed envelope in a secure physical location such as a safe or a safety deposit box.
One Unique Password Per Account
This is the most critical rule of password hygiene: never reuse a password across multiple accounts. Every single account should have its own unique, randomly generated password. No exceptions.
The reason is simple. Data breaches happen constantly, and they are outside your control. When a service gets breached and your password is exposed, attackers immediately try that same email and password combination on hundreds of other services. If you reused that password anywhere, those accounts are now compromised too.
How credential stuffing works:
1. Attacker obtains breach data:
user@example.com : MyPassword123
2. Attacker runs automated tools against:
- Gmail
- Facebook
- Amazon
- Banking sites
- Netflix
- Every other major service
3. Any account using "MyPassword123" is compromised instantly.
If every account has a unique password, a breach on one
service affects only that one service.
Using "MyPassword1" for Gmail and "MyPassword2" for Facebook is not unique -- it is a pattern. Attackers know people do this and their tools account for it. A truly unique password is one that has no relationship whatsoever to any of your other passwords. Let your password manager generate random strings for you.
Checking for Breached Passwords
Even if you follow good practices now, passwords you used in the past may already be floating around in breach databases. The website Have I Been Pwned (haveibeenpwned.com) lets you check whether your email address or passwords have appeared in known breaches.
Checking Your Email
Visit haveibeenpwned.com and enter your email address. The site will tell you if your email appeared in any known data breaches and which services were involved. This does not mean your current passwords are compromised, but it tells you which accounts have had credentials exposed at some point.
Checking Your Passwords
Have I Been Pwned also offers a password checker at haveibeenpwned.com/Passwords. You can enter a password to see if it appears in any breach database. The check is done securely using a technique called k-anonymity -- your full password is never sent to the server. Only the first five characters of its hash are transmitted, and the matching is done locally on your device.
How the k-anonymity password check works:
1. Your password "example" is hashed with SHA-1:
SHA-1("example") = C3499C2729730A7F807EFB8676A92DCB6F8A3F8F
2. Only the first 5 characters are sent to the server:
Request: C3499
3. The server returns ALL hashes starting with C3499
(typically 500-800 hashes)
4. Your browser checks locally if the full hash matches
any in the returned list.
Your actual password never leaves your device.
KeePassXC, Bitwarden, and 1Password all include features that automatically check your stored passwords against known breach databases. Enable this feature and review the results regularly. Any password that appears in a breach should be changed immediately, regardless of how strong it is.
Changing Compromised Passwords
When you discover that a password has been compromised -- whether through a breach notification, a Have I Been Pwned check, or suspicious account activity -- you need to act quickly and methodically.
- Change the compromised password immediately -- Log in to the affected service and change the password to a new, unique, randomly generated one
- Check for unauthorized changes -- Review account settings, recovery email addresses, connected apps, and recent activity for anything you did not do
- Revoke active sessions -- Most services let you sign out of all devices. Do this to terminate any sessions the attacker may have open
- Change the password everywhere you reused it -- If you used the same password on other accounts, change those too. Each one gets its own unique password
- Enable two-factor authentication -- If the account supports 2FA and you have not enabled it, do so now. This adds a second layer that a stolen password alone cannot bypass
- Watch for follow-up attacks -- Attackers who compromised one account may use the information they found to target you further through phishing or social engineering
If your email password was compromised, treat it as the highest priority. Your email account is the master key to your digital life -- it receives password reset links for almost every other service you use. An attacker with access to your email can reset and take over all of your other accounts.
Crack Your Own Password Habits, in Five Steps
Password advice has been repeated so often that most people can recite it and almost nobody has
seen the evidence behind it. That is a problem, because two of the most repeated rules — mix
in symbols and numbers, change it every ninety days — turn out to be much weaker than they
sound once you count properly. In the next twenty minutes you will count the real size of a
password's haystack, crack P@ssw0rd123 on your own machine, measure the difference a
website's choice of hash makes, and check a password against a breach list without revealing it.
Every line of output below came from running these files.
Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.
Do: save this as space.py and run python3 space.py.
"""Two ways to count the haystack: naively, and the way an attacker counts."""
import math
GUESSES_PER_SEC = 10_000_000_000 # a rented GPU rig against a fast hash
def naive(pw):
"""Pretend every character was chosen at random from 95 printable ones."""
return 95 ** len(pw)
def realistic(pw, structure):
"""Count the choices actually made, given how the password was built."""
n = 1
for count in structure:
n *= count
return n
CANDIDATES = [
("Tr0ub4dor&3", [100_000, # a base word from a large word list
2 ** 9, # which letters got leet-substituted
33 * 10]), # one symbol and one digit on the end
("Summer2026!", [500, # season/month/name
100, # year
33]), # trailing symbol
("correct horse battery staple", [7776] * 4), # four words from a 7776-word list
]
def readable(seconds):
for limit, unit, name in ((60, 1, "seconds"), (3600, 60, "minutes"),
(86400, 3600, "hours"), (86400 * 365, 86400, "days")):
if seconds < limit:
return "%.4g %s" % (seconds / unit, name)
years = seconds / 86400 / 365
return "%.4g years" % years if years < 1e6 else "%.2e years" % years
print("%-30s %-22s %s" % ("PASSWORD", "IF EVERY CHAR RANDOM", "AS AN ATTACKER COUNTS IT"))
print("-" * 88)
for pw, structure in CANDIDATES:
n_naive, n_real = naive(pw), realistic(pw, structure)
print("%-30s 2^%-4.1f %-15s 2^%-4.1f %s" % (
pw, math.log2(n_naive), readable(n_naive / GUESSES_PER_SEC),
math.log2(n_real), readable(n_real / GUESSES_PER_SEC)))
print()
ratio = realistic("x", [7776] * 4) / realistic("x", [100_000, 2 ** 9, 330])
print("passphrase vs Tr0ub4dor&3, attacker's count: %.0f times harder" % ratio)
print()
print("The first two columns are why people believe short mangled passwords")
print("are strong. The third column is what actually happens to them.")
You should see: the same passwords surviving eighteen thousand years or one second, depending on how you count:
PASSWORD IF EVERY CHAR RANDOM AS AN ATTACKER COUNTS IT
----------------------------------------------------------------------------------------
Tr0ub4dor&3 2^72.3 1.804e+04 years 2^34.0 1.69 seconds
Summer2026! 2^72.3 1.804e+04 years 2^20.7 0.000165 seconds
correct horse battery staple 2^184.0 7.54e+37 years 2^51.7 4.232 days
passphrase vs Tr0ub4dor&3, attacker's count: 216392 times harder
The first two columns are why people believe short mangled passwords
are strong. The third column is what actually happens to them.
The middle column is the calculation password-strength meters do, and it is
wrong for exactly one reason: it assumes the characters were chosen at random, when in
fact they were chosen by a person applying a rule. Tr0ub4dor&3 is not
eleven random characters; it is one word, plus a decision about which letters to substitute, plus
a symbol and a digit. Count those choices and the space collapses from 2^72 to 2^34.
The right-hand column is what an attacker's software actually enumerates, which is why the last row wins despite looking less “complex”. Four words drawn at random from a list of 7,776 involve four genuinely independent choices, and there is no shortcut through them.
If not: OverflowError or a math domain error means one of the
structure lists contains a zero — every entry must be the number of options at
that position, so it is at least 1.
Go: the same folder.
Do: save this as crack.py and run python3 crack.py. It builds
candidates the way real cracking tools do: a word list, the five substitutions everybody makes,
and the endings everybody adds.
"""A dictionary attack with the mangling rules everyone independently invents."""
import hashlib, itertools, time
WORDS = ["password", "summer", "dragon", "liverpool", "monkey", "welcome",
"letmein", "trustno", "sunshine", "princess"]
SUBS = {"a": "@", "e": "3", "i": "1", "o": "0", "s": "$"}
SUFFIXES = [""] + [str(y) for y in range(1990, 2030)] + ["!", "1!", "123", "123!"]
def leets(word):
options = [[c, SUBS[c]] if c in SUBS else [c] for c in word]
return {"".join(p) for p in itertools.product(*options)}
def candidates():
for w in WORDS:
for base in leets(w):
for cap in {base, base.capitalize()}:
for suf in SUFFIXES:
yield cap + suf
TARGET = hashlib.sha256(b"P@ssw0rd123").hexdigest()
start = time.time()
tried = 0
for guess in candidates():
tried += 1
if hashlib.sha256(guess.encode()).hexdigest() == TARGET:
print("cracked :", guess)
break
else:
print("not found in", tried, "candidates")
print("guesses :", format(tried, ","))
print("wordlist :", len(WORDS), "words")
print("total space searched:", format(sum(1 for _ in candidates()), ","))
print()
print("Every substitution you thought made it clever is one of five rules")
print("that every cracking tool has had built in since the 1990s.")
You should see: the password found in under five hundred guesses:
cracked : P@ssw0rd123
guesses : 494
wordlist : 10 words
total space searched: 8,190
Every substitution you thought made it clever is one of five rules
that every cracking tool has had built in since the 1990s.
Four hundred and ninety-four guesses, from a word list of ten. A real attacker starts with a list of several hundred million real passwords taken from previous breaches, ordered by how often people chose them, and applies thousands of rules rather than five.
This is the finding that should change a habit. The substitutions are not a
secret you invented — they are so universal that they were built into cracking software
before most people reading this had an email address. Replacing o with
0 multiplies the search by two, not by a thousand.
If not: if it prints not found, the target string and the candidate generator
disagree about capitalisation — the target is P@ssw0rd123 with a capital P, and
the generator produces capitalised forms via base.capitalize(). The guess count may
differ slightly from the transcript because Python sets are unordered, so the winning candidate
can be reached at a different position; the order of magnitude is what matters.
Go: the same folder.
Do: save this as hashspeed.py and run python3 hashspeed.py. It
takes about ten seconds — most of that is the slow hash doing its job.
"""Why the site's choice of hash matters more than your password's cleverness."""
import hashlib, time
password = b"correct horse battery staple"
salt = b"a-random-salt-per-user"
def timed(label, fn, n):
start = time.time()
for _ in range(n):
fn()
elapsed = time.time() - start
per_sec = n / elapsed
print("%-34s %12s guesses/second on this machine" % (label, format(int(per_sec), ",")))
return per_sec
fast = timed("sha256, unsalted", lambda: hashlib.sha256(password).digest(), 200_000)
slow = timed("scrypt, n=2^14 r=8 p=1", lambda: hashlib.scrypt(password, salt=salt, n=2**14, r=8, p=1), 20)
print()
print("ratio: the slow one is %s times more expensive per guess"
% format(int(fast / slow), ","))
print()
space = 7776 ** 4 # the four-word passphrase from step 1
for label, rate in (("sha256", fast), ("scrypt", slow)):
days = space / rate / 86400
print("four-word passphrase against %-8s %14.1f days" % (label + ":", days))
print()
print("Same password. The only thing that changed is what the site did with it.")
You should see: a difference of four or five orders of magnitude per guess:
sha256, unsalted 1,842,322 guesses/second on this machine
scrypt, n=2^14 r=8 p=1 22 guesses/second on this machine
ratio: the slow one is 80,514 times more expensive per guess
four-word passphrase against sha256: 22969.2 days
four-word passphrase against scrypt: 1849342119.6 days
Same password. The only thing that changed is what the site did with it.
Your numbers will differ from these and that is expected — the rates depend entirely on the processor you are using. What holds on any machine is the ratio, which is tens of thousands to one, and the consequence in the last two lines.
A password-hashing function such as scrypt, bcrypt or Argon2 is deliberately slow and
deliberately memory-hungry, so that an attacker who steals the database cannot test billions of
candidates per second. sha256 is a fine hash and a terrible password hash, because
being fast is the whole of its design.
You cannot choose which one a website uses. What you can do is stop relying on their choice, which is what steps 4 and 5 are about.
If not: if scrypt raises a memory error, lower n to 2**12 —
the parameters are chosen to consume real memory on purpose, and that is what makes it expensive
to parallelise. The ratio will shrink accordingly, which is itself worth seeing.
Go: the same folder.
Do: save this as kanon.py and run python3 kanon.py.
"""How to ask 'has my password leaked?' without revealing the password."""
import hashlib
# Stand-in for a breach corpus. The real one holds hundreds of millions.
BREACHED = ["password", "123456", "qwerty", "P@ssw0rd123", "letmein",
"monkey", "iloveyou", "dragon", "sunshine", "princess"]
INDEX = {}
for p in BREACHED:
h = hashlib.sha1(p.encode()).hexdigest().upper()
INDEX.setdefault(h[:5], []).append(h[5:])
def check(password):
h = hashlib.sha1(password.encode()).hexdigest().upper()
prefix, suffix = h[:5], h[5:]
bucket = INDEX.get(prefix, []) # <- ONLY the prefix is sent
return prefix, len(bucket), suffix in bucket
for pw in ("P@ssw0rd123", "correct horse battery staple"):
prefix, bucket_size, found = check(pw)
print("password :", pw)
print(" sent to the server:", prefix, "(5 hex characters, nothing else)")
print(" answers returned :", bucket_size)
print(" in a known breach :", "YES -- change it everywhere" if found else "not in this corpus")
print()
print("The server sees 5 characters of a hash. It cannot tell which password")
print("you asked about, and cannot reverse them. You do the final comparison")
print("yourself, on your own machine.")
You should see: one password found in the corpus, having sent five characters:
password : P@ssw0rd123
sent to the server: 0F0D9 (5 hex characters, nothing else)
answers returned : 1
in a known breach : YES -- change it everywhere
password : correct horse battery staple
sent to the server: ABF7A (5 hex characters, nothing else)
answers returned : 0
in a known breach : not in this corpus
The server sees 5 characters of a hash. It cannot tell which password
you asked about, and cannot reverse them. You do the final comparison
yourself, on your own machine.
This is how the breach-checking services you may have used actually work, and it is worth knowing because the obvious objection — “why would I type my password into someone else's website?” — is a good one that the design answers. Your browser hashes the password locally, sends the first five characters of the hash, receives every hash in the corpus that begins with those five, and finishes the comparison on your machine. The server never sees enough to identify which password you asked about.
What a match means: that exact password appears in a published breach corpus, so it is already in the word lists attackers use, and its strength is now zero regardless of how it is constructed. Change it wherever it is used, starting with your email.
If not: if both passwords report not in this corpus, the strings in
BREACHED lost a character; P@ssw0rd123 must match exactly. The prefixes
shown are deterministic, so yours will be identical to these.
Go: the same folder.
Do: save this as reuse.py and run python3 reuse.py.
"""One breach, and then what?"""
ACCOUNTS = {
"old forum (breached 2019)": "sunshine88",
"webmail": "sunshine88",
"online banking": "sunshine88!",
"shopping site": "sunshine88",
"work login": "Sunshine88",
}
leaked = "sunshine88"
def variants(p):
return {p, p + "!", p.capitalize(), p.capitalize() + "!", p + "1", p + "123"}
tried = variants(leaked)
print("password leaked in the 2019 breach:", leaked)
print("variants an attacker tries :", len(tried))
print()
for account, pw in ACCOUNTS.items():
print(" %-28s %s" % (account, "OPENS" if pw in tried else "holds"))
opened = sum(1 for pw in ACCOUNTS.values() if pw in tried)
print()
print("accounts opened by one leaked password:", opened, "of", len(ACCOUNTS))
print()
print("Adding a '!' or a capital does not make a password different. It")
print("makes it the same password with one of six endings a script tries")
print("in the first second.")
print()
print("With unique passwords the same breach opens exactly 1 of 5 -- and")
print("that one is a forum you stopped using in 2019.")
You should see: a single 2019 leak opening every account:
password leaked in the 2019 breach: sunshine88
variants an attacker tries : 6
old forum (breached 2019) OPENS
webmail OPENS
online banking OPENS
shopping site OPENS
work login OPENS
accounts opened by one leaked password: 5 of 5
Adding a '!' or a capital does not make a password different. It
makes it the same password with one of six endings a script tries
in the first second.
With unique passwords the same breach opens exactly 1 of 5 -- and
that one is a forum you stopped using in 2019.
Note which accounts fell to a variant rather than the exact string. Banking used
sunshine88! and work used Sunshine88 — both felt like different
passwords to the person choosing them, and neither survived a list of six obvious endings. This
is called credential stuffing, it is automated, and it is why a breach at a forum you forgot about
turns into a bank problem years later.
The single change worth making today is a password manager, because it makes uniqueness free. You stop choosing passwords, so they stop having patterns; every site gets a different random string; and one breach becomes one problem. The two passwords you still have to remember are the manager's own and your email account's, and both should be passphrases of four or more random words, as measured in step 1.
If not: if fewer accounts open, an entry in ACCOUNTS was changed — the
point of the exercise is that all five are variants of one another. Adding a genuinely different
password to the list is a worthwhile experiment: it will report holds.
Without scrolling up: your workplace requires passwords of at least twelve characters
with an uppercase letter, a number and a symbol, changed every ninety days. A colleague says this
is best practice. Which parts of it help, which part is counter-productive, and why? Answer: the length requirement helps, and it is the only part doing serious work — step 1
showed that independent choices are what enlarge the search space, and length is how you get them.
The composition rules help far less than they appear to: forcing an uppercase letter and a symbol
mostly produces a capital at the front and an exclamation mark at the end, which is one of the
first patterns cracking software tries, as step 2 demonstrated in under five hundred guesses. The
counter-productive part is the ninety-day expiry. People asked to change a password frequently do
not invent a new one; they increment the old one, so Sunshine88! becomes
Sunshine89! — and step 5 showed those variants are the same password as far as
an attacker is concerned. It also encourages writing them down insecurely. Modern guidance,
including NIST's, therefore recommends long passwords, screening against known-breached lists as
in step 4, and forced changes only when there is evidence of compromise.
Now do it without the page: add your own habitual password shape to space.py — not the
password, the structure: how many words or names you draw from, how many endings you use,
how many substitutions. Count the choices honestly and see where it lands in the third column.
Then do the part that actually helps: install a password manager, and move just three accounts
into it — your email first, because every other account can be reset through it.
Summary
Good password hygiene is not about memorizing complicated strings. It is about using the right tools and habits so that every account is protected by a strong, unique password without relying on your memory.
- Passwords are your primary defense -- a single compromised password can cascade into multiple account takeovers
- Length beats complexity -- a long passphrase of random words is stronger and easier to remember than a short complex string
- Use the Diceware method to generate truly random passphrases when you need to memorize a password
- Use a password manager for everything else -- let it generate and store unique passwords for every account
- Never reuse passwords -- not even with minor variations. One breach should only compromise one account
- Check haveibeenpwned.com regularly to find out if your credentials have appeared in known breaches
- Change compromised passwords immediately and revoke active sessions on affected accounts
You do not need to fix everything at once. Install a password manager today, secure your email account with a strong unique passphrase and two-factor authentication, then gradually work through your other accounts. Every password you update is one fewer vulnerability in your digital life.