When you sign up for an online service, your password should be stored in a form that not even the service operator can read. Not encrypted, because whatever is encrypted can be decrypted with the key, but transformed with a password hashing function. The difference between doing this well and doing it badly emerges on the day the database is stolen: in one case the passwords remain protected, in the other they are recovered within hours.
Why any old hash will not do
A hash function is one-way: you cannot go back from the fingerprint to the original. But an attacker does not need to "invert" the function. They only need to guess: take a candidate password, compute its hash and compare it with the stolen ones. If they match, the password has been found.
General-purpose hashes such as SHA-256 are designed to be extremely fast. Modern graphics cards compute billions of them per second. With a dictionary of common passwords, variants and combinations, most real-world passwords fall quickly.
Salt: every password becomes unique
The first safeguard is the salt: a random value, different for each user, combined with the password before hashing and stored alongside the hash.
Salt solves two problems:
- two users with the same password get different hashes, so you cannot tell who shares the same password;
- precomputed tables, such as so-called rainbow tables, become useless, because they would have to be rebuilt from scratch for every single salt.
The salt is not secret: its only job is to make each computation unique. It does not, however, slow down an attack targeting a single password.
Deliberate slowness: the cost factor
The second safeguard is to make the computation deliberately slow and expensive. If a legitimate user has to wait a tenth of a second when logging in, they will not notice. An attacker who has to try billions of combinations, on the other hand, sees their workload multiplied enormously.
Password functions have an adjustable cost parameter, to be increased over time as hardware gets faster.
The main functions
PBKDF2
It repeats a hash, typically HMAC-SHA256, a large number of times. It is standardised and widespread, but has a limitation: it needs very little memory, so it parallelises well on graphics cards and dedicated hardware. It must be used with a very high number of iterations.
bcrypt
Designed at the end of the 1990s, it was for a long time the default choice in many frameworks. Its cost factor is exponential: each increment of one doubles the computation time. It has one quirk worth knowing: it only considers the first 72 bytes of the password, and anything after that is ignored.
scrypt
It introduces an important idea: being memory-hard as well as time-consuming. Computing a hash requires a significant amount of RAM. This makes it much harder and more expensive to build specialised hardware that runs millions of attempts in parallel.
Argon2
In 2015 it won the Password Hashing Competition, a public selection designed specifically to identify a new standard. It allows time, memory and degree of parallelism to be tuned separately. There are three variants; the one generally recommended for passwords is Argon2id, which combines resistance to dedicated-hardware attacks with resistance to certain attacks based on observing memory access patterns.
A quick comparison
| Function | Memory cost | Notes |
|---|---|---|
| PBKDF2 | Low | Widespread standard, needs many iterations |
| bcrypt | Low and fixed | Battle-tested, 72-byte limit |
| scrypt | High | Good resistance to dedicated hardware |
| Argon2id | Configurable | Recommended choice for new systems |
Pepper: one more secret
Some systems add a pepper: a secret value, the same for everyone, which is not stored in the database but somewhere separate, such as a hardware module or the server configuration. If only the database is stolen, the attacker cannot even begin to try passwords. Pepper is a reinforcement, not a substitute for salt and slowness.
From password to key
The same functions are also used to derive an encryption key from a password, for example when you protect an archive, a password manager or a certificate's private key. The principle is identical: make every guessing attempt expensive. An archive encrypted with AES-256 but with a key derived quickly from a weak password is only as protected as that password.
What really matters for users
Even the best hashing function cannot save a password like "123456" or your pet's name followed by your year of birth: these are among the first attempts in any attack. A long, randomly generated password stored in a password manager, combined with multi-factor authentication, makes the theft of a single service's database almost irrelevant.
In short
- Passwords are stored with dedicated hashing functions, not with encryption or fast hashes.
- Salt makes every hash unique and neutralises precomputed tables.
- The cost factor makes every attempt slow and should be increased over time.
- Argon2id is the recommended choice; scrypt, bcrypt and PBKDF2 remain acceptable if well configured.
- The same functions are used to derive encryption keys from a password.