Skip to content
AhmadKhidir

POST

The last password hashing guide you need

Jul 20268 MIN READ

#security#passwords#cryptography#backend

Every few months, a breach happens, and the leaked database contains passwords that were hashed with MD5, or SHA-1, or a fast variant of SHA-256, and the security community does the same dance. Someone posts that these hashes are being cracked at billions of guesses per second. A developer replies that they did not know, or that it is legacy code, or that it was fine at the time. The phrase "at the time" is doing a lot of work, because MD5 was never fine for passwords. It was designed to be fast, and speed is the one property that makes a password hash useless.

The good news is that the answer has been settled for years, and it is not complicated. Use a password hashing function that is deliberately slow, with per user salt, tune the cost for the hardware you run, and plan to upgrade the cost as hardware gets faster. The OWASP password storage cheat sheet says this in more detail, and the underlying history explains why each piece exists. Understanding the history turns the checklist from a list of rules into a set of reasons.

The era of fast hashes

The story starts with the mistake that keeps repeating. Cryptographic hash functions like MD5 and SHA-1 were designed for integrity checking, not password storage. They were built to be fast, because their job was to checksum files and verify messages, and speed was a feature. When early systems stored passwords with these functions, they inherited the speed, and the speed became the vulnerability.

Here is the arithmetic that makes fast hashes fatal. An attacker who steals a database of MD5 hashes can guess passwords at enormous rates, because computing MD5 is cheap. The modern GPU can try billions of hashes per second. A dictionary of common passwords takes seconds to run against the whole database. The security of the scheme rests entirely on the passwords being unpredictable, and the history of breaches is the history of predictable passwords, "password", "123456", "letmein", being cracked by exactly this process.

The shift that fixed this was the realization that password hashing needs to be slow, and slow in a specific way. The functions designed for this, bcrypt, scrypt, PBKDF2, and later Argon2, take a parameter that controls how much work the hash costs, and the parameter is part of the stored hash. When the attacker cracks, they pay the same cost for every guess, and the cost is chosen so that guessing a strong password is impractical.

The salt is not optional

A salt is a random value, unique to each user, that gets mixed into the hash. Its job is to make identical passwords produce different hashes, and to break the economics of the attacker's precomputation. Without a salt, the attacker can precompute hashes for the million most common passwords and compare against every hash in the database at once. With per user salts, the precomputation dies: each guess must be hashed with each user's salt, and cracking one password tells you nothing about the next.

The salt has a second property that is easy to miss. It means two users with the same password have different hashes, so the attacker cannot tell they share a password. That sounds like a minor privacy point until you realize that telling "these two accounts use the same password" is the beginning of a much worse attack.

The practical advice from OWASP is that the libraries handle the salt automatically, which is true of the good libraries and the reason to use them. The bad implementations are the ones that try to roll their own salt management and get it wrong, storing the salt in the same place as the hash, which is fine, or forgetting the salt entirely, which is not.

The modern choice: Argon2id

The current recommendation, and the one worth reaching for in new systems, is Argon2id. Argon2 won the Password Hashing Competition in 2015, which was an open contest to design a password hashing function that could resist the attacks that were breaking the older ones, and it has three parameters instead of one. The memory parameter controls how much RAM the hash uses, the iterations control the CPU cost, and the parallelism controls threads. The reason the memory parameter matters is the GPU: a GPU cracks fast because it has many cores, but memory is harder to scale, and a function that uses a lot of memory is a function that does not run well on a GPU cracker.

OWASP currently recommends a minimum configuration of 19 MiB of memory, two iterations, and one degree of parallelism, with the note that the exact numbers are a tradeoff between your server's capacity and the attack cost, and that the hash should take around half a second to compute. The point is not the exact number. The point is that the number is tunable and that you should tune it to the hardware you run, and raise it over time as the hardware gets cheaper.

The also ran list is worth knowing for legacy reasons. scrypt is the second choice when Argon2 is unavailable, with its own memory parameters. bcrypt is the legacy workhorse, and the one a huge number of existing systems use, and the one with a famous footgun: it truncates passwords at 72 bytes, so a very long password is silently cut off, and the truncation is a real issue for users who use passphrases. PBKDF2 is the one that exists because FIPS certification requires it, and when it is used it needs a high iteration count, on the order of hundreds of thousands, because it has no memory hardness and GPUs tear through it.

The pepper debate

The salt is public, stored with the hash, and its job is to make each hash unique. A pepper is different: a secret value, shared across the system, not stored with the hash. It is defense in depth for the specific case where the attacker has the database but not the application server, which is the case for a SQL injection or a leaked backup.

The pepper is a genuinely good idea with a genuinely annoying cost: if the pepper leaks or needs to change, every hash in the database has to be redone, which means every user has to reset their password. The honest framing is that the pepper protects against a narrower attack than the salt, and it is optional. The salt is not optional. The teams that get the priority right, salt first, slow hash second, pepper third, are the teams that sleep fine.

The password shucking attack

There is a specific attack on bad bcrypt implementations that is worth knowing because it shows how the pieces fit together. Some teams, trying to handle bcrypt's 72 byte limit, pre hash the password with SHA-512 and then bcrypt the result. The attack, called shucking, exploits the fact that if the inner hash is known and the bcrypt hashes leak, the attacker can reduce the problem to cracking the inner hash, which is fast. The lesson is not "do not pre hash". The lesson is that combining primitives is subtle, and the pre hash needs an HMAC with a pepper, not a bare fast hash.

The general lesson is bigger than the specific attack. Password storage is a place where rolling your own is a bad idea, and the reason is that the failure modes are subtle and the attacker knows them all. The libraries that exist, in every major language and framework, have the salt handling, the parameter defaults, and the constant time comparisons built in. Use them, and use the defaults, and spend your effort on the one thing the library cannot do: choosing a cost that matches your hardware and raising it over time.

The upgrade path

The last piece of the discipline is the plan for the future. Hardware gets faster, and the cost that is expensive today is cheap in five years. The system that was secure at launch needs to raise its cost over time, and the mechanism is the same one used for years: when the user next logs in, re hash the password with the new cost, and update the stored hash. The old hashes age out naturally as users return.

The honest version of the plan includes the legacy problem. There are systems out there with MD5 and SHA-1 hashes, and migrating them is a real project. The approach that works is the one OWASP describes: layering, where the old hash becomes the input to a new, slow hash, plus a forced reset for users who do not come back. It is not elegant, and it is better than leaving the fast hashes in place, because the fast hash is the thing the attacker actually exploits.

The one sentence

The whole field of password hashing reduces to a sentence: make the attacker pay more for every guess than the password is worth. The slow function sets the price. The salt makes the price apply per user. The cost parameter makes the price go up over time. Everything else is detail, and the details are why you use the library instead of the textbook.

The databases that leak are the ones where the hash was fast. The databases that leak and survive the headline are the ones where the hash was slow, and the attacker got a pile of salted Argon2 hashes and a calculator that told them to go home. Be in the second group. Use Argon2id, let the library handle the salt, tune the cost on your hardware, and raise it every couple of years. That is the whole job, and it is the only part of the system you control after the database is gone.