Skip to content

· 3 min read · 606 words

What $2a, $2b, $2x and $2y Mean in a Bcrypt Hash

The version prefix in a bcrypt hash records which implementation quirk it was made under. Here is what each one means and when the difference matters.

  • bcrypt
  • compatibility
  • php

A bcrypt hash starts with a version marker. You will see $2a$, $2b$ and $2y$ in the wild, and $2x$ if you are unlucky. They are not different algorithms.

$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
 ^^  ^^ ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   |          salt                      digest
  |   cost
  version

Where the letters came from

$2 and $2a

The original was $2$. It handled non-ASCII passwords badly, so it was replaced by $2a$ in 1997, which fixed the encoding and became the version everything used for the next fourteen years.

$2x and $2y

In 2011 a bug turned up in the PHP implementation, not in bcrypt itself. It mishandled passwords containing bytes above 127, which meant PHP was producing hashes that were weaker than they looked and that did not match what other implementations produced for the same input.

PHP fixed it and introduced two markers at once. $2x$ tags hashes made by the broken code so they can still be verified, and $2y$ tags hashes made by the fixed code. The letters exist to tell those two apart, and $2y$ is what PHP has written ever since.

$2b

OpenBSD found a separate bug in 2014, this time in their own code. It used an 8 bit variable for the password length, so a password of 256 bytes or more wrapped around to a very short one. Almost nobody was affected, since bcrypt only reads 72 bytes anyway and getting there requires a deliberately strange input, but they bumped the version to $2b$ to mark the corrected behaviour.

What this means in practice

For any password under 72 bytes, $2a$, $2b$ and $2y$ produce identical output. The letter is a label on the implementation, and the digest bytes are the same.

Most libraries verify all three regardless of which one they write. PHP password_verify accepts $2a$, $2b$ and $2y$. So does Spring Security. So does bcryptjs.

WritesLibrary
$2y$PHP password_hash, Laravel
$2b$bcryptjs, node bcrypt, Python bcrypt
$2a$jBCrypt, Spring Security, golang.org/x/crypto/bcrypt

When it does matter

A few older or stricter implementations check the prefix before they verify and reject anything they do not recognise. The usual symptom is a hash generated by a Node script that PHP refuses, or the reverse, with no error beyond the login failing.

Laravel: this password does not use the bcrypt algorithm

The common case, and the one that produces an actual error message rather than a silent failure. PHP password_verify accepts $2a$, $2b$ and $2y$, but password_get_info only reports bcrypt for $2y$, and Laravel checks that before it compares anything.

// Illuminate\Hashing\BcryptHasher
if ($this->verifyAlgorithm && ! $this->isUsingCorrectAlgorithm($hashedValue)) {
    throw new RuntimeException('This password does not use the Bcrypt algorithm.');
}

// and that check is simply
password_get_info($hashedValue)['algoName'] === 'bcrypt'

So a $2b$ hash pasted into a seeder or a users row throws on the first login attempt, even though the digest is correct and password_verify on its own would have returned true. Relabelling it to $2y$ fixes it. Turning off hashing.verify in config/hashing.php also silences it, but the relabel is the better answer, since the check exists to catch hashes that really are from another algorithm.

If you hit that, changing the three characters at the front is enough. The rest of the string does not change.

// A $2b$ hash that a strict PHP setup will accept.
const forPhp = hash.replace(/^\$2[abxy]\$/, '$2y$');

This is a relabel, not a conversion. It works because the bytes are already identical. Do not use the same trick to move a hash between two functions that are genuinely different.

The generator on this site lets you pick the prefix directly, and the framework pages default to whatever the stack in question expects.

Read next