54 min read

The One Job of a Password Hash: Why Fast Is the Enemy, and Memory Is the Frontier

A password hash has one job: make each guess expensive on the cheapest attacker hardware. Why fast hashes fail, and how bcrypt, Argon2, and yescrypt fix it.

Permalink

1. Your Database Leaked, and the Race Is Already Over

In June 2012, roughly six million LinkedIn password hashes appeared on a Russian forum, and within days most were plaintext. Not because anyone had broken SHA-1, and not because there was a key to steal, but because the passwords had been stored with a hash whose whole design goal is to be fast [1].

A single modern gaming GPU tries tens of billions of SHA-1 guesses per second, so a stolen dump of a fast hash is not ciphertext an attacker must break. It is a race the defender has already lost, for every password common enough to sit in a wordlist. The uncomfortable lesson: the property that makes SHA-1 a good cryptographic hash -- its blazing speed -- is exactly what makes it a catastrophic way to store a password.

Fix that number in your head, because it governs everything that follows: on one NVIDIA RTX 4090, the hashcat benchmark clocks SHA-1 at about 50.6 billion guesses per second and MD5 at 164.1 billion [2]. Nobody attacked the mathematics. The attacker simply guessed, very fast, against a list of hashes that were never designed to make guessing expensive.

Stored verifier (password hash)

A value derived from a password by a deliberately slow, salted, one-way function and stored so that a leaked database does not directly reveal the passwords. It is distinct from a password-derived encryption key, which is computed and used but never stored (LUKS, VeraCrypt, password managers -- the subject of Part 9).

Offline attack

The attack this whole primitive exists to raise the cost of: the adversary holds the stolen verifiers and guesses locally at full hardware speed, with no server and no rate limiter in the loop. Nothing throttles them except the per-guess cost you baked into the hash. This is the offline-attacker threat model the series formalizes in Part 1, "Secure Against Whom?"

So here is the thesis, in your hands from the first page: a password hash has exactly one job -- make each attacker guess as expensive as possible on the attacker's cheapest hardware, while staying cheap enough on your server. The variable that decided LinkedIn was never SHA-1's strength. It was SHA-1's speed, compounded by a missing salt that let identical passwords share a hash and let precomputed tables apply.

Carry one diagnostic sentence through every scheme in this article: on the cheapest hardware an attacker can rent, how much does one guess at one password cost, and how far above your own per-login cost can you push it? By the end, you will drop bcrypt, Argon2id, yescrypt, and tomorrow's breach into that one economic question on sight.

One boundary, stated once. This article is about the stored-verifier case: turning a low-entropy password into a value you store so that a stolen database does not directly yield the passwords. The same primitives -- PBKDF2, scrypt, Argon2 -- also serve as key-derivation functions whose output becomes an encryption key and where nothing is stored (full-disk encryption, password managers). That dual use is Part 9's territory; here it is a pointer, not a detour.

This is also the handoff Part 10, "A Field Guide to Cryptographic Hashes," pointed at: a general-purpose hash such as BLAKE3 is engineered to be fast, which makes it exactly wrong for storing a password. Use a purpose-built password KDF instead. This is Part 12 of 25, and it is that return.

Ctrl + scroll to zoom
The offline-cracking loop. A salt forces the attacker to recompute per user, but it sets no speed limit -- for a fast hash the whole loop runs billions of times per second.

To make the economics visceral, run the numbers yourself. The rates below are the real measured throughputs of one rented RTX 4090, and the wordlist is the published rockyou.txt -- the first list every attacker loads, whose 14,344,391 entries are reproducible with wc -l [3].

JavaScript How long to try an entire wordlist, per scheme, on one RTX 4090
// Real measured throughputs on one NVIDIA RTX 4090 (hashcat v6.2.6).
var guessesPerSecond = {
"SHA-1 (fast hash)":            50600000000, // 50.6 GH/s
"PBKDF2-HMAC-SHA256, 999 it.":  8865700,     // 8.87 MH/s
"bcrypt (cost 5)":              184000,      // 184 kH/s
"scrypt (N=16384)":             7126         // 7,126 H/s
};

var wordlist = 14344391; // rockyou.txt: 14,344,391 entries

function human(seconds) {
if (seconds < 1)     return (seconds * 1000).toFixed(1) + " ms";
if (seconds < 60)    return seconds.toFixed(2) + " s";
if (seconds < 3600)  return (seconds / 60).toFixed(1) + " min";
if (seconds < 86400) return (seconds / 3600).toFixed(1) + " hours";
return (seconds / 86400).toFixed(1) + " days";
}

for (var scheme in guessesPerSecond) {
var seconds = wordlist / guessesPerSecond[scheme];
console.log(scheme.padEnd(30) + human(seconds));
}

console.log("");
console.log("Now add a per-password salt.");
console.log("It blocks precomputed tables and stops two users sharing a hash.");
console.log("Effect on the throughputs above: exactly 0 percent. Same rates, same wall clock.");

Press Run to execute.

The fast hash exhausts all fourteen million guesses in well under a second. The memory-hard end of the table takes hours to do the same work on the same silicon -- a gap of roughly seven orders of magnitude, bought entirely by making each guess cost more. And notice what the salt does to those rates: nothing. It is a necessary defense against precomputation and hash-sharing, and it is useless against speed. Speed is the whole vulnerability.

A password hash has one job: make every guess cost as much as possible on the attacker's cheapest hardware, while staying cheap on yours. A fast hash gets that job exactly backwards.

If the cipher was never the weak link and the key was never touched, then the defense was lost the moment someone chose a fast function to store a guessable secret. That trade-off -- store something guessable, so make guessing expensive -- was understood with total clarity in 1979, by two people who assumed from the start that the attacker would read the password file.

2. 1979: The Three Rules That Still Govern Everything

Bell Labs, 1979. Robert Morris and Ken Thompson publish "Password Security: A Case History" and reason like economists rather than cryptographers [4]. Their founding assumption is the one every subsequent scheme inherits: assume the attacker can read the password file, because on a shared UNIX system they can. Everything else follows from that single concession.

Three rules fall out of it, and every scheme in this article -- forty-seven years later -- still obeys all three.

Salt

A per-password random value stored in the clear alongside the hash, so that identical passwords do not produce identical hashes and precomputed tables cannot be reused. A salt is public, not secret; its job is to make each stored verifier unique, not to hide anything. Generating salts correctly is the domain of a CSPRNG, covered in Part 2, "Predictable or Repeated."

Rule one: never store the password -- store a one-way function of it. A stolen file should be a file of hashes, not a file of passwords. Rule two: salt it. Attach a per-password random value -- twelve bits in the original DES crypt -- so identical passwords do not collide and an attacker cannot precompute one giant table and apply it to every account at once. Rule three: make it deliberately slow. Calibrate the cost so that a legitimate single login is trivial but bulk guessing is not.

The mechanism was concrete, and you can see all three rules in it without touching DES's internals. The password was truncated to eight characters and turned into a 56-bit DES key; that key enciphered an all-zero block twenty-five times over; and the twelve-bit salt perturbed DES's expansion permutation so that off-the-shelf DES hardware could not be pointed straight at the problem [4]. One-way, because you cannot run DES backward without the key that is the password. Salted, because of those twelve bits. Slow, because of those twenty-five iterations.

That salt design is more pointed than it looks. Morris and Thompson perturbed the DES expansion permutation specifically so an attacker could not use stock DES chips to attack the hashes -- the same "make the attacker's silicon useless" instinct that, thirty years later, becomes memory-hardness [4].

The economic point was already fully formed in 1979: the openly readable password file was a design choice, so the hash itself had to survive offline guessing. That is the offline-attacker model the whole series leans on, and it is why the strength of the underlying cipher was never the interesting question. The interesting question was cost per guess.

Ctrl + scroll to zoom
Password hashing at a glance. bcrypt (1999) and PBKDF2 (2000) are contemporaries from different communities, not a before-and-after; the one genuine inflection point is memory-hardness (scrypt, 2009).
Founding rule (1979)What it defeatsHow every modern scheme still does it
Store a one-way function, never the passwordA stolen file is not a stolen password listbcrypt, Argon2, and yescrypt are all one-way; you verify by recomputing and comparing
Salt it (per-password random value)Precomputation and identical-password collisionsa 16-byte CSPRNG salt embedded directly in the stored hash string
Make it deliberately slowBulk offline guessinga tunable work factor, iteration count, or memory cost

Three rules, 1979: one-way, salted, slow. They were right, and they are still the entire skeleton. But two of the three had hard numbers baked in -- an eight-character limit, a twelve-bit salt, twenty-five fixed iterations -- and numbers calibrated to 1979 hardware do not stay calibrated. The next fifteen years are the story of those ceilings collapsing.

3. The Antipattern That Would Not Die

A design calibrated to a single moment ages at the speed of Moore's law. DES crypt was tuned for 1979; by the mid-1990s the same twenty-five iterations were a rounding error, and its eight-character truncation threw away everything a longer password could have bought. The fixes that followed solved real problems -- and, in the same breath, set a trap the industry still steps in today.

Removing the ceilings, and inventing migration

Poul-Henning Kamp's md5crypt ($1$), created in 1995 in his own words, removed the eight-character and twelve-bit-salt limits with roughly a thousand iterations of MD5 and a larger salt [5]. Its most durable gift, though, was not the hash. It was the format: the Modular Crypt Format (MCF), a self-describing string of the shape $<id>$<params>$<salt>$<hash> that lets one password file hold several algorithms at once and migrate between them over time.

Modular Crypt Format (MCF)

A self-describing hash string of the form $<id>$<params>$<salt>$<hash>. The leading <id> names the algorithm, so a verifier can look at a stored value, dispatch to the right function, recompute with the stored parameters, and -- when those parameters fall below today's target -- transparently rehash. It is the reason a single verifier column can hold $1$, $2b$, $6$, and $argon2id$ values side by side and upgrade in place.

That registry of <id> tags is worth memorizing, because it is a map of this entire article:

MCF prefixSchemeStatus in 2026
$1$md5cryptlegacy, retired by its author [5]
$2a$ / $2y$ / $2b$bcryptactive [6]
$5$sha256cryptactive (older Linux default) [7]
$6$sha512cryptactive (older Linux default) [7]
$7$scryptactive [8]
$y$yescryptactive (current Linux default) [8]
$argon2id$Argon2idactive (cross-platform recommendation) [9, 10]
JavaScript Decode a Modular Crypt Format string into its fields
// A Modular Crypt Format value is self-describing: $<id>$<params>$<salt>$<hash>.
// That is exactly what makes in-place migration possible.
function decodeMCF(s) {
return s.split("$").filter(function (p) { return p.length > 0; });
}

var argon  = "$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0$aGFzaGhhc2hoYXNoaGFzaA";
var bcrypt = "$2b$12$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm";

console.log("argon2id ->", JSON.stringify(decodeMCF(argon)));
// ["argon2id", "v=19", "m=65536,t=3,p=4", "<salt>", "<hash>"]

console.log("bcrypt   ->", JSON.stringify(decodeMCF(bcrypt)));
// ["2b", "12", "<22-char salt and 31-char hash, concatenated>"]

console.log("");
console.log("The first token names the algorithm. A verifier dispatches on it,");
console.log("recomputes with the stored parameters, and rehashes if they are stale.");

Press Run to execute.

The other move of this era was the SHA-2 bridge. Ulrich Drepper's sha256crypt and sha512crypt (2007, $5$ and $6$) iterated a NIST-approved hash a default of 5,000 rounds, tunable up to just under a billion, with a salt up to sixteen characters [7]. The motive was compliance as much as cryptography: security teams wanted a NIST-tested primitive, which ruled out Blowfish-based bcrypt and made sha512crypt the long-time Linux /etc/shadow default. Same instinct as md5crypt -- make the cost tunable -- with a FIPS-friendly hash underneath.

The antipattern, named and buried

Now the recurring villain of the whole story: the fast, unsalted, general-purpose hash -- raw MD5, SHA-1, or SHA-256 -- pressed into service as a password store. It fails on both axes at once. Unsalted means precomputed rainbow tables apply and identical passwords share a hash. Fast means billions of guesses per second.

This is precisely the LinkedIn disaster from Section 1 -- unsalted SHA-1, plaintext in days [1] -- and, one rung worse, RockYou in 2009, which stored roughly 32.6 million passwords in plaintext, no hash at all [11]. That dump became rockyou.txt, the canonical cracking wordlist now itself part of the arms race.

The unsettling corollary for defenders: if your password is memorable, it is almost certainly already a line in that cracking wordlist, tried in the first seconds of any offline attack.

The fast-hash trap: the property that makes SHA-256 a good general-purpose hash -- blazing speed -- is exactly what makes it a catastrophic password hash. A salt defeats precomputation and hash-sharing, but it changes the attacker's throughput by exactly zero. The attacker never breaks the hash; a stolen fast-hash dump is a race already lost.

Kamp himself buried md5crypt in 2012, and his exact words matter because the field has misquoted them ever since. His notice is titled "Md5crypt Password scrambler is no longer considered safe by author," and it reads, plainly, "I implore everybody to migrate to a stronger password scrambler without undue delay" [5]. The widely repeated phrase "unfit for use" is a paraphrase, not his language. What he did write is startling in hindsight: the replacement should "soak up area in hardware based attack implementations" -- a demand for area-cost hardening, seven years before scrypt would make it a formal property.

Kamp's 2012 retirement of md5crypt cited about one million guesses per second on a commodity 2012 GPU and the vulnerability CVE-2012-3287. Read his phrasing carefully -- "soak up area in hardware based attack implementations" is a plea for area-time cost, the exact idea memory-hardness would formalize [5].

The successor should "soak up area in hardware based attack implementations." -- Poul-Henning Kamp, retiring md5crypt in 2012, describing memory-hardness before it had a name.

Keep this master catalog in view; the article returns to each entry and unpacks the interesting ones in Sections 9 through 11. Every row is the same lesson from a different angle.

IncidentYearWhat was storedRoot causeThe lesson
RockYou2009PlaintextNo hash at allThe floor of the failure catalog [11]
LinkedIn2012Unsalted SHA-1Fast and unsaltedSpeed is the vulnerability [1]
Adobe2013Reversible 3DES in ECB modeEncryption, not hashingHash, never encrypt, a password [12]
Ashley Madison2015bcrypt cost-12, plus an MD5 tokenA strong hash undone by a parallel fast pathYou are only as strong as the weakest function [13]
Dropbox2016SHA-512, then bcrypt-10, then AES-256A good example, shown for contrastLayer defenses, keep a separate pepper [14]
Facebook2019Plaintext, in log filesPlaintext logging beside a good hashAudit the whole path, not the column [15]
Password shucking2020bcrypt(md5(pw)), unsalted innerA naive pre-hashNever feed an unsalted fast hash into bcrypt [16]
crypt_blowfish bug2011bcrypt with an 8-bit character flawSign-extension truncationWhy bcrypt has $2x$ and $2y$ tags [6]

md5crypt and sha512crypt fixed 1979's ceilings but kept its third rule frozen in place: a fixed iteration count chosen once and forgotten. The next idea was to stop freezing the cost and make it a knob the defender could turn as hardware improved. That idea arrived twice, independently, in two different communities within about a year -- and the blind spot those twins shared is where the modern story actually begins.

4. A Branch, Not a Line: bcrypt and PBKDF2 Are Twins

The textbook story is a clean relay race: each generation hands the baton to a stronger successor that retires it. For password hashing, that story is wrong, and getting it right is the single most useful correction an expert reader can take from this article. Around 1999 and 2000, two communities answered the same question -- "make the cost a tunable knob" -- independently, and neither replaced the other. They are twins from different families, and both are still alive in 2026.

bcrypt, from the systems world (1999)

Niels Provos and David Mazieres built bcrypt for OpenBSD on top of Eksblowfish, an "expensive key schedule" variant of the Blowfish cipher [17]. The construction repeats Blowfish's key setup 2cost2^{\text{cost}} times, then enciphers the constant string "OrpheanBeholderScryDoubt" sixty-four times, salted with 128 bits and encoded as $2a$<cost>$.... The innovation is the adaptive cost factor: raise cost as hardware improves, without changing the algorithm or invalidating a single stored hash. In the authors' own framing, "the computational cost of any secure password scheme must increase as hardware improves" [17].

Work factor (adaptive cost)

A tunable parameter -- bcrypt's cost, PBKDF2's iteration count, Argon2's time and memory knobs -- that raises the per-guess cost so a defender can track hardware improvements over time without changing the algorithm or re-hashing existing verifiers with a new scheme. Turning it up is how you "restore the cost ratio" the spine keeps referring to.

bcrypt was also, almost by accident, the first widely deployed scheme to touch memory in a way that mattered. Blowfish's key schedule builds about 4 KiB of S-boxes, and that small working set already made bcrypt meaningfully harder to parallelize on a GPU than a bare hash -- a fact scrypt's paper would later quantify [18]. Two structural limits recur later: bcrypt silently truncates its input at 72 bytes, and its $2a$ / $2x$ / $2y$ / $2b$ version tags are a bug-fix lineage, not a version-number vanity.

bcrypt's prefix zoo is a repair history. The $2x$ and $2y$ tags came out of a 2011 fix to an 8-bit-character sign-extension flaw in crypt_blowfish (CVE-2011-2483) that could truncate the input; $2b$ matches the behavior of OpenBSD 5.5 and later [6].

PBKDF2, from the standards world (2000)

Burt Kaliski's PBKDF2 came out of RSA Laboratories as part of PKCS #5 v2.0, later republished as RFC 2898 and updated in RFC 8018 [19, 20]. Its design is deliberately plain: iterate a pseudorandom function -- in practice HMAC over an approved hash -- a count c times over the password and salt, concatenating output blocks. Because it is built entirely from NIST-approved hashes, it is the option a FIPS-constrained system can actually deploy, which Blowfish-based bcrypt is not. In 2026 it remains the mandated path wherever FIPS applies.

Its weakness has to be stated precisely, because the folklore version is wrong. PBKDF2 is not weak because "GPUs are fast." It is weak because it has negligible, constant memory and is otherwise a bare loop of HMAC, which makes it the most parallelization-friendly of every deployed scheme. The crux is low, constant memory implies cheap, massive parallelism on GPU, FPGA, and ASIC -- an attacker fits thousands of independent PBKDF2 evaluations onto one chip because each one needs almost no RAM.

A nuance the OWASP floors encode: PBKDF2-HMAC-SHA-512 does 64-bit-word arithmetic, which is somewhat less GPU-friendly than SHA-256's 32-bit words. That is exactly why OWASP's SHA-512 iteration floor (210,000) sits below its SHA-256 floor (600,000) -- fewer iterations buy the same attacker cost [21].

Here is the master head-to-head. The rightmost columns are the ones that decide real deployments; the article earns each judgment in the sections that follow.

SchemeYearNew cost leverMemory-hard?FIPS lane?Side-channel postureStatus in 2026
Raw fast hash (MD5, SHA-1, SHA-256)--none; general-purpose speedNoprimitive onlyn/anever use for passwords
DES crypt(3)1979one-way, salt, 25 iterations [4]NoNodata-independenthistoric
md5crypt ($1$)1995~1000 iterations, MCF [5]NoNodata-independentretired by author
bcrypt1999adaptive cost, expensive key schedule [17]No (touches ~4 KiB)Non/a (not an MHF)active
PBKDF22000iterated HMAC, tunable count [19]No (negligible memory)Yesdata-independentactive (FIPS)
sha256crypt / sha512crypt2007tunable SHA-2 rounds [7]NoYesdata-independentactive (legacy default)
scrypt2009sequential memory-hardness [18]Yes (data-dependent)Nodata-dependentactive
Argon2i2015data-independent MHF [9]Yes (provably suboptimal)Nodata-independentsuperseded by id
Argon2id2015hybrid i-then-d MHF [9]YesNohybridrecommended default
yescrypt2015scrypt plus pwxform, optional ROM [22]Yes (data-dependent)Nodata-dependentLinux shadow default

The blind spot all three twins shared

Every one of the 1999-2007 schemes stretches time only, with a small fixed memory footprint. Time-hardness has a fatal asymmetry: a function that is slow but tiny in memory is cheap to parallelize. The defender runs one core per login; the attacker runs thousands of small cores on a GPU or millions in an ASIC. "Slower per guess" is a race the defender loses at scale.

One benchmark makes the asymmetry impossible to unsee -- a single RTX 4090 under hashcat v6.2.6 [2]:

Hash mode (one RTX 4090)Guesses per secondSlowdown vs MD5
MD5164.1 billion1x
SHA-150.6 billionabout 3x
PBKDF2-HMAC-SHA256, 999 iterations8.87 millionabout 18,000x
bcrypt, cost 5184,000about 890,000x
Ctrl + scroll to zoom
The spine as a staircase. Each lever is eroded by a new attacker capability and restored by a new, harder-to-parallelize cost. This is the same economic move, repeated for forty years.

Look at the last two rows. PBKDF2 with a thousand iterations barely dents the attacker's throughput, because it is almost pure compute. bcrypt at a low cost of 5 -- with nothing but its 4 KiB of S-boxes -- already costs roughly five orders of magnitude more per guess than a raw hash, purely because it touches memory and cannot be trivially unrolled. That gap is the entire clue.

If a mere 4 KiB of forced memory buys bcrypt that five-orders-of-magnitude edge over a raw hash, the lever is obvious in hindsight: stop making a guess take longer and start making it take more room. CPU cycles and logic gates are cheap and endlessly parallel. Memory bandwidth and capacity are not. That inversion -- from time to memory -- is the one genuine conceptual leap in this entire history.

5. The Breakthrough: Make Memory the Bottleneck

In 2009, Colin Percival stated the idea that splits the timeline in two: force the computation to hold a large array in RAM for a long time, so that computing the hash cheaply requires buying memory -- the one resource an attacker with GPUs, FPGAs, and ASICs cannot parallelize away [18]. This is not a faster hash. It is a differently shaped one.

scrypt and sequential memory-hardness

The core of scrypt is a routine called ROMix, and you can understand it without touching the Salsa20 mixing inside. It runs in two phases. In the fill phase, it writes an array V[0..N-1] into memory, where each block is the previous block run through a mixing function -- a strictly sequential chain. In the mix phase, it takes N more steps, and on each step it reads V[j] at an index j that depends on the data computed so far, mixing that block back into a running state [18, 23].

That second phase is the whole trick. Because the read indices depend on the data, you cannot know in advance which blocks you will need, so computing scrypt with fewer than N blocks resident forces you to recompute missing blocks on demand -- and that recomputation cascades.

To keep the work down, you must hold about N blocks for about N steps: an area times time cost of roughly ATΘ(N2)AT \approx \Theta(N^2). Halving the memory you hold roughly doubles the time, so the AT product stays near Θ(N2)\Theta(N^2). Double the cost parameter N instead and both time and memory double, so the attacker's hardware cost rises four-fold [18].

Memory-hard function (MHF)

A function deliberately designed so that computing it cheaply requires holding a large amount of memory for a large fraction of its running time. The point is not that it uses memory, but that no attacker strategy avoids the memory without paying more somewhere else -- imposing a cost that cheap, massively parallel hardware cannot dodge.

AT-cost (area times time)

The attacker's real cost metric for a memory-hard function: the memory it occupies multiplied by the time it is held, a proxy for the silicon area and energy one guess consumes. Time-hardness alone raises only the time term; memory-hardness raises the product. Maximizing AT-cost per guess is the entire objective of the memory-hard program.

Ctrl + scroll to zoom
scrypt's ROMix: fill an array of N blocks, then walk it at data-dependent indices. Holding fewer than N blocks forces cascading recomputation, which is what pins the area-times-time cost near N squared.

The payoff shows up on the same RTX 4090 that cracked SHA-1 at 50.6 billion guesses per second. scrypt tuned to N of 16384 runs at just 7,126 guesses per second on that hardware [2] -- about 25 times slower than bcrypt at cost 5, and roughly seven orders of magnitude below raw MD5, because the GPU's thousands of cores now fight over scarce fast memory instead of streaming through cheap arithmetic.

Time-hardness is cheap to parallelize: the defender runs one core, the attacker runs thousands, so making a guess merely slower loses the cost race at scale. Making a guess occupy memory for a long time -- an area-times-time cost -- targets the one resource an attacker cannot buy asymmetrically cheaply. That inversion, from time to memory, is the modern era of password hashing.

Institutionalizing it: the PHC and Argon2

Faced with a hard design problem, cryptography did what it does best -- it ran an open competition. The Password Hashing Competition ran from 2013 to 2015, initiated by Jean-Philippe Aumasson, drew 24 submissions, and was judged by a panel in the same spirit as NIST's AES and SHA-3 contests [24]. It named one winner, Argon2, and gave special recognition to four finalists.

The PHC's four special-recognition finalists were Catena, Lyra2, Makwa, and yescrypt -- the last of which went on to become the Linux /etc/shadow default. The competition format, modeled on NIST's, is why the field trusts Argon2: it survived years of public cryptanalysis, not a vendor's say-so [24].

Argon2 is the purpose-built memory-hard function. It exposes three independent knobs -- time t (passes), memory m (in KiB), and parallelism p -- fills and mixes a large array using a BLAKE2b-based compression function, and encodes its output in MCF as $argon2id$v=19$m=...,t=...,p=...$salt$hash, where v=19 is version 0x13. The reference defaults are t=3, m=4 MiB (that is 2122^{12} KiB), and p=1 [10]. Crucially, Argon2 comes in three variants, and choosing among them is a threat-model decision, not a performance tweak.

Data-independent (iMHF) versus data-dependent (dMHF) memory access

Whether the memory addresses a function reads depend on the password. Data-independent (an iMHF) is cache-timing-safe, because the access pattern reveals nothing about the secret -- but, as the next section proves, it cannot be optimally memory-hard. Data-dependent (a dMHF) can reach the ideal hardness bound but leaks information through cache-timing side channels. This tension is the reason Argon2id exists.

Ctrl + scroll to zoom
Argon2's three variants map to two threat models. Argon2id runs data-independent addressing for the first half-pass, then switches to data-dependent, which is why RFC 9106 makes it the must-support choice.

Argon2d uses data-dependent indexing for maximum resistance to GPU and trade-off attacks, at the cost of a cache-timing side channel. Argon2i uses data-independent indexing, which is cache-timing-safe but needs more passes and is provably weaker. Argon2id is the hybrid: data-independent addressing for the first half-pass, where the side channel matters, then data-dependent addressing for the rest. RFC 9106 makes Argon2id the MUST-support variant [9], for reasons the theory section will make exact.

Memory-hardness works. It is the first idea in thirty years that changes the shape of the attacker's cost, not merely its size. So the modern era ought to be simple -- pick the memory-hard winner, turn the memory knob to maximum, and go home. It is not simple, and the two reasons why -- one you meet in production, one buried in complexity theory -- are the rest of this article.

6. State of the Art (2024-2026): Two Defaults, Not One

Ask a working engineer "what should I use in 2026?" and the honest answer is two names, not one -- and which one depends on what you are protecting and where it runs.

For greenfield application code, Argon2id is the choice. It is the RFC 9106 MUST-support hybrid and the OWASP recommendation, and it meets NIST SP 800-63B-4's generic requirements -- though NIST itself names no specific memory-hard scheme, and its only approved-scheme pointer is SP 800-132 (PBKDF2) [25]. Its one algorithm answers both threat models at once: the data-independent first half-pass closes the cache-timing side channel, and the data-dependent tail resists GPU and ASIC trade-offs. You get independent time, memory, and parallelism knobs, plus a self-describing MCF encoding that makes migration a matter of reading the stored parameters [9, 10].

Two authoritative parameter sets anchor it. RFC 9106's first recommended option is t=1, p=4, m=2 GiB, a default suitable for all environments; its memory-constrained second option is t=3, p=4, m=64 MiB [9]. OWASP publishes a much lower floor -- m=19 MiB, t=2, p=1 -- and it is exactly that: a bare minimum to clear, not a target to aim at [21].

Where Argon2id struggles is the mirror image of its strength. The defender pays the full memory cost on every login, so a 2 GiB-per-hash setting is a genuine RAM and denial-of-service hazard -- which is precisely why the deployed floors sit so far below the theoretical ideal. Some language bindings also default to the weaker Argon2i or to under-powered parameters, so "we use Argon2" is not the same claim as "we use Argon2id at sound parameters."

yescrypt: the Linux /etc/shadow default

Alexander Peslyak's yescrypt, from Openwall, is what actually hashes the login password on most modern Linux systems. It builds on scrypt's sequential memory-hardness and adds pwxform -- S-box lookups interleaved with integer multiplication that annoy GPUs, FPGAs, and ASICs even at low memory settings -- plus an optional large, site-specific ROM of tens of gigabytes that throttles "attackers' use of pre-existing hardware such as botnet nodes" [22].

It is engineered so its effective memory cost cannot be driven "unreasonably low" the way plain scrypt's and Argon2's can be at high throughput and low latency. That is exactly why it dominates the high-request-rate regime an OS login endpoint lives in [22].

yescrypt is the default /etc/shadow scheme on ALT Linux, Arch, Debian 11 and later (it became the default in 11, not 12), Fedora 35 and later, Kali 2021.1 and later, and Ubuntu 22.04 and later; it is also supported but not default on Fedora 29+, RHEL 9+, and Ubuntu 20.04+. The selection mechanism is libxcrypt together with login.defs [22, 8].

Where yescrypt struggles is breadth: it is Linux-centric, comparatively young, and thin on non-Linux bindings, and its ROM feature demands real operational machinery -- the ROM must never be lost, because losing it invalidates every hash computed against it.

Beware a naming collision before you grep a Windows codebase: the BCrypt* functions in Windows CNG (Cryptography API: Next Generation) are not the bcrypt password hash -- they are the platform's general crypto API [26]. See the CNG architecture post for what those calls actually do.

The honest status line

scrypt (still widely deployed, standardized as RFC 7914), bcrypt (ubiquitous and battle-tested), and PBKDF2 (the FIPS lane) are all active, not retired. The branch-not-line reality of Section 4 persists straight into the present: there is no single winner that made the others obsolete, only a recommended default and a set of live alternatives chosen by constraint.

"Use Argon2id, or leave yescrypt alone" is the entire answer for new code on a known platform. But engineers inherit constraints -- a regulator that forbids the primitive, a runtime with no good binding, a login endpoint one traffic spike away from falling over. So the real question is not "what is best" in the abstract. It is "what are my options, ranked, and exactly when does each one apply?"

7. The Decision Matrix: Use X With These Parameters in Case Y

Five deployed schemes, one page, ranked -- not by fashion but by the economic yardstick this whole article is built on: how much does one attacker guess cost, and what does imposing that cost do to you? The master matrix in Section 4 lays out the axes that decide it -- memory-hardness, standardization and FIPS status, side-channel posture (data-dependent versus data-independent), library breadth, and tunability. What follows turns those axes into rules you can apply on sight.

Your binding constraintUse thisAt these parameters
Greenfield app, good libraryArgon2id [9]2 GiB, t=1, p=4 if affordable; else 64 MiB, t=3, p=4; never below the 19 MiB, t=2, p=1 floor [21]
Linux OS loginyescrypt [22]the distro default; do not hand-roll it [8]
Huge single-operator, can run a ROMyescrypt with ROM [22]raise the attacker's capital cost in scarce large RAM
Need memory-hardness, no Argon2idscrypt [21]N=2^17, r=8, p=1 (about 128 MiB)
FIPS-140 or regulatory constraintPBKDF2-HMAC-SHA256 [21]at least 600,000 iterations
Legacy, small, or simplicity-firstbcrypt [21]work factor 10 or higher; enforce the 72-byte cap

Read the axes together and the rules explain themselves. If you need cache-timing safety on a shared or multi-tenant host, you want a data-independent or hybrid scheme, which points at Argon2id over Argon2d or scrypt. If a regulator dictates approved primitives, memory-hardness is off the table entirely and PBKDF2 is the only compliant answer, whatever its per-guess weakness.

If your constraint is a login endpoint hammered at high request rates, yescrypt's low-memory hardening earns its keep. And if your real constraint is a small team that needs one dependency-light library that has existed for two decades, bcrypt is a defensible engineering choice, not a mistake.

That last point is the one that makes this section trustworthy, so state it plainly: the ranking is not absolute. A well-tuned bcrypt on a small service can out-secure an under-provisioned Argon2id that a denial-of-service-averse operator was forced to dial down to the 19 MiB floor. The most credible voice on this is not a bcrypt partisan -- it is the author of yescrypt, the very scheme that sits at the top of the memory-hard heap.

"For smaller deployments, bcrypt with its simplicity and existing library support is a reasonable short-term choice." -- Alexander Peslyak, author of yescrypt [22]

FIPS deserves one more word, because it does something the rest of the matrix does not: it collapses the whole decision to a single cell. Under a FIPS-140 constraint, none of the memory-hard reasoning applies -- Argon2, scrypt, and yescrypt are simply not approved -- and you deploy PBKDF2-HMAC-SHA256 at a high iteration count and accept its parallelism weakness as the price of compliance [21]. The economic yardstick still governs; the regulator has just fixed one of its variables for you.

The matrix ranks the schemes, but notice what it quietly concedes. Every "best" here is bounded by what the defender can afford to run on every single login, and every entry leans on assumptions about attacker hardware that no one can hold fixed across a decade. Pull on that thread and you arrive at the theory -- where memory-hardness turns out to carry a hard, proven ceiling that no amount of engineering can lift.

8. Theoretical Limits: Memory-Hard by Design Is Not Optimally Memory-Hard

Password hashing is one of the rare corners of applied cryptography with real complexity theory bolted to it -- and the theory delivers two humbling results the marketing never mentions.

The ideal, and the impossibility

The best conceivable attacker cost for a function that touches n blocks over n steps is AT=Θ(n2)AT = \Theta(n^2): you pay the full memory-time area even with unlimited parallel cores. scrypt's data-dependent ROMix is designed to reach it. But you cannot have all three of Θ(n2)\Theta(n^2) hardness, cache-timing safety, and free parameter choice at once -- and that is a theorem, not a limitation of any particular design.

Joel Alwen and Jeremiah Blocki proved that data-independent memory-hard functions -- the class you are forced into for cache-timing safety, because their access pattern must not depend on the password -- provably cannot reach the ideal bound. Any iMHF can be computed at O(n2/log1εn)O(n^2/\log^{1-\varepsilon} n), and Argon2i specifically at O(n7/4logn)O(n^{7/4}\log n) [27]. That sublinear shortfall is small, but it is real, and it is unavoidable for the whole data-independent class.

This is the single most consequential theorem in the area, because it is the actual reason RFC 9106 prefers the Argon2id hybrid over the safer-looking pure-data-independent Argon2i. Recall the addressing diagram from Section 5: Argon2id uses data-independent addressing only for the first half-pass, where the side channel would otherwise leak, then switches to data-dependent addressing, which escapes the iMHF impossibility.

The RFC bakes this in directly. It makes Argon2id MUST-support, notes that Argon2i "makes more passes over the memory to protect from trade-off attacks," and leans on the Alwen and Blocki attack -- its reference tag [AB16] -- to conclude that three passes is "almost optimal" for Argon2i [9, 27].

The follow-on results fill in the picture, and they are worth one breath each so the section stays proportionate. Blocki and Zhou tightened the Argon2i bound to O(n1.768)O(n^{1.768}) from above and Ω(n1.75)\Omega(n^{1.75}) from below [28]. DRSample is the best practical iMHF construction known, built for high depth-robustness [29]. Balloon was the first practical iMHF to ship with a memory-hardness proof [30].

On the data-dependent side, the attacks are only constant-factor trade-offs found by real cryptanalysis rather than asymptotic breaks -- Biryukov and Khovratovich, for instance, cut Catena's AT-cost by a factor of about 25 using M4/5M^{4/5} memory [31]. The map is detailed, but the headline is simple: the class you need for side-channel safety is the class that cannot be optimal, so the field hybridizes.

The second, mundane impossibility: no free lunch

The other limit needs no complexity theory at all. The defender pays the same per-evaluation cost as one attacker guess. So the hardness you can impose is capped by your own budget: the RAM you can spare per concurrent login, the hash latency your users will tolerate (roughly one second at the very most), and the denial-of-service surface you expose when an attacker hammers your login endpoint to make you do the expensive work.

RFC 9106 frames the rational defender not as someone climbing an unbounded hardness curve but as someone maximizing attacker cost for a fixed defender time budget -- an optimization with a ceiling, not a dial that goes to infinity [9].

Quantum computing does not change this calculus either. The Section 11 FAQ carries the full argument; in short, Grover's square-root speedup is no Shor-style break against a one-way hash and is swamped by the per-guess costs a memory-hard scheme already imposes, so post-quantum worry belongs to your key exchange, not your password store [32].

The humbling synthesis: "memory-hard by design" is not "optimally memory-hard" -- Alwen and Blocki prove the data-independent class cannot reach the ideal AT bound, which is why Argon2id is a hybrid. The defender pays the memory too, so hardness is bounded by your own budget. And the hash is rarely the weakest link anyway. The verdict is not "use the strongest function" but "price a guess correctly, on both sides of the ledger" -- an economic decision, not a single winner.

So the design problem has a proven best answer -- the id hybrid -- and a proven ceiling -- your own budget. Yet scanners still find broken password storage in production every year, and the field still argues, in public, about whether any of this memory-hardness is worth paying for. Both of those live on the open frontier.

9. Open Problems: The Arms Race, and the Argument About Whether to Fight It

The construction-level question is largely settled -- use the id hybrid, and the theory tells you why. The deployment question is wide open, and one of its honest sub-questions is whether the field's own default is worth its price.

Principled parameter selection under real budgets. The question every engineer actually has -- given RAM times concurrency, a latency target, login queries per second, and a denial-of-service tolerance, output the AT-optimal (m, t, p) -- has no closed-form answer. RFC 9106 offers two fixed configurations; OWASP offers equivalence classes; neither is a portable optimizer you can point at your server and trust [9, 21]. In practice you measure, pick a latency target, and revisit it, which is craft, not calculation.

The moving hardware economics. Every parameter floor silently assumes an attacker hardware model, and that model drifts. When a new GPU generation gains memory bandwidth, yesterday's "19 MiB minimum" quietly weakens without a line of your code changing. The RTX 4090 benchmark in this article is one snapshot of a gradient that shifts every hardware generation [2]. yescrypt's optional ROM is one structural answer: raise the attacker's capital cost -- the price of enough scarce large RAM -- rather than only the marginal cost per guess [22].

Peppering and secret-salt key management. A server-held secret mixed into the hash makes a stolen database uncrackable without also stealing a separately stored key -- a large practical win. But get the construction wrong and you cannot ever rotate that secret.

Pepper (secret salt)

A secret, server-held value -- kept in an HSM, KMS, or TEE, separate from the database -- mixed into the hash as defense-in-depth, so that a stolen database alone cannot be cracked. It is distinct from the public per-password salt: the salt makes verifiers unique and is stored beside them; the pepper is a secret and must never be.

The engineering answer is to encrypt the hash under the pepper (which is rotatable, the design Dropbox shipped) rather than to HMAC it into a one-way step (which is not). NIST SP 800-63B-4 recommends exactly this: an additional keyed hashing or encryption operation with the key held in a hardware-protected area such as an HSM or TPM [14, 25]. The how-to is in Section 10.

Every open problem here reduces to the sentence this article began with -- maximize attacker cost per guess without bankrupting the defender -- now carrying one humbling clause: the hash is only one term in a much larger security budget, and often not the term that decides the outcome. Which means the practical guide is not a victory lap. It is that sentence, made operational.

10. The Practical Guide: Decision Rules, Parameters, and Shipping Code

Everything above collapses into five short things: a decision procedure, a parameter table, the concrete library call for each scheme, a list of patterns to never ship, and the login-handler code the whole article has been building toward. None of it requires re-deriving the theory. It requires knowing your one binding constraint and reading down from there.

Start with the constraint, because it -- not fashion -- picks the scheme. Walk this tree top to bottom and stop at the first branch that matches your situation.

Ctrl + scroll to zoom
The 2026 decision tree. Walk it top to bottom and stop at the first constraint that applies; each leaf names the scheme and the parameters to start from.

The tree points at a scheme; the table below pins the numbers. Every figure here is drawn from RFC 9106, the OWASP Password Storage Cheat Sheet, yescrypt's documentation, or NIST SP 800-63B-4 [9, 21, 22, 25]. One trap to name explicitly: OWASP's iteration counts drift upward over time, so this table prints the values from the dated 2025-06-02 snapshot, not the live page -- if you copy a number, copy it from a dated source so your reviewer can reproduce it.

SchemeParameters to start from (2024-2026)SaltNotes
Argon2idRFC 9106 target t=1, p=4, m=2 GiB; memory-constrained t=3, p=4, m=64 MiB; OWASP floor t=2, p=1, m=19 MiB [9, 21]128-bitPin the id variant; never ship bare Argon2i as the default
yescryptThe distro default settings; add a multi-GB ROM at scale [22]library-managedLinux /etc/shadow; $y$ MCF
scryptN of 2^17, r=8, p=1 (about 128 MiB) [21]128-bitOr an equivalent point on the p-for-RAM curve
bcryptWork factor 10 or higher; enforce the 72-byte cap [21]128-bitSafe pre-hash only as bcrypt(base64(hmac-sha384($pw, key=pepper)))
PBKDF2 (FIPS lane)HMAC-SHA256 600,000; HMAC-SHA512 210,000; HMAC-SHA1 1,300,000 (legacy) [21]32 bits or more; use 16 bytesThese are the dated-snapshot figures; the live page has since drifted up

From a scheme name to a real API call

Naming the scheme is not the same as shipping it, and the gap between them is where a surprising amount of security is lost. Here is the bridge from each scheme to the concrete binding a protocol designer actually calls.

SchemeWhere to call it, in real code
Argon2idlibsodium crypto_pwhash, the PHC reference library, argon2-cffi (Python), PHP PASSWORD_ARGON2ID, golang.org/x/crypto/argon2, Rust argon2, node-argon2, and .NET wrappers [10]
yescryptlibxcrypt through crypt(3), plus Openwall's reference code [8]
scryptOpenSSL, libsodium, Python hashlib.scrypt, Node crypto.scrypt [23]
bcryptPHP PASSWORD_BCRYPT, Spring Security, Django, and bcrypt libraries for Ruby, Node, and Python [6]
PBKDF2Every standard library: .NET Rfc2898DeriveBytes, Python hashlib.pbkdf2_hmac, Django's default hasher [20]

The four things you always do

Whatever the tree selected, four habits are non-negotiable. NIST SP 800-63B-4 section 3.1.1.2 requires (SHALL) two of them -- salted and hashed storage with an approved one-way function, and verification of the entire password with no truncation -- and recommends (SHOULD) a third, a stored algorithm-and-cost reference so you can migrate; it also calls for throttling on repeated failures [25].

First, generate a 16-byte per-password salt from a CSPRNG. NIST's floor is a mere 32 bits; treat that as a floor to clear by a wide margin, not a target, and generate it the way Part 2, "Predictable or Repeated," insists [25]. Second, store the algorithm and its parameters in MCF form, so a verifier can dispatch on the scheme and upgrade in place. Third, compare verifiers in constant time, so you never leak how many bytes an attacker has guessed.

Fourth, rehash on login: when the stored cost sits below today's target, transparently recompute the verifier from the plaintext you just checked, and write the stronger value back. That last habit turns a parameter choice from a one-time decision into a living one -- and it is short enough to paste in.

JavaScript The login handler pattern: constant-time verify, then rehash on login
// Mock only -- this illustrates control flow, not real crypto.
// A real system calls its library's verify(), needsRehash(), and hash().

var TODAYS_TARGET_COST = 12; // e.g. a bcrypt work factor, or an Argon2 (m,t,p) tuple

// Constant-time comparison: never return early on the first differing byte,
// or the timing leaks how much of the verifier the attacker has matched.
function constantTimeEquals(a, b) {
if (a.length !== b.length) return false;
var diff = 0;
for (var i = 0; i < a.length; i++) {
  diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return diff === 0;
}

function mockHash(password, cost) {
// Stand-in for bcrypt or Argon2id. Real code never rolls its own.
return "cost=" + cost + ":" + password.split("").reverse().join("");
}
function costOf(verifier) {
return parseInt(verifier.split(":")[0].split("=")[1], 10);
}

function login(password, stored, updateStore) {
var candidate = mockHash(password, costOf(stored));
if (!constantTimeEquals(candidate, stored)) {
  return "reject"; // and throttle repeated failures for this account
}
// Verified. If the stored cost is below today's target, upgrade transparently.
if (costOf(stored) < TODAYS_TARGET_COST) {
  updateStore(mockHash(password, TODAYS_TARGET_COST));
  return "accept, and rehashed from cost " + costOf(stored) + " to " + TODAYS_TARGET_COST;
}
return "accept";
}

var stored = mockHash("correct horse battery staple", 10); // hashed years ago at cost 10
console.log(login("correct horse battery staple", stored, function (h) {
console.log("  stored verifier upgraded ->", h.slice(0, 16) + "...");
}));
console.log(login("wrong password", stored, function () {}));

Press Run to execute.

Peppering, and server-cost budgeting

Two operational topics finish the guide. The first is peppering -- powerful, and easy to get wrong in a way you cannot undo.

The second is the budget the whole design lives inside. Memory multiplied by concurrency is the real constraint, not CPU: Argon2id at 1 GiB per hash across 200 simultaneous logins wants roughly 200 GiB of transient RAM. That is the concrete reason the deployed floors of 19 to 64 MiB sit so far below RFC 9106's 2 GiB ideal [9, 21].

Target somewhere between half a second and one second per hash, measured on your own hardware. Rate-limit the login endpoint, because an attacker who floods it makes you pay the expensive hashing cost -- a denial-of-service lever you handed them. And where the traffic justifies it, put a cheap pre-filter in front of the expensive hash. The parameters are not a trophy you set once; they are a running lease on your own server budget.

Measure your own hash latency before you pick parameters

The parameter tables above are starting points, not answers -- the only number that finally matters is how long a hash takes on your hardware. The Argon2 reference CLI measures it directly. This runs Argon2id at 64 MiB, three passes, and four lanes, and prints the elapsed time:

echo -n "correct horse battery staple" | argon2 mysalt1234 -id -t 3 -m 16 -p 4

Here -id selects the Argon2id variant and -m 16 means 2^16 KiB, which is 64 MiB. Raise -m until the reported time approaches your latency budget -- roughly half a second to a second -- then hold there, and re-measure on the hardware you will actually deploy on, not your laptop [10].

The checklist is short because the lesson is one sentence. But checklists are not what fail in production -- confident, wrong sentences are, the kind that survive code review because they sound obviously true. Clear those before the conclusion, and the bug that has haunted this entire article finally has nowhere left to live.

11. FAQ and Common Misuse: The Confident, Wrong Sentences

Broken password storage survives mostly because a handful of plausible-sounding claims keep getting said in design meetings, and a handful of code patterns keep getting shipped past review. Named and corrected, they lose their power. Here is the consolidated catalog of what goes wrong in real code, each pattern mapped to the named breach it reproduces and the one change that fixes it.

Misuse pattern in the codeThe break it reproducesThe fix
A bare fast hash (MD5, SHA-1, SHA-256) as the password storeLinkedIn, RockYouUse a deliberately slow, ideally memory-hard KDF [1]
bcrypt silently truncating input at 72 bytesLong passphrases lose entropy past byte 72Enforce the 72-byte cap explicitly, or pre-hash correctly [6, 21]
Naive unsalted pre-hash bcrypt(md5($pw)), and the $2a$ NUL bugPassword shuckingSalt or HMAC the inner step and base64-encode it [16]
Argon2d (data-dependent) on shared or multi-tenant hostsCache-timing exposure of the access patternPrefer Argon2id, whose first half-pass is data-independent [9]
A global salt, a reused salt, or no saltLinkedIn: rainbow tables and cross-account hash sharingA unique per-password CSPRNG salt, stored in the MCF string [1]
A parallel weak code path that also touches the passwordAshley Madison's fast MD5 $loginkey beside strong bcryptAudit every function that sees the password, not just the verifier [13]
Encrypting the password instead of hashing itAdobe's reversible 3DES in ECB modeHash, never encrypt, a stored password [12]
Storing the pepper next to the hash it protectsDefeats the entire point of a secretKeep the pepper in a separate HSM, KMS, or TEE [25]
Parameters chosen once and never raisedA 2019 cost is weaker every year afterRehash on login and lift the floor as hardware improves
No maximum input length feeding PBKDF2Django's 2013 long-password DoS (CVE-2013-1443)Cap the accepted input length; Django's fix rejected inputs over 4096 bytes [33]
Verifier comparison that short-circuits on the first byteA timing side channel on the compareConstant-time equality, plus throttling of failed attempts [25]
"Memory-hardness is always mandatory"Over-provisioned Argon2id dialed to a DoS-safe floorTreat memory-hardness as the default, not a universal law [22]

The Django entry is worth one sentence, because it is the misuse people least expect. Django set no maximum on password length, so an attacker could submit a roughly one-megabyte password -- about a minute of PBKDF2 work -- purely to exhaust server CPU, a self-inflicted denial of service the fix closed by rejecting any input over 4096 bytes [33].

With the code patterns cleared, here are the sentences themselves -- the confident, wrong claims, and the short answers that retire them.

Frequently asked questions

SHA-256 is strong and one-way, so why is it not fine for passwords?

Because its strength is not the property that matters, and its speed is the flaw. A password hash exists to make each guess expensive; SHA-256 is engineered to make each evaluation cheap, so one GPU tries tens of billions of guesses per second and a stolen dump falls to a wordlist in minutes [2]. Use a deliberately slow, ideally memory-hard function -- Argon2id, scrypt, bcrypt, or PBKDF2 -- not a general-purpose hash.

Do I still need a salt if I use bcrypt or Argon2?

They already salt for you: bcrypt and Argon2id generate a per-password salt and embed it in the MCF string. What you must never do is reuse a salt, use a single global salt, or omit it -- that is precisely the LinkedIn failure, where identical passwords collided and precomputed tables applied [1]. The salt is public and stored beside the hash; it is not the secret, and it changes attacker throughput by zero.

Is bcrypt obsolete, and should everything move to Argon2?

No. bcrypt (1999) and PBKDF2 (2000) were never superseded, only joined by the memory-hard generation. bcrypt at work factor 10 or higher is a defensible choice for many deployments -- yescrypt's own author says as much for smaller ones -- and a FIPS constraint forces PBKDF2 regardless [22]. Memory-hardness is the recommended default, not a universal mandate.

What is a pepper, and do I need one?

A pepper is a secret, server-held value -- kept in an HSM or KMS, separate from the database -- mixed into the hash so a stolen database alone cannot be cracked [25]. It is defense-in-depth only, never a substitute for a real password hash. If you use one, encrypt the hash under it (rotatable, the design Dropbox shipped) rather than HMAC it into the one-way step (not rotatable), and never store it next to the hash [14].

Is it safe to pre-hash to get around bcrypt's 72-byte limit?

Only if you do it the careful way. A naive bcrypt(md5($pw)) with an unsalted inner hash enables password shucking, and a NUL byte in the pre-hash can truncate the input [16]. Salt or HMAC the inner step and base64-encode it -- bcrypt(base64(hmac-sha384($pw, key=pepper))) -- or enforce the 72-byte cap and skip pre-hashing entirely.

Does quantum computing break password hashes?

No. Grover's algorithm offers only a square-root speedup on guessing, which roughly halves effective bit-strength, and that gain is swamped by the memory-bandwidth and per-guess costs a good password hash already imposes. No Shor-style structural break applies to a one-way hash [32]. Your multi-factor and password-reuse posture matter far more to your real risk.

How do I choose parameters for my server?

There is no closed-form answer, which is why this is still an open problem. Target roughly half a second to one second per hash, measured on your own hardware, within your RAM-times-peak-concurrency and denial-of-service budget. Start from RFC 9106's two configurations or OWASP's floors, and raise them over time with rehash-on-login as your hardware and your traffic allow [9, 21].

Every correction here points at the same root. The strength of the hash was never the variable that decided the outcome. The cost per guess -- on the attacker's cheapest hardware, measured against your own server's budget -- always was. That is the sentence the whole article was built to earn, so it is time to say it in full.

One Job, Priced Two Ways

Return to the LinkedIn dump from the first page: a strong hash, no key to break, plaintext in days -- because the function was fast, and fast is the one thing a password hash must never be. Everything between here and there was the field learning, and relearning, that the hash's cryptographic strength was never the variable. The cost of one guess was.

Read as one arc, forty-seven years of password hashing is a single economic move, repeated. Morris and Thompson's 1979 rules -- one-way, salted, slow -- set the terms. Every advance since has restored the same eroding ratio: the cost of one attacker guess divided by the cost of one honest login.

Faster CPUs eroded fixed iteration counts, so bcrypt made the count a knob and added an expensive key schedule. Cheap parallel hardware -- GPUs, FPGAs, ASICs -- eroded time-hardness itself, so scrypt and Argon2 changed the shape of the cost from time to memory, the one resource an attacker cannot buy asymmetrically cheaply. Botnets and cheap RAM erode even that, so yescrypt adds a site-specific ROM. Iteration time, then an expensive key schedule, then memory, then memory plus a ROM: four levers, one idea.

The failure catalog proves the spine from the other side, because every entry is the ratio collapsing somewhere the verifier column could not see. Ashley Madison ran genuinely strong bcrypt and lost anyway, to a fast MD5 token stored beside it [13]. Facebook's at-rest hashing was undone by plaintext in a log file [15]. Adobe encrypted where it should have hashed [12]. Password shucking turned a careless pre-hash back into a fast-hash problem [16]. In none of these did memory-hardness matter, because the weakest function that touched the password was never the one on the spec sheet.

So the resolution the field converged on is not a single winner. It is a priced menu: Argon2id and yescrypt where you can afford memory-hardness, bcrypt and PBKDF2 where you cannot or where a regulator forbids it -- each choice an economic decision bounded by your own server's budget, exactly as Part 1's attacker model and Part 10's "a fast hash is the wrong tool" both predicted. Keep the one-question test where you can reach it.

On the cheapest hardware an attacker can rent, how much does one guess at one password cost -- and how far above your own per-login cost have you pushed it?

The hash was never the weak link, and no future hash will be, because the weak link is economic, not cryptographic. That is why this is Part 12 of a field guide to protocol design rather than a catalog of algorithms. The job was never to pick the strongest function. It was to price a guess correctly, on both sides of the ledger.

Study guide

Key terms

Stored verifier
A salted, deliberately slow, one-way transform of a password, stored so that a leaked database does not directly reveal the passwords.
Offline attack
The threat model this primitive answers: the adversary holds the stolen verifiers and guesses locally at full hardware speed, with no server or rate limit in the loop.
Salt
A public per-password random value that makes each verifier unique and defeats precomputed tables; it does not change attacker throughput.
Modular Crypt Format (MCF)
A self-describing hash string, id then params then salt then hash, that lets one column hold many algorithms and migrate in place.
Work factor (adaptive cost)
A tunable knob -- bcrypt cost, PBKDF2 iterations, Argon2 time and memory -- that raises per-guess cost as hardware improves, without changing the algorithm.
Memory-hard function (MHF)
A function whose cheap computation requires holding a large array in memory for much of its runtime, imposing an area-times-time cost.
AT-cost (area times time)
Memory occupied multiplied by time held: the attacker's real per-guess cost, and the quantity the memory-hard program maximizes.
Data-independent vs data-dependent access
Whether memory addresses depend on the password; independent is cache-timing-safe but provably suboptimal, dependent is optimal but leaks through timing.
Pepper (secret salt)
A secret, server-held value kept in an HSM or KMS, separate from the database, mixed into the hash as defense-in-depth only.

References

  1. Troy Hunt (2012). Our password hashing has no clothes. https://www.troyhunt.com/our-password-hashing-has-no-clothes/
  2. (2023). hashcat v6.2.6 Benchmark -- NVIDIA GeForce RTX 4090. https://raw.githubusercontent.com/0x3444/hashcat_benchmarks/main/NVIDIA_GeForce_RTX_4090.txt
  3. Daniel Miessler, Jason Haddix, & g0tmi1k (2015). SecLists: rockyou.txt (Leaked-Databases wordlist). https://github.com/danielmiessler/SecLists
  4. Robert Morris & Ken Thompson (1979). Password Security: A Case History. https://dl.acm.org/doi/10.1145/359168.359172
  5. Poul-Henning Kamp (2012). Md5crypt Password scrambler is no longer considered safe by author. https://phk.freebsd.dk/sagas/md5crypt_eol/
  6. Alexander Peslyak (2025). crypt_blowfish. https://www.openwall.com/crypt/
  7. Ulrich Drepper (2007). Unix crypt using SHA-256 and SHA-512. https://www.akkadia.org/drepper/SHA-crypt.txt
  8. (2025). libxcrypt: Extended crypt library for descrypt, md5crypt, bcrypt, and others. https://github.com/besser82/libxcrypt
  9. Alex Biryukov, Daniel Dinu, Dmitry Khovratovich, & Simon Josefsson (2021). Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications (RFC 9106). https://www.rfc-editor.org/rfc/rfc9106.html
  10. Alex Biryukov, Daniel Dinu, & Dmitry Khovratovich (2015). Argon2 Reference Implementation and Specification (phc-winner-argon2). https://github.com/P-H-C/phc-winner-argon2
  11. Imperva Application Defense Center (2010). Consumer Password Worst Practices (RockYou breach analysis, archived snapshot). https://web.archive.org/web/20260603050905/https://www.imperva.com/docs/gated/WP_Consumer_Password_Worst_Practices.pdf
  12. (2013). How an epic blunder by Adobe could strengthen hand of password crackers. https://arstechnica.com/information-technology/2013/11/how-an-epic-blunder-by-adobe-could-strengthen-hand-of-password-crackers/
  13. CynoSure Prime (2015). How we cracked millions of Ashley Madison bcrypt hashes efficiently. https://blog.cynosureprime.com/2015/09/how-we-cracked-millions-of-ashley.html
  14. (2016). How Dropbox securely stores your passwords. https://dropbox.tech/security/how-dropbox-securely-stores-your-passwords
  15. Brian Krebs (2019). Facebook Stored Hundreds of Millions of User Passwords in Plain Text for Years. https://krebsonsecurity.com/2019/03/facebook-stored-hundreds-of-millions-of-user-passwords-in-plain-text-for-years/
  16. Scott Brady (2020). Beware of Password Shucking. https://www.scottbrady.io/authentication/beware-of-password-shucking
  17. Niels Provos & David Mazieres (1999). A Future-Adaptable Password Scheme. https://www.usenix.org/legacy/events/usenix99/provos.html
  18. Colin Percival (2009). Stronger Key Derivation via Sequential Memory-Hard Functions. https://www.tarsnap.com/scrypt/scrypt.pdf
  19. Burt Kaliski (2000). PKCS #5: Password-Based Cryptography Specification Version 2.0 (RFC 2898). https://datatracker.ietf.org/doc/html/rfc2898
  20. Kathleen Moriarty, Burt Kaliski, & Andreas Rusch (2017). PKCS #5: Password-Based Cryptography Specification Version 2.1 (RFC 8018). https://datatracker.ietf.org/doc/html/rfc8018
  21. (2025). OWASP Password Storage Cheat Sheet (dated snapshot 2025-06-02). https://web.archive.org/web/20250602043756/https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
  22. Alexander Peslyak (2025). yescrypt - scalable KDF and password hashing scheme. https://www.openwall.com/yescrypt/
  23. Colin Percival & Simon Josefsson (2016). The scrypt Password-Based Key Derivation Function (RFC 7914). https://datatracker.ietf.org/doc/html/rfc7914
  24. (2015). Password Hashing Competition. https://www.password-hashing.net/
  25. (2025). NIST SP 800-63B-4: Digital Identity Guidelines -- Authenticators and Verifiers. https://pages.nist.gov/800-63-4/sp800-63b/authenticators/
  26. (2025). Cryptography API: Next Generation (CNG). https://learn.microsoft.com/en-us/windows/win32/seccng/cng-portal
  27. Joel Alwen & Jeremiah Blocki (2016). Efficiently Computing Data-Independent Memory-Hard Functions. https://eprint.iacr.org/2016/115
  28. Jeremiah Blocki & Samson Zhou (2017). On the Depth-Robustness and Cumulative Pebbling Cost of Argon2i. https://eprint.iacr.org/2017/442
  29. Joel Alwen, Jeremiah Blocki, & Ben Harsha (2017). Practical Graphs for Optimal Side-Channel Resistant Memory-Hard Functions. https://eprint.iacr.org/2017/443
  30. Dan Boneh, Henry Corrigan-Gibbs, & Stuart Schechter (2016). Balloon Hashing: A Memory-Hard Function Providing Provable Protection Against Sequential Attacks. https://eprint.iacr.org/2016/027
  31. Alex Biryukov & Dmitry Khovratovich (2015). Tradeoff Cryptanalysis of Memory-Hard Functions. https://eprint.iacr.org/2015/227
  32. Lily Chen, Stephen Jordan, Yi-Kai Liu, Dustin Moody, Rene Peralta, Ray Perlner, & Daniel Smith-Tone (2016). NISTIR 8105: Report on Post-Quantum Cryptography. https://csrc.nist.gov/pubs/ir/8105/final
  33. Django Software Foundation (2013). Django security releases issued: 1.4.8, 1.5.4, and 1.6 beta 4 (CVE-2013-1443). https://www.djangoproject.com/weblog/2013/sep/15/security/