Internet Security, October 2023

Password Hashing and Why Plaintext Kills

Storing passwords recoverably means that a single breach hands over every account, and because people reuse passwords, it hands over accounts on systems entirely unrelated to yours.

The obligation is therefore not only to your own users. It is to every other service where those credentials were reused.

Three operations that are frequently confused

Encoding transforms data reversibly with no key and provides no protection whatsoever. Encryption is reversible with a key, which means the key exists somewhere and can be stolen along with the data.

Hashing is one way. There is no key and no inverse, so verification works by hashing the supplied password and comparing, and the stored value cannot be turned back into the original.

Fast hashes are the wrong tool

General purpose hash functions are designed to be fast, which is exactly the property an attacker wants. Modern hardware computes billions of general purpose hashes per second, so a stolen database of them is tested against enormous wordlists in hours.

A password hash should be deliberately slow and deliberately expensive in memory, so that each guess costs the attacker real resources while a single legitimate login costs a fraction of a second.

The algorithms

AlgorithmDesigned forUse for passwords
Argon2idPasswords, memory hardPreferred
scryptPasswords, memory hardGood
bcryptPasswordsAcceptable, long established
PBKDF2Key derivationAcceptable where required
SHA-256General hashingNo
MD5, SHA-1General hashingNo

Memory hardness is what separates the top two. Requiring significant memory per guess removes the advantage of hardware that can run thousands of computations in parallel but cannot give each one enough memory.

Salt, and why it is not a secret

A salt is a unique random value stored alongside each hash and included when hashing. It does not need to be secret and it does not need to be protected.

Its purpose is to ensure two users with the same password produce different stored values, which defeats precomputed tables and forces an attacker to attack each account separately rather than the database as a whole.

Tuning the cost

Every suitable algorithm exposes parameters controlling how much work each hash requires, and the correct values depend on the hardware rather than on a number copied from documentation.

The usual approach is to raise the cost until a single hash takes a few hundred milliseconds on the production hardware, then keep it under review, because hardware improves and a value chosen five years ago is now cheap.

Hashing protects the wrong end of the attack

Storage protects credentials after a breach. It does nothing about guessing at the login page, where the attacker never touches the database at all.

That path needs its own controls. Rate limiting per account and per source, progressive delays rather than hard lockouts, which merely convert guessing into denial of service, and a second factor, which is the only measure that survives a correct password being known.

Checking new passwords against published breach corpora is the highest value addition available, because reused credentials are the single most common route to account takeover and the check costs almost nothing.

Migrating existing hashes

Existing values cannot be converted, because the original passwords are not recoverable, which is the point.

The workable approach is to rehash at the next successful login, when the password is briefly available, and to record which algorithm each stored value uses so both can be verified during the transition. Accounts that never log in are eventually forced to reset.

Note: the length limit in bcrypt is real and silent. Input beyond seventy two bytes is ignored, so a long passphrase can be truncated without any error, which is one reason newer algorithms are preferred for new work.