Store passwords with Argon2id, set to at least 19 MiB of memory, 2 iterations and 1 degree of parallelism, with a unique salt per password. That is the minimum configuration in the OWASP Password Storage Cheat Sheet. If your database still holds MD5, SHA-1 or unsalted hashes, you can fix that without a mass password reset, and without leaving weak hashes behind for dormant accounts.
What to use, and the minimum settings
The cheat sheet gives these minimums:
| Situation | Algorithm | Minimum settings |
|---|---|---|
| Preferred | Argon2id | 19 MiB memory, 2 iterations, parallelism 1 |
| Argon2id unavailable | scrypt | cost parameter 2^17, block size 8, parallelism 1 |
| Legacy systems on bcrypt | bcrypt | work factor 10 or more, 72-byte password limit |
| FIPS-140 compliance required | PBKDF2 | 600,000 or more, HMAC-SHA-256 |
Argon2id is the primary variant in RFC 9106, and any implementation of that document must support it. It works like Argon2i for the first half of the first pass over memory and like Argon2d for the rest.
The bcrypt 72-byte limit
The cheat sheet sets a 72-byte password limit for bcrypt. The sources do not say how individual libraries treat longer input, so test yours. Hash a password of 72 a characters followed by b, then verify 72 a characters followed by c. If verification succeeds, your library truncates silently.
Then either cap the password length in your forms at 72 bytes and say so, or plan the move to Argon2id. RFC 9106 allows passwords up to 2^32-1 bytes.
Why fast hashes fail
MD5 and SHA-1 were designed to be fast, and SHA-256 is unsuitable for the same reason: it lets an attacker make large numbers of guesses quickly. The attacker hashes candidates from leaked password lists, wordlists or brute force and compares the results with your rows. GPUs and rented cloud servers make that cheap.
A unique salt forces the attacker to crack hashes one at a time. It also defeats rainbow tables, and identical passwords stop producing identical hashes. RFC 9106 recommends a 16-byte salt for password hashing. Mature libraries generate and store the salt for you, so do not write that part by hand.
Tuning the work factor
There is no universal number. It depends on your server and your user count, and you find it by experiment on the hardware you run. The general rule is that one hash should take less than one second.
Going too high has a price. Every login attempt burns CPU, and a flood of attempts can exhaust the server. Benchmark under realistic concurrency and choose a value you can afford at peak load.
Check the unit your Argon2 library expects. RFC 9106 defines memory size in kibibytes, while the cheat sheet quotes MiB. 19 MiB is 19456 KiB, and passing 19 where the library wants KiB gives you 19 KiB of memory. That setting works and is far weaker than intended.
The work factor is typically stored in the hash output. That helps with migration, because at login you can see which parameters a given hash was created with.
Pepper as an extra layer
A pepper is a secret shared across all stored passwords and kept outside the database, in a secrets vault or an HSM. It helps when an attacker gets only the database, through SQL injection or a stolen backup. On its own it adds nothing, so treat it as defence in depth.
Rotation is the problem. A compromised pepper cannot be changed without knowing each user's password, so replacing it forces a reset for everyone it protected. Decide who owns that procedure before you introduce a pepper.
Upgrading old hashes
The cheat sheet's approach for raising a work factor is to wait until the user next authenticates, then re-hash their password with the new settings. Moving from MD5, SHA-1 or unsalted hashes follows the same logic, though that extension is ours.
- Mark every stored hash with its algorithm, in a separate column or through the hash format. If you are unsure what is in the table, the hash identifier helps you recognise the formats.
- At login, verify with whatever algorithm the stored hash uses.
- On success, if the hash is legacy or below your current parameters, compute a new Argon2id hash from the submitted password with a fresh salt.
- Overwrite the old value in the same operation.
ok = verify(row.scheme, row.hash, submitted)
if ok and (row.scheme != "argon2id" or needs_rehash(row.hash)):
row.hash = argon2id_hash(submitted) # fresh salt, current parameters
row.scheme = "argon2id"
save(row)
Dormant accounts
Users who never log in again never get upgraded, so their weak hashes sit in the table indefinitely. One fix is to wrap them now. Run Argon2id over the existing MD5 or SHA-1 value, store the result with a scheme marker such as argon2id(md5), and delete the bare hash.
At login you compute the legacy hash of the submitted password, verify it against the Argon2id wrapper, and on success replace the wrapper with a plain Argon2id hash. Record exactly how the legacy value is encoded (hex or raw bytes), because a mismatch breaks every login for that scheme. Test the wrapped path against a copy of the table first.
Wrapping does not make a weak password strong. It stops a stolen table from being attacked at MD5 speed.
The alternative is to remove the old hashes after a cut-off date and require a reset at next sign-in, which the cheat sheet describes as an option depending on the application. If the change touches a codebase you do not fully control, our secure software development team can review it.
Where to start
- Audit the user table: which algorithms, whether salts exist, and whether any passwords are plaintext or reversibly encrypted. Encryption is acceptable only where you must recover the original password, and the cheat sheet says to avoid needing that.
- Choose Argon2id with at least 19456 KiB, 2 iterations and parallelism 1, and benchmark it on production hardware until one hash takes under a second.
- If you are on bcrypt, run the 72-byte test above and decide between a length cap and a move to Argon2id.
- Add an algorithm marker to the schema, change the login code to verify with the old scheme and re-hash on success.
- Wrap the remaining legacy hashes in Argon2id, or set a cut-off date after which they are removed and a reset is required.
- If you add a pepper, store it in a secrets vault and write down the rotation and forced-reset procedure first.
