How passwords are stored
How websites store passwords so that a stolen database does not reveal them, from hash functions and salts to slow and memory-hard functions such as PBKDF2, scrypt and Argon2, and the defenses that go beyond the database.
Every website with a login has to check passwords, so it has to store something that lets it recognize the right one. That something is a prime target: databases get stolen, through attacks, mistakes or insiders, and many people use the same password on several sites. A stolen list of passwords is a key to many other accounts.
Good password storage is designed for that worst case. Its goal is simple to state: even with the whole database in hand, an attacker should find it as slow and expensive as possible to recover any password. This article shows how that is done, step by step, from the obvious mistakes to the memory-hard functions recommended today.
What not to do
Storing passwords as they are is the worst choice: anyone who sees the database, including the site’s own staff and backups, can read every password.
Encrypting passwords is hardly better. Encryption can be undone with the key, and the key must be available to the login code, so it usually ends up in the same place as the database. An attacker who steals one is likely to get the other.
What a site needs is a way to check a password without being able to recover it. That is exactly what a one-way function provides.
Hash functions
A cryptographic hash function such as SHA-256 (RFC 6234) turns any input into a fixed-size fingerprint, and it cannot be run backwards. The same input always gives the same output, and the smallest change gives a completely different one:
SHA-256("sunflower") = 8f398886c326b5f8f07b20ac250c87de6723e062474465273fe1524f2b9092fa
SHA-256("Sunflower") = 875af21cd687e882102a1fccd89ed2a613b922cbd8210b06552884a5e3075914
A site could store only the hash. At login, it hashes the password the user typed and compares the result with the stored hash; if they match, the password was right. The password itself is never stored.
But a plain hash has two serious weaknesses:
- The same password always gives the same hash. Everyone whose password is “sunflower” has the same entry in the database, so cracking one cracks them all. Worse, an attacker can compute the hashes of millions of common passwords in advance and simply look up the stolen hashes in that list.
- Hash functions are fast. They are designed to process large amounts of data quickly. An attacker who tries candidate passwords one after another, a dictionary or brute-force attack, can therefore try an enormous number of guesses on ordinary hardware.
The next two steps address these weaknesses one at a time.
Salt: a different hash for every user
A salt is a random value, generated separately for each password when it is set, that is mixed into the hash and stored next to it. The salt is not secret; its job is to make every hash unique.
With salts, the two weaknesses of the first point disappear:
- Two users with the same password get different stored values, so cracking one tells the attacker nothing about the other.
- Precomputed tables become useless. As RFC 8018 puts it, with a 64-bit salt “there will be as many as 2⁶⁴ keys for each password”, so an attacker can only start guessing once they have the salt, and must guess each password separately.
A salt must be unpredictable and long enough never to repeat. RFC 9106 recommends 16 bytes (128 bits) for password hashing, and it should be produced by a cryptographically secure random number generator (RFC 4086).
Slow by design: key stretching
Salt does not slow down guessing a single password. For that, the computation itself must be slow. Key stretching repeats a cryptographic function many times, so that each guess costs the attacker as much as it costs the site at login.
PBKDF2, defined in RFC 8018, is the classic example. It applies HMAC (RFC 2104), a keyed hash, thousands or millions of times in a row. Its main parameter is the iteration count c, which, according to RFC 8018, “will increase the security strength of a password by log2(c) bits” against guessing: one million iterations add about 20 bits, as if the password were twenty random bits longer. RFC 8018 quotes NIST’s advice that the count should be “as large as possible, as long as the time required to generate the key using the entered password is acceptable for the users”.
The password “sunflower” with two different salts, using PBKDF2 with SHA-256 and 100,000 iterations (an example value):
salt 8c1f3a5e9b2d4f60a7c3e1b5d9f20468 → 6713823d108b05775db3a2388d249388578aeb838ce7c02af4bb0f8ef6afaf4f
salt 2b7e151628aed2a6abf7158809cf4f3c → 6d75bb6870abbe1eebcef6317b2288c3d3716c557aba034951e403dd982ecbb5
The site does the expensive computation once per login, which a user hardly notices. An attacker has to do it once per guess, for every user separately.
Memory-hard functions
Iterations make each guess slower, but attackers do not use ordinary computers. Graphics cards and custom chips can run a great many hash computations in parallel, each needing only a tiny amount of memory, so a simple iterated hash is much cheaper for them than for the website.
Memory-hard functions close this gap. They need a large amount of memory for each computation, and memory is the resource that massively parallel hardware cannot provide cheaply. Two are specified as RFCs:
- scrypt (RFC 7914), “based on memory-hard functions, which offer added protection against attacks using custom hardware”.
- Argon2 (RFC 9106), a newer design that aims to fill memory as fast as possible. Of its three variants, Argon2id is the one every implementation must support and the one RFC 9106 recommends: it combines protection against side-channel attacks with protection against attacks that trade memory for time.
Argon2 has three main parameters: memory (m), passes over the memory (t) and parallel lanes (p). RFC 9106 gives two default choices:
| Option | Memory | Passes | Lanes | Salt | Output |
|---|---|---|---|---|---|
| First recommended option | 2 GiB | 1 | 4 | 128 bits | 256 bits |
| Second recommended option, for less memory | 64 MiB | 3 | 4 | 128 bits | 256 bits |
Otherwise, RFC 9106 advises picking the most memory and time that each login can afford, and tuning the parameters to the server.
How a login works
Putting it together, a site stores four things for each password: the algorithm, its parameters, the salt and the result.
Storing the algorithm and parameters with each record has a practical benefit: when the site later raises the parameters, or moves to a better algorithm, it can upgrade each password the next time its owner logs in, when the plain password is briefly available.
Two smaller details also matter:
- Normalize first. The same visible password can be encoded in different ways in Unicode, for example an “é” written as one character or as “e” plus an accent. RFC 8265 defines how to prepare passwords, including Unicode normalization (NFC), so that the same password always produces the same bytes.
- A secret key can add a layer. Some sites also mix in a secret key that is kept outside the database, for example with HMAC; Argon2 even has an optional input for such a secret. An attacker who steals only the database then cannot test guesses at all.
Beyond the database
Storage protects passwords after a breach. Other defenses protect accounts before one:
- Limit guessing at the login form, for example by slowing down repeated failures, so attackers cannot simply try passwords against the live site.
- Add a second factor. One-time codes from an authenticator app use TOTP (RFC 6238): the app and the site share a secret key, and both compute a code from that key and the current time, in steps of 30 seconds by default, using the HMAC-based method of HOTP (RFC 4226). A stolen password alone is then not enough.
- Never send the password to the server at all. With protocols such as OPAQUE (RFC 9807), the client proves that it knows the password while hiding it from the server, “even during password registration”, and what the server stores is protected against precomputed tables.
Key points
- Never store passwords in plain text or with reversible encryption; store the output of a one-way function.
- A unique random salt per password makes identical passwords look different and makes precomputed tables useless.
- Key stretching (PBKDF2) makes each guess expensive in time; memory-hard functions (scrypt, Argon2id) make it expensive in memory too, which defeats parallel attack hardware.
- RFC 9106 recommends Argon2id, for example with 2 GiB of memory and one pass, or 64 MiB and three passes.
- Store the algorithm and parameters with each hash so they can be upgraded, and protect accounts further with rate limits, second factors and protocols such as OPAQUE.
Sources
- US Secure Hash Algorithms (SHA and SHA-based HMAC and HKDF) (RFC 6234)D. Eastlake 3rd, T. Hansen, RFC Editor, 2011Accessed
- Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications (RFC 9106)A. Biryukov, D. Dinu, D. Khovratovich, S. Josefsson, RFC Editor, 2021Accessed
- PKCS #5: Password-Based Cryptography Specification Version 2.1 (RFC 8018)K. Moriarty, B. Kaliski, A. Rusch, RFC Editor, 2017Accessed
- The scrypt Password-Based Key Derivation Function (RFC 7914)C. Percival, S. Josefsson, RFC Editor, 2016Accessed
- Randomness Requirements for Security (RFC 4086)D. Eastlake 3rd, J. Schiller, S. Crocker, RFC Editor, 2005Accessed
- HMAC: Keyed-Hashing for Message Authentication (RFC 2104)H. Krawczyk, M. Bellare, R. Canetti, RFC Editor, 1997Accessed
- Preparation, Enforcement, and Comparison of Internationalized Strings Representing Usernames and Passwords (RFC 8265)P. Saint-Andre, A. Melnikov, RFC Editor, 2017Accessed
- HOTP: An HMAC-Based One-Time Password Algorithm (RFC 4226)D. M'Raihi, M. Bellare, F. Hoornaert, D. Naccache, O. Ranen, RFC Editor, 2005Accessed
- TOTP: Time-Based One-Time Password Algorithm (RFC 6238)D. M'Raihi, S. Machani, M. Pei, J. Rydell, RFC Editor, 2011Accessed
- The OPAQUE Augmented Password-Authenticated Key Exchange (aPAKE) Protocol (RFC 9807)D. Bourdrez, H. Krawczyk, K. Lewi, C. A. Wood, RFC Editor, 2025Accessed