Bcrypt Hash Generator & Verifier
Generate $2b$ hashes with configurable cost, or verify a password against any existing bcrypt hash.
Bcrypt Internals: Why It's Still the Right Answer for Password Hashing in 2024
Walk into any security review of a web application built in the last decade and you'll find the same question on the checklist: "Are passwords hashed with bcrypt, Argon2, or scrypt?" Bcrypt, despite being designed in 1999, keeps earning that spot. Understanding precisely why — and understanding what the cost factor actually controls inside the algorithm — turns bcrypt from a black-box dependency into a tool you can reason about confidently.
The Blowfish Cipher at the Core
Bcrypt is not an HMAC, not a PBKDF, and not built on SHA-anything. It uses the Blowfish symmetric cipher — specifically a modified version of the key schedule called Eksblowfish (Expensive Key Schedule Blowfish). Blowfish is a 64-bit block cipher with a variable-length key and a notably slow key setup phase. Most block ciphers are designed so key setup is cheap because you set the key once and encrypt megabytes of data with it. Blowfish inverts that assumption: key setup requires 521 applications of the cipher itself, expanding a short key into eighteen 32-bit P-array subkeys and four 256-entry S-boxes — a total of 4,168 bytes of derived state.
The original Blowfish slow key setup was an accident of elegance. Niels Provos and David Mazieres turned it into a feature. Their 1999 USENIX Security paper introduced bcrypt, which wraps Blowfish's key schedule in a loop repeated 2^cost times. The work factor isn't additive — it's exponential. Every increment of the cost parameter doubles the computation required. Cost 10 means 1,024 complete Blowfish key schedules. Cost 12 means 4,096. Cost 14 means 16,384. This is exactly the property you want in a password hash: you can keep it slow even as CPUs get faster, simply by bumping the cost value stored in the hash itself.
The $2b$ Hash Format, Parsed
A bcrypt hash string like $2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW encodes everything needed to verify a future password — no separate salt table required. Breaking it apart:
- $2b$ — algorithm identifier. The "2b" variant (also found as "2a" in older PHP hashes and "2y" in some PHP implementations) specifies that the password is appended with a null byte before hashing, preventing truncation attacks on certain high-byte characters.
- 12 — the cost factor. Stored as a two-digit decimal, so valid values run from 04 to 31.
- R9h/cIPz0gi.URNNX3kh2O — the 22-character bcrypt-base64 encoded salt (128 bits).
- PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW — the 31-character bcrypt-base64 encoded hash output.
The base64 alphabet used by bcrypt is not standard RFC 4648 base64. It uses the custom alphabet ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789, which is one of the subtleties that catches developers who try to verify bcrypt hashes by re-implementing just the base64 layer.
The complete 60-character string is self-describing. To verify a password, you extract the cost and salt from the stored hash, run bcrypt with those parameters, and compare the output in constant time. No additional columns in your database, no separate salt storage, no key rotation headaches on the hash format itself — the format is append-only: future parameters just get a new algorithm version string.
The Key Schedule Step by Step
Here is the precise EksBlowfish sequence that runs during every bcrypt hash operation:
- Initialize state from the fixed constants derived from the hexadecimal digits of pi — the same constants as the original Blowfish cipher. This gives you a known-good starting P-array and four S-boxes.
- XOR the P-array with the password bytes, cycling through the password if it's shorter than the 72-byte limit (bcrypt truncates passwords to 72 bytes — an important limitation).
- Run the initial Eksblowfish round: expand the state using the salt, alternating between password and salt as the key material through 2^cost rounds of the standard Blowfish key schedule.
- Encrypt the magic string "OrpheanBeholderScryDoubt" (6 × 32-bit words, 64 Blowfish encryptions of each pair) using the final Blowfish state.
- Encode the output in bcrypt-base64 and concatenate with the version, cost, and salt.
The critical property is that each step depends on the output of the previous one. There is no shortcut to the final ciphertext that doesn't involve doing all 2^cost key schedules in order. This sequential dependency is exactly what prevents GPU and ASIC attacks from achieving the same parallelism they achieve against MD5 or SHA-256 password hashes.
Why GPUs Don't Like Bcrypt
SHA-256 cracking on a modern GPU can attempt billions of hashes per second because each hash candidate is independent and requires only integer arithmetic that maps efficiently onto GPU shader units. Bcrypt's Blowfish state is 4,168 bytes — far larger than a GPU thread's register file. Each candidate password requires loading and mutating that state through thousands of iterations, causing constant cache misses and memory bandwidth pressure. The result is that bcrypt on a GPU achieves only a small multiple of single-CPU throughput rather than the thousands-of-times speedup you'd see with raw SHA-256.
Argon2 (the 2015 Password Hashing Competition winner) goes further by also requiring large amounts of RAM, making it resistant to both GPU and ASIC attacks simultaneously. For new systems, Argon2id is the current recommendation. But bcrypt remains perfectly acceptable for existing deployments — especially if you use cost 12 or higher.
The 72-Byte Password Truncation Problem
Bcrypt's password input is limited to 72 bytes (not characters — bytes in the UTF-8 encoding). Passwords longer than 72 bytes are silently truncated. This means password1234567890... (73 bytes) and password1234567890... (150 bytes) produce the same hash if the first 72 bytes are identical.
The standard mitigation is to pre-hash the password with a fast hash (typically SHA-256 or SHA-384) before passing it to bcrypt. This converts arbitrarily long passwords into a fixed-size byte string, preventing both the truncation vulnerability and potential denial-of-service attacks where an attacker submits an enormous string hoping bcrypt will still try to process it in full. If you use pre-hashing, be careful not to hex-encode the SHA output — pass the raw bytes or base64-encode them to keep within the 72-byte limit while preserving full entropy. This technique is sometimes called "pepper + prehash" when a server-side secret (the pepper) is also mixed in.
Choosing the Right Cost Factor in Production
The OWASP Authentication Cheat Sheet recommends a cost factor that brings hash generation to at least 100ms on the hardware doing the hashing. As a practical starting point, cost 10 is appropriate for consumer web applications on typical cloud VMs and has been the default in most bcrypt libraries since around 2010. Cost 12 is now reasonable on modern hardware and provides significantly more brute-force resistance. Cost 14 is appropriate for high-value targets like financial services or credential vaults where you can accept slightly longer login latency.
The stored hash includes the cost factor, which means you can perform online cost upgrades without invalidating existing hashes: on each successful login, check if the stored hash uses the old cost, and if so, rehash with the new cost and update the database. Users never notice — the only difference is a slightly longer first login after the upgrade. This makes bcrypt cost migration low-risk in production systems.
Constant-Time Comparison is Not Optional
One detail that bcrypt libraries get right but naive implementations sometimes miss: the comparison of the computed hash against the stored hash must run in constant time, independent of where the first differing byte is. A timing-variable comparison leaks information about how many leading characters of the hash match, which is usable in side-channel attacks against low-latency networks. The correct approach is XOR all bytes together and check if the final accumulated difference is zero. All serious bcrypt libraries implement this — and this tool does too.