Skip to main content
PProfileMatch

Cybersecurity Fundamentals: Passwords, MFA and Password Hashing

CybersecurityLesson 3 of 46 min read

What you'll learn

  • Choose password and password-manager habits that survive real-world breaches
  • Compare MFA options and know which are strongest
  • Explain why passwords must be hashed with a salt and a slow algorithm
  • Write and verify a salted scrypt hash in Python using hashlib and hmac

On this page

  1. What makes a password good in practice
  2. Multi-factor authentication, ranked
  3. Why unsalted fast hashes fail
  4. A runnable salted-hash example
  5. Common mistakes
  6. How this shows up in real projects and interviews

A city bike-share startup stores its riders' passwords as plain SHA-256 hashes, and that feels safe: nobody can read a hash. Then a contractor's laptop is stolen with a copy of the user table. Within the hour, every rider who picked a common password is exposed, and two riders who happened to choose the same password show identical hash values side by side. This lesson explains what went wrong on the server side and what each person can do on theirs.

What makes a password good in practice

Current guidance from standards bodies has moved away from forced complexity rules and frequent rotation, because they push people toward predictable patterns such as Summer2025! followed by Autumn2025!. What matters is length, uniqueness and not being on a list of already-leaked passwords. A passphrase of four or five unrelated words is long and easy to type. A password manager makes uniqueness realistic: one strong master passphrase protects a vault of random passwords you never need to remember.

  • Use a different password for every account, generated by a password manager.
  • Protect the manager itself with a long passphrase and MFA.
  • Change a password when you learn of a breach or suspect compromise, not on a calendar.
  • Keep recovery methods (backup email, recovery codes) as secure as the account itself, since attackers go for the weakest door.

Why does length beat cleverness? Because guessing difficulty grows with every unit of true randomness. A passphrase made of random words from a large list gains the same amount of strength per word, and a few extra words add a lot. The numbers below come from simple arithmetic: the entropy of a random pick is the base-2 logarithm of the number of choices, multiplied by how many picks you make. This only holds when the words are chosen randomly by a generator, not by you.

entropy.py
import math

WORDLIST_SIZE = 7776  # a diceware-style list: 6**5 words, picked uniformly at random

for words in (3, 4, 5, 6):
    bits = words * math.log2(WORDLIST_SIZE)
    print(f"{words} random words -> about {bits:.0f} bits of entropy")

# For comparison: 8 truly random printable characters (94 possible symbols each)
print(f"8 random printable characters -> about {8 * math.log2(94):.0f} bits")

Output

3 random words -> about 39 bits of entropy
4 random words -> about 52 bits of entropy
5 random words -> about 65 bits of entropy
6 random words -> about 78 bits of entropy
8 random printable characters -> about 52 bits

Four random words match eight random symbols, yet are far easier to type and remember, and each added word multiplies the work of guessing by thousands. Human-chosen passwords are much weaker than these figures, since people pick predictable words, so let the manager generate them.

Multi-factor authentication, ranked

MFA asks for something more than the password: something you have or something you are. It does not make accounts invulnerable, but it turns a leaked password from a full compromise into an inconvenience for the attacker.

MethodStrengthsWeaknesses
SMS codeFamiliar, works on any phoneSIM swapping, interception, phishable
Authenticator app (TOTP)Works offline, no phone number tied to itCodes can still be typed into a fake login page
Push approvalOne tapFatigue attacks: users approve a prompt just to make it stop
Security key or passkeyBound to the real site, resistant to phishingNeeds a device; recovery planning matters

A practical order of effort: turn on MFA for email first (it resets everything else), then your password manager, then banking, then work accounts. Where a security key or passkey is offered, prefer it.

Why unsalted fast hashes fail

Applications should never store passwords, only a value derived from them. A hash function turns a password into a fixed-size digest and cannot be run backwards. But two weaknesses remain in the bike-share design. First, a general-purpose hash such as SHA-256 is designed to be fast, so an attacker with a stolen table can test billions of guesses per second on ordinary hardware. Second, without a salt, identical passwords give identical digests, so cracking one reveals every user with the same password, and precomputed tables work across all users.

The fixes are a salt (a unique random value per user, stored next to the hash) and a key derivation function that is deliberately slow and memory-hungry, so each guess costs real resources. Python's standard library offers hashlib.scrypt and hashlib.pbkdf2_hmac; dedicated libraries implement bcrypt and Argon2, which many teams prefer. Use whichever your framework recommends, and never invent your own scheme.

A runnable salted-hash example

This program first shows the unsalted problem, then hashes with scrypt and a random 16-byte salt, and verifies a login using hmac.compare_digest so the comparison time does not leak how many leading bytes matched. The cost parameters (N, r, p) are stored with the hash so you can raise them later and still verify old records.

hash_demo.py
import hashlib
import hmac
import os

# 1. The mistake: a fast, unsalted hash. Equal passwords give equal digests.
def weak_hash(password: str) -> str:
    return hashlib.sha256(password.encode()).hexdigest()

print("unsalted, same password for two users:")
print(weak_hash("blue-kettle-42") == weak_hash("blue-kettle-42"))

# 2. The fix: a per-user random salt and a deliberately slow key derivation function.
N, R, P = 2**14, 8, 1  # scrypt cost parameters; raise them as hardware allows

def hash_password(password: str) -> str:
    salt = os.urandom(16)
    digest = hashlib.scrypt(password.encode(), salt=salt, n=N, r=R, p=P, dklen=32)
    return f"scrypt${N}${R}${P}${salt.hex()}${digest.hex()}"

def verify_password(password: str, stored: str) -> bool:
    _, n, r, p, salt_hex, digest_hex = stored.split("$")
    candidate = hashlib.scrypt(
        password.encode(), salt=bytes.fromhex(salt_hex),
        n=int(n), r=int(r), p=int(p), dklen=32,
    )
    return hmac.compare_digest(candidate, bytes.fromhex(digest_hex))

stored_a = hash_password("blue-kettle-42")
stored_b = hash_password("blue-kettle-42")

print("\nsalted, same password for two users:")
print("stored values identical:", stored_a == stored_b)
print("record format:", stored_a.split("$")[0], "with", len(stored_a.split("$")), "fields")
print("right password accepted:", verify_password("blue-kettle-42", stored_a))
print("wrong password accepted:", verify_password("blue-kettle-43", stored_a))

Output

unsalted, same password for two users:
True

salted, same password for two users:
stored values identical: False
record format: scrypt with 6 fields
right password accepted: True
wrong password accepted: False

The salt and digest values change on every run, which is why the program prints comparisons rather than the raw values. Note what is not here: no encryption, no key to protect, no way to "recover" a forgotten password. A reset flow issues a new one instead, which is a feature.

Common mistakes

  • Hashing with MD5 or plain SHA-256. Symptom: after a table leak, large fractions of passwords are recovered quickly. Fix: a slow, salted KDF (scrypt, bcrypt, Argon2).
  • Encrypting passwords instead of hashing. Symptom: whoever steals the key and the table gets every password at once. Fix: hash, and never store a decryptable form.
  • Comparing digests with `==`. Symptom: a subtle timing difference that can reveal progress. Fix: hmac.compare_digest.
  • One global salt (a "pepper" misused as the only salt). Symptom: identical passwords again produce identical hashes. Fix: a random salt per user; an additional secret pepper is optional on top.
  • Truncating or silently limiting length. Symptom: users with long passphrases log in with only the first 72 characters accepted, or get a vague error. Fix: know your algorithm's limits and tell users clearly.

How this shows up in real projects and interviews

"How would you store passwords?" is one of the most common security questions in backend interviews, and a strong answer mentions salting, a slow algorithm, constant-time comparison and a migration plan. In real projects you will rarely implement hashing yourself; you will configure a framework and review that no one logs plaintext passwords or emails them. For wider interview preparation see how to crack FAANG interviews.

Try it yourself

Extend hash_demo.py so verify_password also returns a flag when the stored cost parameters are lower than your current N, R and P, meaning the hash should be upgraded after a successful login. Print the flag for a record created with N = 2**12.

Show a hint

Return a tuple (ok, needs_upgrade). Parse the stored n, r and p as integers and compare them with the constants. Only re-hash and save when ok is True, because you only have the plaintext password at that moment.

Key takeaways

  • Length, uniqueness and leak-checking beat complexity rules and forced rotation.
  • Turn on MFA for email first, and prefer security keys or passkeys where available.
  • Store passwords only as salted, slow hashes (scrypt, bcrypt or Argon2), never encrypted and never plain.
  • A per-user random salt makes identical passwords produce different stored values.
  • Compare secrets with a constant-time function such as hmac.compare_digest.

Test yourself

A trick from this lesson's ride: Length and uniqueness beat complexity and rotation. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

How do I store passwords securely in a database?

Never store the password itself. Store the output of a slow, salted password hashing function such as Argon2, bcrypt or scrypt, with a unique random salt per user, and compare logins using a constant-time check. Keep the algorithm parameters with each record so you can upgrade later.

What is the difference between hashing and encryption?

Encryption is reversible with a key. Hashing is one-way: you cannot recover the original from the digest. Passwords should be hashed, because the server only needs to check a guess, never to read the password back.

Is a password manager safe to use?

For most people it is much safer than reusing passwords. Protect the vault with a long passphrase and MFA, keep the app updated, and store recovery information carefully. A breach of one site then does not expose your other accounts.