How Password Storage Works: From Plaintext to Argon2

Services don't store your password as you typed it. Here's the thirty-year climb from plaintext to hashing to salting to slow KDFs — and what the 2012 LinkedIn breach taught everyone.

everytooleverywhereallatonce · Published

When you log in, all a service actually needs to decide is whether the password you know is the same one you chose when you signed up. The tempting way to check that — write your password into a database and compare — means that the moment the database leaks, every account is open. So over about thirty years the industry worked out how to verify a password without keeping the original. This is that staircase, one step at a time.

Step 0: Plaintext — why it fails

Storing passwords as typed breaks two ways. First, any insider or intruder who reads the database instantly knows every secret. Second, because people reuse the same password across sites, one breach feeds credential-stuffing attacks: attackers replay the leaked list against banks and email providers. The starting insight is that no service actually needs to know your original password.

Step 1: Hashing — don’t keep the original

A hash function turns any input into a fixed-size value from which the input cannot be recovered. So a service can store only the hash at sign-up, hash whatever you type at login, and compare the two — verifying you without holding the original. Unix systems did this from early on.

The trouble was using a fast hash. MD5 and SHA-1 were built to be as fast as possible for verifying files. An attacker holding a leaked list of such hashes can hash billions of common-password guesses per second and look for matches. Watching a short password fall almost instantly in the site’s hash cracker makes the weakness concrete. Worse, the same password always produces the same hash, so attackers could precompute a giant lookup table — a rainbow table — and reverse many hashes at once.

Step 2: Salting — defeat precomputation

A salt is a distinct random value attached to each password. Mixing the salt in before hashing means two users with the same password get different stored values, which neutralizes rainbow tables: an attacker would have to build a fresh table per salt, and with a different salt per user a single table can’t crack many accounts.

Salts are not secret — they are usually stored right next to the hash. Their job is to stop one computation from cracking many accounts at once, not to stay hidden. You can see the property directly in the hash generator: prepend a different value to the same text and the digest changes completely.

LinkedIn 2012: what happens without salt

In June 2012, hashes for roughly 6.5 million LinkedIn passwords appeared online. They were unsalted SHA-1. With no salt, public rainbow tables and dictionary attacks applied directly, and hundreds of thousands fell within days. In 2016, more than 117 million account records reportedly from the same breach were put up for sale again. LinkedIn moved to salting and slow hashing afterward and settled a class-action suit. It remains the textbook demonstration of why “hashed but not salted” storage is not enough.

Step 3: Slow KDFs — being fast was the problem

Even with salting, one issue remains: the hash itself is too fast. That prompted purpose-built, deliberately slow password functions, known as key derivation functions (KDFs).

  • bcrypt (1999): introduced by Niels Provos and David Mazières. It carries a cost factor; raising it by one doubles the work. As hardware speeds up, you just raise the cost to keep pace — which is why the paper called it “future-adaptable.” You can feel it by changing the cost in the bcrypt hash generator.
  • scrypt (2009): designed by Colin Percival and later written up as RFC 7914. It is deliberately memory-hard as well as slow, making massively parallel attacks on memory-light hardware (ASICs, GPUs) far more expensive.
  • Argon2 (2015): winner of the open Password Hashing Competition. It lets you tune memory, time and parallelism separately, and its side-channel-resistant Argon2id variant is today’s recommended default for new systems.

The point is the asymmetry. A real user’s login can afford a quarter of a second, but an attacker must multiply that quarter-second across hundreds of millions of leaked hashes, which quickly becomes unaffordable.

Where the bar sits now: NIST and OWASP

NIST’s Digital Identity Guidelines (SP 800-63B, 2025 revision) say stored passwords shall be salted and hashed with a suitable password-hashing scheme, with a salt of at least 32 bits and a cost factor as high as performance allows. They further recommend an extra keyed step with a secret known only to the verifier — a pepper. OWASP’s Password Storage Cheat Sheet gives concrete numbers: Argon2id with at least 19 MiB of memory, 2 iterations and 1 degree of parallelism; scrypt (cost 2^17) where Argon2 isn’t available; and for legacy systems, bcrypt with a work factor of 10 or more.

The advice on the passwords users choose has shifted too: favor length over composition rules that force character types, and don’t require periodic changes. Why length wins is easiest to see in the password generator, where the entropy in bits climbs with each added character.

Salt, pepper, and a common confusion

People often blur three ideas. A salt is unique per password, stored beside the hash, and its job is to make precomputation and cross-account attacks useless. A pepper is a single secret value shared across all passwords but kept outside the database — in application config or a hardware security module — so that a database-only leak (the most common kind) doesn’t hand attackers everything they need. If both leak, the pepper adds nothing; that’s why it must live somewhere the database dump won’t reach.

The other confusion is “hashing” versus “encryption.” Encryption is reversible by design: with the key you get the plaintext back. Hashing is one-way, and that irreversibility is the entire point for passwords — a service should never be able to email you your old password, only reset it. If a site can show you your existing password, it stored it reversibly, which is the red flag Step 0 was about.

Upgrading old hashes without a password reset

A frequent worry is that adopting a slow KDF means forcing everyone to reset. It doesn’t. The standard migration hashes the existing stored hash with the new function — for example, wrapping old unsalted SHA-1 values as bcrypt(sha1_hash) with a fresh salt and cost. Every stored record becomes strong immediately, and when a user next logs in you can transparently re-hash their actual password with the modern scheme and drop the wrapper. Raising a bcrypt cost factor later works the same way: re-hash at login and replace. Because the parameters (cost, salt, algorithm) travel inside the stored string, the verifier always knows how a given record was made and can compare correctly during the transition.

The short version

The history of password storage compresses to three moves: don’t keep the original, defeat precomputation, and make each guess expensive. Each step patched the hole the previous one left, and services that skipped a step — LinkedIn in 2012 — paid for it. If you are building something today the answer is settled: Argon2id (or bcrypt/scrypt as circumstances require) with a per-user salt, and users encouraged to pick long, random passwords.

Sources