Skip to content
pwnsy
cryptographyintermediate#password-hashing#argon2#bcrypt#cryptography#authentication

Password Hashing Explained: bcrypt, scrypt and Argon2

Why password hashing has to be slow, how work factors and memory-hardness stop cracking, and how to choose Argon2id for new systems.

When a service is breached, the passwords rarely leak in the way people imagine. The attacker gets a table of hashes, not plaintext. What happens next depends entirely on how those hashes were made. Choose the wrong algorithm and the whole table falls in hours. Choose the right one and most of it stays locked for years, or forever. Password hashing is the difference between a bad day and a catastrophe.

The counterintuitive part is the goal. For almost everything else in computing, faster is better. For password hashing, slow is the point. A password hash is a function you run once per login and an attacker runs billions of times. Making it slow costs you a few milliseconds and costs the attacker their entire economic model.

Why a normal hash is the wrong tool

SHA-256 and its relatives are cryptographic hashes built for speed. They verify file integrity and back digital signatures, jobs where you want to process gigabytes quickly. That speed is a liability the moment you point it at passwords.

Modern hardware can compute billions of SHA-256 hashes per second on a single GPU. Feed it a stolen table and a dictionary of common passwords, and it works through them almost instantly. The fast hash did its job perfectly. That job just happened to be helping the attacker.

Two properties fix this. First, a salt. Second, deliberate slowness with a tunable cost. Password hashing functions are designed around both.

Salts: the non-negotiable baseline

A salt is a random value generated per user and stored alongside the hash. Before hashing, the salt is combined with the password, so two people with the same password get completely different hashes.

This defeats precomputation. Without salts, an attacker can build a rainbow table once and reuse it against every database on earth. With a unique salt per user, that shortcut disappears, and each password has to be attacked on its own. Salts are not secret and they do not need to be. Their job is uniqueness, so store one per user in the same row as the hash. For the full picture see Salting and Peppering Explained.

Salts stop precomputation. They do nothing about raw guessing speed. That is what the work factor is for.

Work factors and memory-hardness

A work factor is a knob that controls how much computation each hash takes. Turn it up and every login gets slightly slower for you and dramatically more expensive for an attacker running a brute-force campaign. Because it is a parameter rather than a fixed property, you raise it every few years as hardware improves, without migrating to a new algorithm.

Memory-hardness is the second lever, and it is what modern functions add on top. A memory-hard function forces each guess to allocate and touch a large block of RAM. This matters because attackers do not crack on ordinary CPUs. They use GPUs, FPGAs and custom ASICs that pack thousands of tiny compute units side by side. Those units are cheap on arithmetic and starved for memory. Demand a megabyte of RAM per guess and the attacker's massive parallelism collapses, because they cannot give every unit enough memory. For the general mechanics of hashing, see How Hashing Works.

The three functions in practice

Three algorithms dominate real deployments. They arrived in that order, and each addressed a weakness in the one before.

FunctionYearTunable costMemory-hardNotes
bcrypt1999Cost factor (rounds)No (fixed, small)Battle-tested, widely available, caps long passwords
scrypt2009CPU and memory paramsYesFirst mainstream memory-hard design
Argon22015Time, memory, parallelismYesWinner of the Password Hashing Competition

bcrypt is based on the Blowfish cipher and has a single cost factor. Raise the cost by one and the work roughly doubles. It has held up remarkably well and remains a reasonable default where a strong Argon2 library is not on hand. Its main limitation is that it uses a small fixed amount of memory, so it does not blunt GPU cracking the way the later designs do. It also truncates input beyond 72 bytes, which matters if you allow long passphrases.

scrypt was the first widely used memory-hard function. You tune CPU cost and a memory parameter separately, which lets you force each guess through a large memory region. It is solid and still recommended where Argon2 is unavailable.

Argon2 won the Password Hashing Competition in 2015 and is specified in RFC 9106. It has three variants:

  • Argon2d maximises resistance to GPU cracking but touches memory in a password-dependent pattern, which can leak through side channels.
  • Argon2i uses a password-independent memory pattern that resists side-channel attacks but gives up some cracking resistance.
  • Argon2id runs Argon2i for the first pass and Argon2d after, so it gets both properties. This is the variant to use.
Default to Argon2id

For any new system, reach for Argon2id first. It resists both the parallel-hardware cracking that GPUs enable and the side-channel leakage that pure Argon2d risks. bcrypt is a defensible fallback only when a vetted Argon2 implementation is genuinely unavailable on your platform.

Why encryption is the wrong instinct

A recurring mistake is to encrypt passwords instead of hashing them. It feels safer because encryption is reversible, and that is exactly the problem. Anything encrypted can be decrypted by whoever holds the key, so an attacker who steals both the database and the key recovers every password in plaintext. Encryption keys leak through backups, configuration files, memory dumps and insider access. Hashing is a one-way function on purpose: there is no key to steal and no way to reverse it, only the slow work of guessing. The whole design goal is that a full database dump still leaves the attacker with a hard problem rather than a lookup.

The same reasoning explains why you verify a password by hashing the submitted value and comparing it to the stored hash, never by decrypting the stored value. The comparison itself should use a constant-time function so that the time it takes to reject a wrong password does not leak how many leading characters were correct. Good libraries handle this for you, which is another reason to lean on a vetted implementation rather than assembling your own.

Choosing parameters

The right settings depend on your hardware and your latency budget, so treat published figures as a starting point and measure on your own servers. The principle is to make a single hash as slow as your login flow can tolerate, commonly in the range of a few hundred milliseconds.

  • Argon2id. Set memory first, then iterations, then parallelism. OWASP guidance suggests a memory cost on the order of tens of megabytes as a floor, with iterations tuned so one hash lands in your latency target. More memory is the strongest lever against cracking hardware, so prefer raising it over raising iterations where you can.
  • bcrypt. Pick a cost factor that keeps a single hash around a couple of hundred milliseconds on current hardware, and revisit it as your servers get faster.
  • scrypt. Tune the memory parameter high enough to matter, then set the CPU cost to hit your latency target.

Never invent your own scheme, never chain fast hashes together hoping the layering helps, and never store the parameters somewhere you cannot change them. Modern libraries encode the algorithm, cost and salt directly into the stored hash string, so a future you can detect an outdated hash on login and transparently upgrade it.

See the downstream risk

Weak password storage turns one breach into credential stuffing across every service a user touches. Our Exploit Intelligence dashboard tracks how quickly authentication and credential-related CVEs move from disclosure to active exploitation, which is a useful reminder of how fast a leaked table gets weaponised.

The attacker's economics

Understanding why slow hashing works means thinking like the person holding the stolen table. Their whole operation is a rate problem: how many candidate passwords can they test per second, multiplied by how long they are willing to run.

Against a fast general-purpose hash, that rate is enormous. A single modern GPU tests billions of candidates per second, and a rig with several GPUs multiplies that. The attacker feeds in a dictionary of common passwords, then variations, then exhaustive short strings, and the weak accounts fall almost immediately because their passwords sit near the top of every guessing list.

A deliberately slow hash attacks the rate directly. Raise the per-guess cost so that a single hash takes a couple of hundred milliseconds on the defender's server, and the attacker's billions-per-second collapses toward single digits per second per core. The dictionary that cleared in seconds now takes years. Memory-hardness attacks the same economics from a second angle: by forcing each guess to occupy a large block of RAM, it denies the attacker the massive parallelism that made GPUs and custom hardware cheap. A chip that can run thousands of tiny arithmetic units in parallel cannot give each of them a megabyte of dedicated memory, so its effective guess rate falls far below its raw compute.

This is why the defender's few milliseconds matter so little and the attacker's cost matters so much. You pay the cost once per legitimate login. The attacker pays it billions of times. Every increment you add to the work factor is multiplied across the attacker's entire campaign, which is the leverage that makes the whole approach work.

Detection and response after a leak

Password hashing is a control you rely on precisely when other controls have already failed, so it pays to think about what happens once a table is suspected stolen.

  • Assume offline cracking begins immediately. Once a hash table leaves your control, the attacker works on it at their own pace with no rate limit you can impose. The clock that matters is how long your algorithm and parameters hold, not how fast you can respond.
  • Weak passwords fall first. Regardless of algorithm, a user whose password is on a common list is at risk quickly, because the attacker tries those candidates first. This is why hashing strength and password strength policy work together, and neither substitutes for the other.
  • Force a reset and invalidate sessions. A confirmed or suspected table leak calls for a password reset across affected accounts and invalidation of existing sessions and tokens, so that a cracked password cannot be quietly reused.
  • Watch for credential stuffing elsewhere. Cracked passwords get replayed against other services, and inbound stuffing against your own login using passwords leaked from a third party is the mirror image. A rise in login attempts using known-breached credentials is a signal to watch.
  • Record the algorithm and parameters per hash. When you can read, from the stored hash string, which algorithm and cost each account used, you can reason precisely about which accounts had weak protection and prioritize their resets.
Hashing buys time, not immunity

A strong hash does not make a stolen table safe. It makes cracking slow and expensive, which buys you the time to detect the leak, force resets, and rotate secrets before most passwords fall. Treat a confirmed table leak as a live incident even when the hashing was strong, because weak individual passwords can still fall fast.

Common mistakes

Using a fast hash with many rounds instead of a real password hash. Chaining SHA-256 thousands of times feels like added cost, but it lacks the memory-hardness that defeats parallel hardware, and homegrown iteration schemes often introduce subtle flaws. Use a function built for the job.

Reusing a single global salt. A salt has to be unique per user to defeat precomputation and to make identical passwords hash differently. One shared salt across the table restores much of the advantage a rainbow table gives the attacker.

Encrypting instead of hashing. Encryption is reversible by whoever holds the key, and keys leak through backups, config files, and memory. Hashing has no key to steal. Reach for a one-way function so a full database dump still leaves a hard problem.

Truncating or transforming the password before hashing. Lowercasing, trimming, or cutting a password to fit reduces the space an attacker must search. Feed the password to the hash as the user typed it, within sane length limits, and let the function do its work.

Setting the work factor once and never revisiting it. Hardware gets faster, so a cost that was strong years ago erodes. Schedule a review, and upgrade hashes transparently on login when a user with outdated parameters authenticates.

Comparing hashes with an ordinary string comparison. A comparison that returns early on the first differing byte can leak timing. Use the constant-time comparison your library provides so rejection time reveals nothing about how close a guess was.

What each property defends against

It helps to line up the defensive properties against the attacks they answer, because each one closes a specific door and leaving any open weakens the whole.

PropertyAttack it bluntsPresent in
Per-user saltPrecomputed and rainbow tables, identical-password correlationAll three functions
Tunable work factorRaw brute-force guessing speedbcrypt, scrypt, Argon2
Memory-hardnessCheap parallel cracking on GPUs, FPGAs, ASICsscrypt, Argon2
Side-channel resistanceTiming leaks from data-dependent memory accessArgon2i and Argon2id
Pepper (external secret)Offline cracking of a database-only dumpAdded on top, stored separately

Read down the table and the logic of choosing Argon2id for a new system becomes plain and hard to argue with. It carries the salt, the tunable cost, memory-hardness, and side-channel resistance in one function, then a pepper stored outside the database adds the final layer that a table leak alone cannot defeat.

How password hashing evolved

The earliest Unix systems stored passwords using a crypt function built on the DES cipher, with a tiny salt and a fixed, by modern standards trivial, cost. It was adequate for the hardware of its day and became grossly insufficient as computers sped up. General-purpose hashes like MD5 and later SHA-1 and SHA-256 were sometimes pressed into password storage, which was a mistake for the same reason: they are fast by design, and speed is what the attacker wants.

bcrypt arrived in 1999 with a crucial idea: a tunable cost factor, so the same algorithm could be made slower as hardware improved without changing to a new scheme. It held up remarkably well and is still a defensible choice. Its limitation is a small fixed memory footprint, which leaves it exposed to the parallel-hardware cracking that later became cheap.

scrypt introduced memory-hardness to mainstream use in 2009, forcing each guess through a large memory region so that GPUs and custom chips lost their parallelism advantage. Then the Password Hashing Competition, concluded in 2015, selected Argon2 as its winner, and its Argon2id variant became the current default recommendation. The direction across these decades is consistent: raise cost, then add memory-hardness, then combine memory-hardness with side-channel resistance, always staying ahead of the falling price of computation.

Frequently asked questions

Is bcrypt still safe to use in 2026? bcrypt with a suitable cost factor remains a defensible choice, especially where a vetted Argon2 library is not available. Its main weaknesses are a small fixed memory footprint, which gives less protection against parallel-hardware cracking than the memory-hard functions, and a 72-byte input limit. For new systems Argon2id is the first recommendation, with bcrypt as an acceptable fallback.

Why can I not simply use SHA-256 with a salt? SHA-256 is built for speed, and speed is exactly what a password cracker wants. Even salted, a fast hash lets an attacker test billions of candidates per second per GPU. Password hashing functions add a tunable work factor, and the modern ones add memory-hardness, both of which exist to make each guess slow and expensive.

Do I still need a salt if I use Argon2? Yes, and modern libraries generate and store one for you inside the hash string. The salt defeats precomputed tables and ensures two users with the same password get different hashes. The work factor and memory cost address guessing speed, which is a separate problem the salt does not solve.

What is the difference between a salt and a pepper? A salt is unique per user, stored alongside the hash, and not secret. Its job is uniqueness. A pepper is a single secret value applied on top of the hash and stored separately from the database, in a secrets manager or hardware module. The pepper is a defense-in-depth layer, so that a database dump alone still lacks a value the attacker needs.

How slow should a password hash be? Make it as slow as your login flow can tolerate, commonly a few hundred milliseconds per hash on your production hardware. Measure on your own servers rather than trusting a published number, and prefer raising Argon2's memory cost over its iteration count, because memory is what defeats parallel cracking hardware.

Which Argon2 variant should I choose? Argon2id. It runs Argon2i for its first pass, which resists side-channel leakage, and Argon2d afterward, which maximizes resistance to GPU cracking. That combination is why current OWASP and NIST-aligned guidance leads with Argon2id for general password storage.

How do I upgrade hashes without asking everyone to reset? Store the algorithm and parameters in the hash string, which modern libraries do by default. When a user logs in and their stored hash uses outdated parameters, verify the password, then rehash it with current settings and store the new value. Over time your active users migrate transparently, and you can force resets only for accounts that never log in.

A practical checklist

  1. Use Argon2id for new systems. Fall back to bcrypt or scrypt only when a trusted Argon2 library is not available.
  2. Generate a unique random salt per user and let the library store it inside the hash string. Do not reuse a global salt.
  3. Tune the work factor to your hardware, targeting a few hundred milliseconds per hash, and schedule a review to raise it over time.
  4. Prefer raising memory cost over iteration count with Argon2, because memory is what defeats parallel cracking hardware.
  5. Add a pepper as defence in depth, a secret key stored outside the database (in a secrets manager or HSM) and applied on top of the hash. See Key Derivation Functions Explained.
  6. Upgrade legacy hashes on login. When a user with an old hash authenticates, rehash their password with current parameters.

Password hashing is one of the few places in security where the correct answer is settled and boring. Pick Argon2id, salt every user, set the cost as high as your login latency allows, and store a pepper somewhere the database dump will not reach. Do that and a stolen table becomes an expensive, slow, mostly fruitless project for whoever ends up holding it. For the attacker's side of this, see Password Cracking Techniques.

Sources & further reading

Sharetwitterlinkedin

Related guides