4.2 - Authentication
Cryptographic Hash Functions and Password Storage
What Is a Hash Function?
A cryptographic hash function takes input of any length and turns it into a fixed-length output called a hash (or digest). Common ones include MD5 (128-bit, deprecated and insecure), SHA-1 (160-bit, also deprecated), SHA-256 and SHA-512 (currently considered secure), NTHash (used for Windows passwords), and RIPEMD-160.
Example: hashing "password123" produces one specific string of characters, but hashing "password124", just one character different, produces a completely unrelated-looking output.
Here's what that looks like with SHA-256, a 256-bit hash. Hashes are usually written in hexadecimal, where each character stands for 4 bits, so every SHA-256 hash is 64 characters long:
Input: password123 SHA-256: ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f Input: password124 SHA-256: 33631376724e5d5480fa397dfcf03b66ad47b934ab495174d7058c38f2bb0087 Input: password123 SHA-256: ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f Input: The red fox jumps over the blue dog SHA-256: 12d6ae1b4a5c1c99ba6e4d60c22c42abe56d0e08b3111222880606a12a9b5aab
The same input always gives the same hash (the first and third). Changing one character changes the whole hash (the first and second). And a whole sentence comes out exactly as long as an 11-character password, since the output length is fixed. The output goes by several names: a hash, hash output, checksum, message digest, or just digest. That's why a cryptographic hash function is also called a message digest function.
Hash Properties
A hash of a certain bit length has a fixed number of possible outputs (a 256-bit hash has 2^256 possibilities, an astronomically large number). Good hash functions are collision-resistant, meaning it's extremely hard to find two different inputs that produce the same output, even though collisions must mathematically exist somewhere since inputs are infinite and outputs aren't. They're also pre-image resistant, meaning you can't work backward from a hash to figure out the original input, it's a one-way function. The same input always produces the exact same hash every time (repeatability), and even a tiny change to the input completely changes the output, an effect called the avalanche effect.
In general, an n-bit hash has possible outputs. MD5 is a 128-bit hash, so it has (about ) possible outputs. That's huge but still limited, while the number of possible inputs is infinite, so some different inputs have to share a hash. Two different inputs that produce the same hash are called a collision.
Each of the four properties is part of why hashes work so well for storing passwords:
Property | What it means | Why it matters for passwords |
Collision resistance | It's hard to find two different inputs that give the same hash | An adversary can't easily make up some other string that would also log in |
Pre-image resistance | Given a hash, it's infeasible to figure out the input | A stolen hash doesn't give away the password |
Repeatability | The same input always gives the same hash | The system can check a login by hashing what was typed and comparing |
Fixed length | Every hash from a given function is the same length | Every password takes the same space to store, and the hash doesn't reveal how long the password is |
MD5 and SHA-1 are deprecated because researchers have found efficient ways to force collisions, breaking their security guarantees; SHA-256 and SHA-512 are the current standard for anything security-sensitive.
Password Storage Using Hashes
Storing plaintext passwords is a disaster waiting to happen, since anyone who steals the database instantly has every password. The fix is to store hashes instead: when a user creates a password, the system hashes it and stores only the hash. When they log in later, the system hashes what they typed and compares it to the stored hash, if they match, access is granted. If an attacker steals the database, they only get hashes, and thanks to pre-image resistance, they can't reverse those back into actual passwords.
The salting problem: without a unique salt, two users with the same password end up with identical hashes, which tells an attacker they share a password, letting them crack one and get both. A salt is random data added to the password before hashing, unique per user, so identical passwords produce completely different hashes for different users. This means identical passwords don't look identical anymore, it defeats rainbow table attacks (explained below), and it forces an attacker to crack each password individually instead of all at once.
The salt isn't a secret. It's stored right next to the hash, since the system needs it at every login: it adds that user's salt to whatever was typed, hashes the result, and compares. Example: two users in the same user:password database both pick
Summer2026!. With different salts, their stored hashes have nothing in common:User Salt Stored hash = SHA-256(salt + password) jortiz 3f9a1c07 891843cec36f3cc7a0e1a9bacb2be488bf426de6d0bbe2365853d83f923cb9cb mwang d24e88b1 0aa5132a53636b73436f3f86f31f7092a04003382a603949c4188a0d282bfb33
Without salt, both rows would store the same hash (
a5c95e887dbf...), and cracking one would crack both.Password Attack Methods
If an attacker gets valid credentials and there's no MFA, they can access systems with full privileges, read, modify, or delete data, impersonate the user, and use that access as a springboard for further attacks.
Online attacks try credentials against a live login system, but they're limited by account lockouts, network delays, logging that can catch them, and rate limiting. Offline attacks work against a stolen user:password database on the attacker's own hardware, meaning no lockouts, unlimited attempts, blazing speed (millions to billions of guesses per second, depending on the hash), and no way for the target organization to even detect it's happening.
Credential reuse and breaches: people reuse passwords across services despite constant warnings. When a site's user database gets stolen, the usernames, emails, and passwords usually get sold to other adversaries or posted online, and adversaries often start an attack on someone by trying that person's stolen or leaked credentials on their email, VPN, banking, or social media. If they reused the password anywhere, the attacker gets in. The 2012 LinkedIn breach stole about 117 million passwords (stored as unsalted SHA-1 hashes), and when the full set went up for sale in 2016, attackers used it to break into other accounts where people had reused those passwords. The fix is unique passwords everywhere, which is exactly what password managers are for.
Password spraying tries one common password across many accounts, avoiding lockouts since each account only gets tried once, and it works because plenty of users pick predictable passwords. Trying something like "Welcome2024!" against 500 accounts might fail on most but succeed on a handful whose users happened to pick that exact common password, all without tripping any lockout.
Credential stuffing tries credentials the adversary already has instead of guessing new ones: either common default credentials or account credentials stolen in an earlier breach. Default credentials are the big risk on devices that never had them changed, things like a router still set to admin/admin or a camera left on its factory password. Attackers scan the internet for exposed devices, identify the model, look up its default credentials, and try them. The Mirai botnet in 2016 compromised over 600,000 IoT devices this way and used them to launch massive DDoS attacks. The fix is simple: change default credentials immediately after setup. For stolen credentials, the fix is a unique password for every account, plus MFA. (Outside the AP course, "credential stuffing" usually means only stolen username and password pairs, and guessing factory defaults goes by other names, but the CED counts both.)
How offline cracking works: hashes can't be reversed, so hash-cracking tools don't try to undo them. Instead, the tool takes a guess, hashes it with the same hash function (and the same salt, if there is one), and compares the result to the stolen hash. No match, next guess. A match means the guess is the password, or at least a string that produces the same hash, which logs in just as well. Brute force and dictionary attacks are two ways of picking the guesses:
Brute force (offline) systematically tries every possible combination. Length matters enormously here. Assume an attacker with a fast GPU setup that can try about 10 billion guesses per second against a fast hash (real speeds depend a lot on the hardware and the hash). At that speed, every 4-character lowercase-only password can be tried in a fraction of a second, while trying every 8-character password mixing upper, lower, numbers, and symbols takes about 7 days, since each added character multiplies the number of combinations (by 94 when all four character sets are allowed).
Dictionary attacks (offline) test a curated list of likely passwords instead of every possible combination, things like common passwords, known patterns, previously leaked passwords, dictionary words, and common substitutions like "P@ssw0rd." Since somewhere around 10 to 15% of users pick passwords straight out of common lists, this can crack a password in seconds instead of the years brute force might take.
Rainbow table attacks use a precomputed table mapping millions of passwords to their hashes, sorted for fast lookup, so instead of computing hashes on the fly, an attacker just looks up the stolen hash and instantly gets the matching password. These tables take a long time to build once, but can then be reused instantly against many different targets. Salting defeats this entirely, since without a salt, one rainbow table works against everyone using a given password, but with unique salts, an attacker would need a completely separate rainbow table for every single user, which is completely impractical to generate at scale.
Example: an adversary steals this unsalted user:password database, where the passwords were hashed with MD5:
Username | Password hash (MD5) |
amorgan | 16b6f879a4b91ec9bf95c294a853a147 |
bkim | 3fc0a7acf087f549ac2b266baf94b8b1 |
cdavis | aadccb7b8dca886770ce566fbdb23655 |
They already have a rainbow table built from common passwords, sorted by hash:
Password hash (MD5) | Password |
0d107d09f5bbe40cade3de5c71e9e9b7 | letmein |
3fc0a7acf087f549ac2b266baf94b8b1 | qwerty123 |
7c6a180b36896a0a8c02787eeafb0e4c | password1 |
8621ffdbc5698829397d97767ac13db3 | dragon |
e10adc3949ba59abbe56e057f20f883e | 123456 |
e56f3bb5392369b6468e744ab1da078b | Summer2026! |
f25a2fc72690b780b2a14e140ef6a9e0 | iloveyou |
Searching the sorted table for each stolen hash finds bkim's hash (
3fc0a7ac...), which belongs to qwerty123, so the adversary can log in as bkim. The other two hashes aren't in the table, since amorgan and cdavis picked passwords that aren't on a common list. If the database had been salted, none of these precomputed hashes would have matched anything.Telling Password Attacks Apart
Attack | Online or offline | What gets tried | Main defense |
Online guessing | Online | Many passwords against one account on a live login | Account lockout, MFA |
Password spraying | Online | One or a few common passwords (like Summer2026!) against many accounts, a try or two each | Banning common passwords, MFA |
Credential stuffing | Online | Known username and password pairs: defaults (like admin/admin) or pairs stolen in another breach | Changing default credentials, unique passwords, MFA |
Brute force | Offline | Every possible password up to some length | Long passwords that use several character sets |
Dictionary | Offline | A list of common and leaked passwords | Passwords that aren't on any list |
Rainbow table | Offline | Precomputed hashes of common passwords, looked up instead of computed | Salting |
Spraying and stuffing get mixed up a lot. Password spraying keeps the password the same and changes the account (one common password, many users). Credential stuffing uses whole username and password pairs the adversary already knows, either factory defaults or stolen ones.
Authentication Factors
Authentication mechanisms are technical controls that verify a user's identity, so only authorized users get into a system. The proof a user gives to identify themselves is called a factor, and there are four common types:
Knowledge factors (something you know) include passwords, PINs, and security questions. They're free to implement, easy to change, and don't require any physical item, but they can be guessed, phished, shoulder-surfed, forgotten, or written down somewhere insecure.
There's a built-in tradeoff: a knowledge factor that's hard for an adversary to guess is usually harder for the user to remember too.
Possession factors (something you have) include smart cards, hardware tokens, smartphones (for SMS or authenticator apps), and USB security keys like YubiKey. They're hard for a remote attacker to steal and can't be guessed, but they can be physically lost or stolen, cost money to issue and manage, and phone-based methods are vulnerable to SIM swapping.
A possession factor is only as strong as the object is hard to get: the harder it is for an adversary to steal it or make a copy of it, the more secure it is. A USB security key is harder to copy than a code sent by text message, since a SIM swap can send the user's texts to the adversary's phone.
Biometric factors (something you are) include fingerprints, facial recognition, iris or retina scans, palm prints, and voice recognition. They can't be forgotten, are hard to duplicate, and can't easily be shared, but they raise privacy concerns, can't be changed if ever compromised, aren't perfectly accurate (false positives and negatives happen), need expensive scanners, and can be affected by certain medical conditions.
Location factors (somewhere you are) use GPS, IP-based geolocation, Wi-Fi network identification, cell tower triangulation, or expected time zone. Organizations might allow logins only from a corporate IP range, require extra verification if location suddenly changes, block high-risk countries, or flag impossible travel between two logins. The catch is that VPNs and proxies can spoof location fairly easily.
Multifactor Authentication (MFA)
MFA requires two or more different factors, meaning compromising just one isn't enough to get in. Common combinations include a password plus an SMS code, a password plus an authenticator app code, a badge plus a PIN, or a fingerprint plus a password.
Example: without MFA, an attacker who phishes a password gets in immediately. With MFA, that same stolen password gets them nothing, since the system also demands a code from the user's actual phone, something the attacker doesn't have. As a bonus, the user might get an unexpected MFA prompt they didn't request, which itself is a red flag that their password's been compromised.
MFA needs two different types of factor, not just two things to type. A password plus a PIN, or a password plus a challenge question, is still single-factor authentication, since both are things the user knows. To name the type of authentication, ask what each step actually checks:
Login requires | Factors | MFA? |
A password | Knowledge | No |
A password and the answer to a challenge question | Knowledge + knowledge | No, both are things the user knows |
A password and a code from an authenticator app on the user's phone | Knowledge + possession | Yes |
An ID badge and a PIN at a door | Possession + knowledge | Yes |
A fingerprint to unlock a phone | Biometric | No, only one factor |
A password, and logins are only allowed from the office's IP addresses | Knowledge + location | Yes |
The AP course counts location as a factor, so the last row is MFA by the CED's rules. Some standards, like NIST's, don't count location as a factor on its own, since a VPN or proxy can fake it.
Configuring Secure Login Settings
Password complexity requirements make every new password include at least one character from each character set: uppercase letters (A to Z), lowercase letters (a to z), digits (0 to 9), and special characters (like
!, @, #, and ?). Together that's 94 characters to choose from (26 + 26 + 10 + the 32 special characters on a standard keyboard) instead of just 26. This makes a huge difference in cracking time: at the assumed 10 billion guesses per second, trying every 8-character password made only of lowercase letters takes about 21 seconds, while trying every 8-character password that uses all four character sets takes about 7 days.Minimum password length matters even more than complexity in a lot of cases, since each added character multiplies the total possibilities. At the same speed, trying every 6-character password across all character types takes just over a minute, every 8-character one about 7 days, and every 12-character one roughly 1.5 million years. Modern guidance recommends at least 12 to 16 characters for anything sensitive.
The math behind these numbers: if there are characters to choose from and the password is characters long, there are possible passwords, and a brute force tool may have to try all of them:
Length | Characters allowed | Possible passwords | Time to try them all at 10 billion per second |
8 | Lowercase only (26) | About 21 seconds | |
8 | All four sets (94) | About 7 days | |
12 | Lowercase only (26) | About 110 days | |
12 | All four sets (94) | About 1.5 million years | |
16 | Lowercase only (26) | About 138,000 years |
Two things stand out. A 12-character password of only lowercase letters (110 days) holds out longer than an 8-character password with all four sets (7 days), which is why length matters so much. And these are worst-case times: on average a tool hits the password about halfway through, and real tools try likely passwords first, so a common word with a number and a symbol tacked on falls much faster than the table suggests.
Maximum password age (forcing periodic changes, typically every 90 to 120 days) used to be standard practice, based on the idea that forced changes limit how long a compromised password stays useful. But current guidance, including NIST's, actually argues against this, since forced changes tend to make people create predictable patterns like going from "Summer2023!" to "Summer2024!" to "Fall2024!", or write passwords down because they can't remember the latest variant. It also doesn't do much against phishing or real-time credential theft, and it creates extra help desk burden. Current best practice: skip mandatory periodic changes unless there's an actual known compromise, and instead focus energy on password strength and MFA.
Password history prevents reuse by remembering a number of past password hashes, typically 5 to 10, and rejecting anything that matches. Without this, someone could just cycle between two or three passwords every time they're forced to change, effectively never really changing anything. With history enforcement in place, they're forced to create something genuinely new each time.
Account lockout locks an account after a set number of failed attempts, typically 3 to 5, for some duration. This can be a simple temporary lock (15 to 30 minutes, auto-unlocking), a manual unlock requiring admin intervention, or a progressive lockout that gets stricter with repeated failures. Without lockouts, an attacker can try thousands of passwords rapidly; with a 3-attempt lockout, cracking even 10,000 passwords could take over two months instead of minutes. The tradeoff is that legitimate users occasionally lock themselves out from typos, creating help desk calls, and in rare cases an attacker could deliberately trigger lockouts as a denial-of-service tactic against legitimate users, so most organizations settle on something like 3 to 5 attempts with a 15 to 30 minute lockout as a reasonable balance.
A comprehensive password policy typically requires at least 14 characters mixing all four character types, bans usernames and dictionary words, remembers the last 10 passwords to prevent reuse, skips mandatory expiration unless a breach is suspected, uses per-account (not per-IP) lockout after repeated failures, requires MFA for admin accounts, remote access, and sensitive data, and recommends a password manager for handling genuinely complex, unique passwords. All these controls working together create real defense in depth, protecting accounts even if any single control gets bypassed.
On a Windows computer, these login settings are configured in Local Security Policy (
secpol.msc) under Account Policies. The first five settings below are under Password Policy and the last three are under Account Lockout Policy. The example values match a strict policy like the one in Scenario 4B:Setting | What it controls | Example value |
Enforce password history | How many old passwords are remembered and blocked from reuse | 10 passwords |
Maximum password age | Days until a password must be changed (0 means it never expires) | 0 |
Minimum password age | Days before a user can change their password again, so they can't cycle right back to an old one | 1 day |
Minimum password length | The fewest characters allowed | 14 characters |
Password must meet complexity requirements | Requires characters from several character sets (Windows' version needs at least three of the sets, not all four, and blocks passwords that contain the username) | Enabled |
Account lockout threshold | Failed login attempts before the account locks | 5 attempts |
Account lockout duration | How long the account stays locked | 30 minutes |
Reset account lockout counter after | How long until failed attempts start counting from zero again | 30 minutes |