Security
How password hashing works: MD5, SHA-2, PBKDF2 and bcrypt
A stolen password table is only as useful to an attacker as the hash function lets it be. This article explains why fast hashes such as MD5 and SHA-256 are the wrong choice for passwords, what a salt does and does not prevent, how a work factor turns each guess into real cost, how PBKDF2 and bcrypt are put together, and what the OWASP Password Storage Cheat Sheet currently recommends. It is written for developers choosing or auditing a password scheme, and for anyone who has wondered what a string like $2a$10$ actually contains.
By the MaffeiTools team · Updated · 9 min read
Hashing, encryption and encoding answer different questions
Encoding, such as base64 or hex, changes representation and involves no secret: anyone can reverse it. Encryption is reversible by design, but only for someone holding the key. A hash function is neither. It maps input of any length to a fixed-size digest and is meant to be impractical to run backwards.
A login system never needs to recover the original password. It only needs to answer one question: does what the user just typed match what they chose earlier? Storing a hash lets the server answer that by hashing the new attempt and comparing. Encrypting passwords instead would mean a key exists somewhere that can turn the whole table back into plaintext, and a breach that reaches the database usually reaches that key too. OWASP's cheat sheet says passwords should be hashed with a modern adaptive algorithm rather than encrypted or stored in plaintext.
Encoding is the most common confusion in the wild. A value that looks like random letters and digits may be nothing more than base64 of the password, which the base64 decoder tool turns back into text in one step. If reversing a stored value takes no key and no guessing, it was never protected.
Why a fast hash is the wrong tool for a password table
MD5 and SHA-256 were designed to digest large inputs quickly and to be cheap in hardware. That is a virtue for checksums and signatures and a defect for passwords. An attacker who obtains a table of hashes tries candidate passwords offline, with no rate limit and no server watching, and every candidate costs one hash computation. The faster the function, the more candidates per second.
The problem is speed, not only cryptographic breakage. MD5 has known collision attacks, but collisions are not what breaks a password table; a guess simply gets hashed and compared. SHA-256 has no practical weakness and is still unsuitable, which is why OWASP states that fast algorithms such as SHA-256 are not appropriate for password storage because they let attackers make large numbers of guesses quickly.
The strength checker on this site makes the gap visible through its own assumptions. Its estimator models an offline attack against a slow hash at 10,000 guesses per second and against a fast hash at 10 billion. Those are the library's modeling constants, not benchmarks of any real hardware, but the six-orders-of-magnitude ratio is the point: the same password can be minutes from cracked under one scheme and untouched under the other.
Determinism is the second flaw. The same input always yields the same digest, so identical passwords produce identical stored values and a precomputed list of common passwords can be matched against a whole table at once.
input : correct horse battery staple MD5 : 9cc2ae8a1ba7a93da39b46fc1019c481 SHA-256 : c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a (hashing the same string again returns exactly the same MD5)
What a salt does, and what it leaves untouched
A salt is a random value, unique per stored password, that is mixed into the hash input and stored in the clear next to the result. OWASP lists what it buys: an attacker must crack hashes one at a time rather than computing a candidate once and comparing it against every row, precomputed rainbow tables stop working, and two users with the same password no longer have matching hashes.
A salt does not make a single hash harder to guess. If the attacker targets one specific user, they simply include that user's salt in each attempt and the work per guess is unchanged. Nor is a salt a secret; hiding it adds nothing. Its job is to break economies of scale, not to slow individual guesses. That second job belongs to the work factor.
How long should it be? RFC 8018 says a randomly generated PBKDF2 salt should be at least 8 octets (64 bits). RFC 9106 recommends 16 bytes for Argon2 password hashing, and bcrypt fixes its salt at 16 bytes. Use a cryptographic random source, never a username or a timestamp. Mainstream password-hashing libraries generate and store the salt for you, which is the safest arrangement.
Worth knowing. A separate secret called a pepper is a different mechanism: one value shared by all passwords, kept outside the database in a vault or HSM. OWASP treats it as defense in depth on top of a proper password hash, not as a substitute for one.
Work factor: making every guess cost something
A work factor is a tunable dial that makes the hash deliberately slow. A legitimate login pays the cost once. An attacker pays it for every guess, across every candidate, which is where it hurts. Because the dial is a parameter, it can be raised as hardware improves; that is what "adaptive" means.
PBKDF2 is the plainest example. Standardized in RFC 2898 and now in RFC 8018, which obsoletes it, it runs a keyed hash (HMAC) over the password and salt and feeds the output back through the same function c times. RFC 8018 notes that an iteration count of c adds log2(c) bits of security strength against trial-based attacks. The OWASP cheat sheet gives the current minimums: 600,000 iterations for PBKDF2-HMAC-SHA256, 220,000 for HMAC-SHA512, and 1,400,000 for HMAC-SHA1 in legacy systems only. Doubling the count adds exactly one bit of work, so 600,000 is about 19.2 bits.
OWASP points to PBKDF2 when FIPS-140 compliance is required, because validated implementations exist and NIST permits it. Its weakness is that it needs almost no memory, so attackers can run enormous numbers of copies in parallel on GPUs or custom hardware. That is the gap memory-hard designs were made to close.
RFC 6070 publishes PBKDF2 test vectors for HMAC-SHA1. Running them through the pbkdf2 tool's own derivation code reproduces the published values byte for byte, which is the practical check that an implementation follows the standard.
password = "password", salt = "salt"
c = 1 -> 0c60c80f961f0e71f3a9b524af6012062fe037a6
c = 2 -> ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957
c = 4096 -> 4b007901b765489abead49d926f721d065a429c1
password = "passwordPASSWORDpassword"
salt = "saltSALTsaltSALTsaltSALTsaltSALTsalt"
c = 4096, 200-bit output
-> 3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038Inside a bcrypt string, and the 72-byte limit
bcrypt output is a single self-describing string. The parts, in order: a dollar sign and a version tag such as 2a, 2b or 2y; a dollar sign and a two-digit cost; a dollar sign; 22 characters encoding the 16-byte salt; and 31 characters encoding the digest. That is 60 characters in total. The 31 characters carry 23 bytes, because bcrypt publishes only 23 of the 24 bytes its core produces. Both encodings use bcrypt's own base64 alphabet, which starts with a dot and a slash and is not the alphabet from RFC 4648.
The cost is a power-of-two exponent. Cost 10 runs the expensive key-schedule loop 2^10 = 1,024 times and cost 12 runs it 4,096 times, so each increment doubles the work. Because the cost, salt and version all live inside the string, a verifier needs nothing else, and old hashes at a lower cost stay verifiable while new ones use a higher setting. OWASP advises a cost of at least 10 for legacy bcrypt use.
bcrypt has a hard input limit. It feeds the password plus a terminating NUL byte into its key setup and ignores everything beyond 72 bytes in total. OWASP recommends enforcing a 72-byte maximum password length for bcrypt. Multi-byte characters use up that budget faster than their visual length suggests, and a long passphrase can be silently cut short. Some people pre-hash long passwords before bcrypt; OWASP warns that raw digest bytes can contain a NUL, which truncates the input, so the digest must be base64-encoded, and that pure pre-hashing without a pepper invites password shucking.
password: correct horse battery staple, cost 10 (salt is random, so every run differs)
$2a$10$HG0VVnE8wvquCQyq5qlh8OcbLMukjeJz9l3GLYD2YzPGnoe.pSDnu
$2a$10$0denSfMmxjDpA.e9ZIBzOu9MLIWXfCfdowCLGedmr6Jfqg8oyvw3y
first hash split up:
$2a$ version tag
10$ cost, 2^10 rounds
HG0VVnE8wvquCQyq5qlh8O salt, 22 chars (16 bytes)
cbLMukjeJz9l3GLYD2YzPGnoe.pSDnu digest, 31 chars
total length 60
verify(password, hash 1) -> true
verify(password, hash 2) -> true (different salt, same password)
verify(password + "!", hash 1) -> false
cost 4, password = 72 x "a"
verify("a" x 72 + "-anything after byte 72") -> true
verify("a" x 71) -> falseArgon2id and scrypt add memory cost, and what to choose
A memory-hard function forces each guess to occupy a large block of RAM as well as time. Memory is expensive to replicate at scale, so it shrinks the advantage of GPUs and custom chips. Argon2 won the Password Hashing Competition in 2015 and was published as RFC 9106; its Argon2id variant balances resistance to side-channel and GPU attacks. scrypt, created by Colin Percival, is an older memory-hard design.
OWASP's decision order is clear: use Argon2id; if it is unavailable, use scrypt; use bcrypt only for legacy systems; and use PBKDF2 when FIPS-140 compliance is required. The concrete floors it lists follow.
- Argon2id: at least 19 MiB of memory, 2 iterations, 1 degree of parallelism. Equivalent trade-offs listed: 46 MiB with t=1, 12 MiB with t=3, 9 MiB with t=4, 7 MiB with t=5.
- scrypt: N=2^17 (128 MiB), r=8, p=1, or a lower-memory row such as N=2^16 with p=2.
- bcrypt: cost 10 or higher, passwords capped at 72 bytes.
- PBKDF2: 600,000 iterations with HMAC-SHA256.
Worth knowing. RFC 9106 gives its own first recommendation for Argon2id at 2 GiB of memory with t=1 and p=4, and a second at 64 MiB with t=3 and p=4. These are stronger than OWASP's minimums and assume a server that can spare the RAM for every concurrent login. Pick parameters by measuring on your production hardware, not by copying a table.
What a strength checker can and cannot tell you
Counting character classes and multiplying gives a number that looks like entropy and mostly isn't. Entropy describes how a password was chosen, not how it looks. A password generated at random from 94 symbols has that entropy; "P@ssw0rd!" does not, because everyone who substitutes characters follows the same handful of patterns.
The password strength checker takes the other route. It runs the zxcvbn-ts library in the browser, which matches the input against dictionaries of common passwords and words, keyboard patterns, repeats, sequences and dates, then estimates how many guesses an attacker following those patterns would need. The tool reports a 0 to 4 score and the crack time under the slow-hash assumption. The comparison below is a naive character-class estimate against the library's guess count, both computed from this repo's dependencies.
Two limits matter. The checker only knows the patterns and word lists it was built with; a password made of unusual words or a personal detail it has no entry for can score higher than it deserves. And it cannot tell you whether a password has appeared in a breach. A good policy therefore pairs a pattern-aware check with a breached-password lookup and, above all, unique passwords per site.
password len naive bits guesses (zxcvbn) score slow-hash crack time P@ssw0rd! 9 59.1 889,472 1 1 minute Summer2024! 11 72.3 10,010,000 2 17 minutes aaaaaaaaaaaa 12 56.4 145 0 less than a second correct horse battery staple 28 164.7 2.47e20 (approx.) 4 centuries naive bits = length x log2(size of the character classes used) aaaaaaaaaaaa warning: Repeated characters like "aaa" are easy to guess.
Where the tools fit, and what they do not do
The MD5 and SHA generators show what a fast hash produces and are the right tool for checksums and file integrity, never for storing passwords. The bcrypt generator hashes with a chosen cost and verifies a password against a hash; it emits 2a-format hashes and accepts 2a, 2b and 2y when verifying. The PBKDF2 generator defaults to SHA-256 and 600,000 iterations with a random 16-byte salt. The HMAC generator computes keyed hashes, useful for the post-hashing pepper OWASP describes and for signed tokens, but it is not a password hash on its own.
None of them covers Argon2id or scrypt, so treat them as tools for understanding formats, checking test vectors and debugging an existing scheme, not as a substitute for a maintained password-hashing library in production. All of them compute in the browser, so the values you type are not sent anywhere.