Authentication and Password Security
Every time you log into a laptop, an email account, or a bank app, a system must decide whether you really are who you claim to be. This is the problem of authentication. For decades the dominant solution has been the password — a shared secret that only the legitimate user should know. Passwords seem simple, but designing a system that stores and checks them securely is surprisingly subtle. The history of computer security is littered with breaches caused by getting the details wrong. The stakes are enormous. A single leaked password database can expose millions of accounts. Because people reuse passwords across sites, one breach often cascades into many. Real systems — operating systems, web servers, password managers, corporate single sign-on — all depend on the same underlying primitives: Cryptographic Hash Functions, random salts, and deliberately slow computation. Understanding how these pieces fit together explains why modern password storage is (relatively) safe. It also explains why so many historical systems were not. This article builds the full picture from first principles. It covers what it means to authenticate someone, and why storing plaintext passwords is catastrophic. It explains how hashing and salting defend against offline attacks. It also shows how adversaries actually crack passwords in practice, using dictionaries, rainbow tables, and brute force. We then examine purpose-built slow hash functions. We also explore the tradeoffs system designers must make between security, usability, and computational cost. By the end, you will be able to design (and critique) a password authentication scheme. You will be able to reason about the cost of cracking attacks in concrete numbers. You will also understand why password security is really a special case of a broader field: verifying identity under adversarial conditions. This theme recurs in Access Control Models and in the human-centered challenges of Security Engineering and the Human Factor.
- Authentication verifies identity; passwords are "something you know," one of three classic authentication factors.
- Storing plaintext passwords is never acceptable — a single database leak compromises every user instantly.
- Cryptographic hashing lets systems verify passwords without storing them, but naive hashing is still crackable offline.
- Salting defeats precomputed rainbow tables by forcing attackers to redo work for every single account.
- Slow, memory-hard hash functions (bcrypt, scrypt, Argon2) raise the cost of brute-force and dictionary attacks by orders of magnitude.
- Password security is a cost-asymmetry game: the defender wants verification to be cheap once, but expensive to repeat billions of times.
Secure password storage is not about hiding the password — it is about making every guess an attacker makes as expensive as possible, for every single account independently.
The idea
Authentication is the process by which a system establishes that a claimed identity is genuine, distinct from authorization, which decides what an authenticated identity is allowed to do. Authentication factors are traditionally grouped into three categories: something you know (a password or PIN), something you have (a hardware token or phone), and something you are (a fingerprint or other biometric). Passwords remain the dominant "something you know" mechanism because they require no special hardware and are easy for humans to reason about, even though they are also the weakest link in practice. The central engineering challenge is this: a server must be able to check whether a submitted password matches the one a user originally chose, without ever storing that password in a form that would let an attacker who steals the database immediately recover it. The tool that makes this possible is the cryptographic hash function — a function that is easy to compute in one direction but computationally infeasible to invert, and that produces wildly different outputs for even slightly different inputs (the avalanche effect). Instead of storing a password , the system stores . To verify a login attempt , it recomputes and compares it to the stored value. If the two hashes match, the passwords almost certainly matched too, because a secure hash function makes it infeasible to find two different inputs with the same output (collision resistance). But hashing alone is not enough. If two users choose the same password, they get the same hash — leaking information and enabling precomputed dictionary attacks, where an attacker builds a lookup table of hash values for common passwords once and then reuses it against every stolen database forever. The fix is salting: generating a random string (the salt) per account, storing it alongside the hash, and computing instead of . Salting does not make an individual password harder to guess, but it destroys the economy of scale that made precomputed attacks devastating, forcing the attacker to redo the entire computation separately for every account.
How it works
A password authentication system has two phases: registration (the user picks a password) and verification (the user logs in later). Both phases interact with a stored credential record consisting of a salt and a hash, never the password itself. ``` // ---------- REGISTRATION ---------- function REGISTER(username, password): salt = GENERATE_RANDOM_BYTES(16) // unique per account hash = SLOW_HASH(password, salt, cost) // e.g., bcrypt/Argon2 STORE(username, salt, hash, cost) // password itself is discarded from memory as soon as possible // ---------- VERIFICATION ---------- function LOGIN(username, submitted_password): record = LOOKUP(username) if record == NOT_FOUND: // still perform a dummy hash to avoid timing leaks SLOW_HASH(submitted_password, DUMMY_SALT, DEFAULT_COST) return REJECT candidate_hash = SLOW_HASH(submitted_password, record.salt, record.cost) if CONSTANT_TIME_EQUAL(candidate_hash, record.hash): return ACCEPT else: return REJECT // ---------- SLOW HASH (conceptual, bcrypt-style) ---------- function SLOW_HASH(password, salt, cost): state = INITIALIZE(password, salt) rounds = 2^cost for i in 1..rounds: state = EXPENSIVE_MIX(state) // CPU- and/or memory-hard step return ENCODE(state) ``` Two details in this pseudocode are load-bearing, not stylistic. First, `CONSTANT_TIME_EQUAL` compares hashes in time independent of where they first differ; a naive byte-by-byte comparison that returns early leaks timing information an attacker could exploit to guess the hash byte by byte (a timing side channel). Second, the dummy hash on a missing user prevents an attacker from distinguishing "no such user" from "wrong password" by measuring response time, which would otherwise let them enumerate valid usernames. The `cost` parameter is the crucial defensive knob. Doubling `cost` roughly doubles the work needed for both the legitimate server and any attacker attempting to guess passwords offline. Because the legitimate server only pays this cost once per login attempt (a rare event, dominated by network latency anyway), while an attacker wants to try billions of guesses, tuning `cost` upward hurts the attacker vastly more than the defender in relative terms.
Complexity & correctness
Correctness argument. The verification procedure is correct if and only if two properties of the hash function hold: (1) determinism — always produces the same output for the same salt and password, so a legitimate user's re-entered password always reproduces the stored hash; and (2) second-preimage resistance — given and , it is computationally infeasible to find any with . Property (1) guarantees no false rejections of genuine users (up to negligible hash-collision probability). Property (2) guarantees that an attacker cannot forge a matching hash without knowing the original password, other than by guessing it directly. The salt does not change these two properties; it only affects the cost of exhaustive search, discussed below.
Time complexity of legitimate use. A single login costs applications of the internal mixing step, where is the cost/work-factor parameter — this is by design, since `SLOW_HASH` runs a fixed loop of rounds. For the legitimate server, this is a small constant (typically tuned to 100–500 milliseconds), negligible compared to network round-trip time. Space complexity is per stored credential for classical bcrypt (fixed-size internal state) but can be tuned to memory for memory-hard schemes like Argon2, where is a configurable memory parameter.
Time complexity of attack. Consider an attacker who has stolen a database of salted hashes and wants to test a candidate password against a specific account. Because each account has an independent salt, checking against all accounts costs separate hash evaluations, each — there is no way to amortize the work across accounts, unlike the unsalted case where one hash of reveals every account using that same password instantly (an -amortized lookup once the table is built). Formally, let be the size of a dictionary of candidate passwords the attacker tries. Cracking one specific salted account costs:
while cracking the same password shared unsalted across accounts, using one precomputed table, costs only:
This is the precise sense in which salting multiplies the attacker's total work by a factor of when facing many accounts — the attacker can no longer reuse a single table.
Correctness of the security claim (not just an assertion). Suppose, for contradiction, that an efficient algorithm could recover a random password from significantly faster than exhaustive search over the password space, for a hash function modeled as a random oracle. Then would violate the one-wayness (preimage resistance) assumption that defines a secure cryptographic hash — the same assumption underlying Cryptographic Hash Functions used elsewhere in security (digital signatures, integrity checks). Since no such efficient inversion algorithm is known for standard hash constructions (SHA-256, etc.), the best known generic attack remains brute-force search over the password space, whose cost we can bound explicitly (see Worked Example below). This is why password strength — the entropy of the password itself — ultimately bounds security no matter how good the hash function is; hashing cannot manufacture entropy that was never in the password.
Honest tradeoffs. Raising the cost factor slows attackers but also slows every legitimate login and increases server CPU/memory load at scale (millions of logins per day). Memory-hard functions (scrypt, Argon2) further defend against attackers using cheap, massively parallel GPU or ASIC hardware, because memory bandwidth is harder to parallelize cheaply than raw arithmetic — but they also increase legitimate server memory pressure. There is no free lunch: password security is fundamentally about shifting cost onto attackers at a real, nonzero cost to defenders.
Worked example
Suppose an attacker steals a database from a breached website. The `users` table row for Alice contains:
| Field | Value |
|---|---|
| username | alice |
| salt | `f3a9...` (16 random bytes, unique) |
| hash | `12$f3a9...Kx8p...` (bcrypt output) |
| cost factor | 12 (i.e., = 4096 internal rounds) |
The attacker has no plaintext password, only this record, plus a common dictionary of the 10 million most popular leaked passwords (a realistic real-world list size), plus knowledge that Alice's employer requires passwords of at least 8 characters.
Step 1 — rule out precomputation. Because the salt `f3a9...` is unique to Alice's account, any rainbow table the attacker built in advance (mapping common-password → hash, without a salt) is useless: those precomputed hashes were computed without this salt and will never match.
Step 2 — dictionary attack, one guess at a time. The attacker must, for each candidate password in the 10-million-entry dictionary, compute:
Each bcrypt evaluation at cost 12 takes roughly 250 ms on ordinary server hardware (bcrypt is deliberately not GPU-friendly, so this number does not shrink much on consumer GPUs, unlike a fast hash like an unsalted single MD5).
Step 3 — total time estimate.
With a cluster of 1,000 cores working in parallel, this drops to about 41 minutes — still nontrivial, and this is per account, since Alice's neighbor Bob has a different salt and must be attacked separately from scratch.
Contrast with an unsalted, fast MD5 hash (a real historical mistake made by many early systems): MD5 computes in roughly 1 microsecond, and without a salt the attacker precomputes the dictionary's hashes exactly once, then checks them against every stolen row via a simple hash-table lookup — an check per account after the one-time precomputation. For a breach of accounts, the attacker's total marginal cost per additional cracked account is essentially zero once the table exists. This comparison — 29 CPU-days-per-account versus microseconds-for-all-accounts — is the entire point of salting and slow hashing, made concrete.
Step 4 — what if Alice's password wasn't in the dictionary? Suppose Alice actually chose a random 10-character password drawn from a 62-character alphabet (upper, lower, digits). Its entropy is:
Exhaustive search over this space at bcrypt cost 12 (250 ms/guess, 1,000 cores) requires:
This dramatic gap between "in a 10-million-word dictionary" (29 days) and "random 10 characters" (millions of years) illustrates precisely why password entropy, not just hashing scheme, ultimately determines real-world security — the hash function only sets the per-guess cost, while the password itself sets the size of the space that must be searched.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Storing plaintext passwords | Seems simpler, "we'll add security later" | Instant total compromise on any leak |
| Using a fast general-purpose hash (MD5, SHA-256 alone) | Mistaking "cryptographically secure" for "slow enough" | GPUs compute billions of guesses/second |
| Reusing one global salt for all users | Misunderstanding what salt is for | Precomputed table works across the whole DB |
| Non-constant-time comparison | Using a normal `==` or `memcmp` early-exit | Timing side channel leaks the hash byte by byte |
| Weak or predictable salt (e.g., username) | Treating salt as cosmetic, not security-critical | Attacker regenerates it trivially, table reused |
| No rate limiting on login attempts | Focus only on storage, ignore online guessing | Online brute force against active accounts |
| Ignoring password reuse across sites | Server-side security is fine, but users repeat passwords | One breach compromises accounts on unrelated systems |
Pioneers & history
Password-based login dates to the earliest time-sharing systems, where multiple users shared one machine and needed a way to separate accounts. The core cryptographic ideas that made secure storage possible — one-way functions, salting, and formal notions of computational hardness — trace back to foundational work in cryptography and computer science more broadly.
Origins · 1 minRobert Morris Sr. and how this was discovered
Morris, along with Ken Thompson, designed the original Unix password scheme in the early 1970s, introducing the `crypt()` function and per-user salts to the `/etc/passwd` file — one of the first widely deployed uses of salting specifically to defeat precomputed dictionary attacks.
Where this matters
Password hashing schemes are embedded in nearly every authentication system in production use. Operating systems (Linux's `/etc/shadow`, Windows credential stores), web frameworks (Django, Rails, and most authentication libraries default to bcrypt or Argon2), and password managers (which must securely derive an encryption key from a master password using the same slow-hashing principles) all rely on these ideas directly. Standards bodies such as NIST and OWASP publish explicit guidance recommending Argon2id or bcrypt with tuned cost parameters, and mandate salts of sufficient length and randomness. Beyond raw password storage, the same conceptual toolkit — controlled computational cost as a defensive mechanism — appears in proof-of-work systems, key-derivation functions (PBKDF2 for disk encryption), and CAPTCHA-adjacent anti-abuse systems. Understanding password authentication is also the natural entry point into broader identity and access questions: once a system can verify who someone is, it must still decide what they can do, which is the domain of Access Control Models. And because the overwhelming majority of real-world password breaches stem from human behavior — reuse, weak choices, phishing — rather than cryptographic failure, this topic connects directly to the study of Security Engineering and the Human Factor.
Ask the tutor: How would you design a login rate-limiter that resists both brute-force attacks and denial-of-service lockout abuse against legitimate users?
Why not just encrypt passwords instead of hashing them?
Encryption is reversible by design — whoever holds the decryption key can recover the original plaintext. That is exactly the wrong property for password storage: if the server (or an attacker who compromises it) can decrypt stored passwords, then a single key compromise reveals every user's password at once. Hashing is deliberately one-way, so even the server itself cannot recover the original password, only verify a guess against it. The server never needs the original password again after registration, so reversibility offers no benefit and only additional risk. This is why virtually every security standard mandates one-way hashing, not encryption, for credential storage.
Does salting protect against an attacker who already knows my specific password guess is correct, just checking many accounts for reused passwords?
Salting protects against precomputation and cross-account reuse of computed work, not against a targeted attack once an attacker already suspects a specific password for a specific account. If an attacker guesses "password123" and tests it against your specific salted hash, salting does not slow that single guess down at all — the slow hash function does that. Salting's real value shows up at scale: without it, one popular password shared by thousands of users could be cracked once and matched instantly across the whole database. With salting, each of those thousands of accounts must be attacked independently.
Is bcrypt still considered secure today, or should everyone move to Argon2?
Bcrypt remains reasonably secure and widely deployed, but Argon2 (specifically Argon2id) is the current recommended standard by NIST and OWASP for new systems. The key advantage of Argon2 is configurable memory-hardness: it can be tuned to require large amounts of RAM per guess, which is expensive to parallelize on GPUs and ASICs, whereas bcrypt's memory usage is fixed and relatively small. Systems with legacy bcrypt deployments are not in immediate danger if cost factors are kept current, but new systems should default to Argon2id with parameters tuned to their hardware and threat model.
Why does increasing the cost factor by 1 double the attacker's work, and what does that mean practically?
Cost factors in schemes like bcrypt are typically exponential: cost corresponds to internal rounds. Increasing by one doubles the number of rounds and therefore roughly doubles the time to compute a single hash, for both attacker and defender. Practically, this means server administrators can periodically raise the cost factor as hardware gets faster, keeping the real-world time per guess roughly constant even as CPUs and GPUs improve — a deliberate design response to Moore's-law-style hardware progress, sometimes summarized as "cost factors age the hash function gracefully."
Can a strong hashing scheme fully compensate for a weak, guessable password?
No. A hash function controls the cost per guess, but the size of the space an attacker must search is determined entirely by the password's entropy. A weak password like "password1" sits in a tiny effective search space (common-word dictionaries), so even a very slow hash function only delays cracking by a bounded, often small, factor. Strong hashing meaningfully protects only passwords with sufficient underlying randomness; it cannot convert a low-entropy secret into a high-entropy one. This is why password strength requirements and hashing strategy must be treated as complementary defenses, not substitutes.
How does rate limiting relate to salting and hashing, if at all?
Salting and slow hashing defend specifically against offline attacks, where an attacker has stolen the hash database and can guess as fast as their own hardware allows. Rate limiting (locking an account after repeated failed attempts, adding delays, requiring CAPTCHAs) defends against online attacks, where the attacker must submit guesses through the live login interface and is bounded by the server's willingness to respond. Both defenses are necessary: strong hashing is useless if an attacker can simply try millions of guesses per second directly against the login form, and rate limiting is useless once the hash database itself has been stolen.
Why is the salt stored in plaintext alongside the hash instead of being kept secret?
The salt's job is not to be secret — it is to guarantee uniqueness, so that no two accounts (even with identical passwords) produce the same hash, and so that no precomputed table built for one salt applies to another. The verification step needs the exact salt used at registration to recompute the hash correctly, so it must be retrievable, and storing it openly costs nothing security-wise as long as it is sufficiently random and unique per account. Confusing salts with secret keys is a common misunderstanding; a separate, genuinely secret server-side value (a "pepper") is sometimes added on top for extra defense, but the salt itself is not meant to be hidden.