Sixty Characters With Four Things Inside
Here's a bcrypt hash:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
It looks like one opaque blob. It isn't. It's a structured record with four distinct fields, separated by $ characters, and you can read every one of them by eye once you know the layout.
More usefully, understanding the format explains one of the most common points of confusion in password storage: where does the salt go? Developers regularly build an extra database column for it, or worse, try to keep it secret. Neither is necessary, and the format tells you why.
Field by Field
Splitting on $:
$ 2b $ 12 $ R9h/cIPz0gi.URNNX3kh2O PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
↑ ↑ ↑ ↑
1 2 3 4
1. Version identifier — 2b
This tells the verifier which bcrypt variant produced the hash. Several exist because bugs were found and fixed:
$2$— the original, now obsolete.$2a$— the widely deployed version. A sign-extension bug was found in some C implementations affecting passwords with 8-bit characters.$2x$and$2y$— introduced by PHP to disambiguate hashes produced by the buggy implementation from correct ones.$2y$marks a corrected hash.$2b$— the current standard, from OpenBSD, fixing a length-handling issue with very long passwords.
For new code, $2b$ is what you want. Most modern libraries emit it by default. PHP's password_hash() with PASSWORD_BCRYPT still emits $2y$, which is functionally equivalent for correctly-implemented input.
The reason this prefix exists at all is version negotiation. A verifier reading an old $2a$ hash knows to apply the old algorithm, so existing users can still log in after you upgrade your library.
2. Cost factor — 12
The work factor, as a base-2 logarithm. Cost 12 means 2¹² = 4,096 iterations of the key setup routine.
Because it's logarithmic, each increment doubles the work. Cost 13 takes twice as long as 12. Cost 10 takes a quarter as long.
Storing it in the hash is what makes gradual upgrades possible. You can raise your application's cost setting today, and existing hashes at the old cost still verify correctly — the verifier reads the cost from the stored string rather than from your config. You then re-hash on next successful login to migrate users forward.
3. Salt — the next 22 characters
R9h/cIPz0gi.URNNX3kh2O is a 128-bit random salt, encoded in bcrypt's own Base64 variant. Note that variant: bcrypt uses a non-standard alphabet beginning with . and / rather than the usual A. Feeding a bcrypt salt into a standard Base64 decoder produces garbage.
Twenty-two characters of 6-bit encoding gives 132 bits, of which 128 are used.
4. Digest — the remaining 31 characters
The actual hash output, 184 bits encoded in the same Base64 variant. Everything before it is metadata; this is the part that depends on the password.
Why the Salt Is Sitting Right There in Plain Text
This is the question that trips people up. If an attacker steals your database, they get the salt too. Doesn't that defeat the purpose?
No — because the salt was never meant to be secret. Its job is to make precomputation useless.
Without salts, an attacker computes a rainbow table once and cracks every database that uses the same algorithm. With a unique random salt per password, that table is worthless. Each password needs its own attack, because the salt changes the input to the hash function.
The salt also ensures two users with the same password get different hashes. Without it, an attacker who cracks one account instantly compromises everyone who chose the same password — and can see at a glance which accounts share passwords, which is itself useful intelligence.
None of that requires secrecy. It requires uniqueness and unpredictability. A salt is a cache-invalidation mechanism against precomputation, not a key.
Storing it inside the hash string is simply pragmatic: verification needs the salt, so keeping it adjacent to the digest means one column and no risk of the two getting out of sync.
How Verification Works
The flow is neat once the format makes sense:
- User submits a password.
- Application fetches the stored hash string for that account.
- Verifier parses out the version, cost and salt.
- It re-runs bcrypt on the submitted password using those exact parameters.
- It compares the resulting digest to the stored digest.
This is why bcrypt libraries expose a verify() or compare() function rather than asking you to hash and compare yourself. The parameters have to come from the stored hash, not from your current config.
Use constant-time comparison
The final comparison must not short-circuit on the first differing byte. A naive string equality check returns faster for a near-miss than for a complete mismatch, and that timing difference is measurable across enough requests.
Every serious bcrypt library handles this internally. The mistake appears when someone writes their own comparison:
// Wrong — timing-vulnerable
if (computedHash === storedHash) { ... }
// Right — use the library's verify function
if (bcrypt.compareSync(password, storedHash)) { ... }
What a Pepper Adds
A pepper is a secret value mixed into the password before hashing, stored outside the database — in an environment variable, a config file, or ideally a hardware security module or key management service.
The threat model it addresses is narrow but real: database-only compromise. An attacker with a SQL injection vulnerability or a stolen backup gets your hashes and salts, but not your application secrets. Without the pepper, every hash is uncrackable regardless of how weak the underlying passwords were.
Implementation notes:
- Apply it as an HMAC over the password before bcrypt, not by naive concatenation. HMAC handles arbitrary-length input cleanly and avoids interaction with bcrypt's 72-byte limit.
- Rotating a pepper is genuinely painful — you can't re-derive existing hashes without the original passwords. Plan for versioned peppers if you might ever need to rotate.
- If your pepper lives in the same repo or the same server as your database credentials, it's providing much less separation than you think.
Peppers are a defence-in-depth measure. They are not a substitute for a proper cost factor.
Trying It Out
The Bcrypt Generator makes the structure easy to explore:
- Enter a test string.
- Choose a cost factor.
- Generate the hash.
- Generate it again with the same input.
Note that the second hash is completely different — that's the random salt at work. Then bump the cost by one and observe how much longer generation takes. The doubling is very noticeable above about cost 12.
Never generate hashes for real production passwords in any browser tool, including this one. Use it for learning, testing, and understanding the format.
Common Mistakes
Adding a separate salt column. Unnecessary. The salt is already in the hash string, and splitting them creates a synchronisation risk for no benefit.
Truncating the hash column. A bcrypt hash is 60 characters. A VARCHAR(50) column silently cuts off the digest and every login fails in a way that's maddening to debug. Use CHAR(60) or wider.
Hard-coding the cost during verification. Read it from the stored hash. Hard-coding it breaks every account created under a different cost.
Reusing a salt across users. Some implementations offer a "generate salt once" pattern that gets misused this way. Every hash needs its own salt, which the library generates automatically when you call the hash function correctly.
Pre-hashing with an unbounded function. Running SHA-256 first to work around bcrypt's 72-byte limit is a known pattern, but if you output raw binary it may contain a null byte, which truncates the input in some implementations. Base64-encode the intermediate hash if you do this.
FAQ
Why is a bcrypt hash always 60 characters? Fixed structure: 4 characters of version and delimiters, 2 for cost, 1 delimiter, 22 for salt, 31 for digest.
Can I decode a bcrypt hash back to the password? No. It's a one-way function. The only attack is guessing candidate passwords and hashing each one, which the cost factor is designed to make expensive.
What's the difference between $2a$ and $2b$?
Bug fixes in handling of high-bit characters and very long inputs. $2b$ is current; $2a$ hashes still verify correctly for compatibility.
Should I use a pepper? It's a reasonable addition if you have real secret-management infrastructure to keep it separate from the database. It's not worth much if the secret sits next to your DB credentials.
Is bcrypt still recommended? It remains acceptable for password storage. Argon2id is generally preferred for new systems because of its memory-hardness, which resists GPU and ASIC attacks better. bcrypt with a well-chosen cost factor is far better than any general-purpose hash.
The Takeaway
A bcrypt hash is a self-describing record — version, cost, salt, digest — and that self-description is what makes it upgradeable in place. Reading it fluently makes the design decisions behind password storage much clearer, starting with why the salt was never supposed to be a secret.
Generate and inspect bcrypt hashes free with the Bcrypt Generator at sadiqbd.com — no sign-up, instant results.