BCrypt

edit

BCrypt hashes passwords so that they can be stored: checking a password is fast enough for a sign-in, but slow enough to make guessing passwords from a stolen database expensive. A BCrypt hash is a string such as $2b$12$... that contains everything needed to verify a password: the revision (2b), the work factor (12), the salt and the hash.

Hash and verify a password

var hash = Bcrypt.HashPassword("my password");
var ok = Bcrypt.Verify("my password", hash);
Assert.True(ok);
  • HashPassword generates a random salt for each call: hashing the same password twice gives two different strings.
  • Verify reads the revision, the work factor and the salt from the hash, so it needs no other parameter.
  • Both methods also take the password as a ReadOnlySpan<char>.

Choose the work factor

The work factor (the cost) is a power of two: each increment doubles the time it takes to hash and to verify a password. It ranges from Bcrypt.MinWorkFactor (4) to Bcrypt.MaxWorkFactor (31), and the default is Bcrypt.DefaultWorkFactor (11). Choose the highest value your servers can afford for each sign-in, and raise it as hardware gets faster.

var hash = Bcrypt.HashPassword(
    password: "my password",
    workFactor: 12,
    version: BcryptVersion.Revision2B);

var shouldRehash = Bcrypt.NeedsRehash(hash, workFactor: 13, version: BcryptVersion.Revision2B);
Assert.True(shouldRehash);

NeedsRehash returns true when a hash was created with another work factor or another revision than the ones you pass.

Upgrade the stored hashes

A hash cannot be converted to a new work factor: the password is needed. The only moment the application knows the password is when the user signs in, so upgrade the hash then:

// The hash stored for the user, created with a work factor of 10
var storedHash = Bcrypt.HashPassword("my password", workFactor: 10);

if (Bcrypt.Verify("my password", storedHash))
{
    // The password is correct: this is the only moment the application knows it,
    // so replace a hash created with another revision or work factor now
    if (Bcrypt.NeedsRehash(storedHash, workFactor: 12))
    {
        storedHash = Bcrypt.HashPassword("my password", workFactor: 12);
    }
}

Assert.Equal(12, Bcrypt.ParseHash(storedHash).WorkFactor);

To find the hashes that use an old revision or a low work factor, read them with ParseHash, or TryParseHash for values that may not be BCrypt hashes:

var hash = Bcrypt.HashPassword("my password", workFactor: 10, version: BcryptVersion.Revision2A);

// Read the revision and the work factor of a stored hash
var info = Bcrypt.ParseHash(hash);
Assert.Equal(BcryptVersion.Revision2A, info.Version);
Assert.Equal(10, info.WorkFactor);

// TryParseHash returns false for a value that is not a BCrypt hash
Assert.False(Bcrypt.TryParseHash("not a hash", out _));

ParseHash throws a FormatException when the value is not a BCrypt hash.

Revisions

BcryptVersionPrefixSupport
Revision2$2$Verified only: salts cannot be generated
Revision2A$2a$Hashed and verified
Revision2B$2b$Hashed and verified; the default
Revision2Y$2y$Hashed and verified; identical to $2b$
Revision2X$2x$Read by ParseHash only

$2x$ exists only to reproduce a sign-extension bug of an old implementation, which this library does not emulate. Hashing or verifying a $2x$ hash throws a NotSupportedException rather than silently mis-verifying the passwords that contain non-ASCII characters. Use ParseHash to find those hashes, and ask the users to reset their password.

Generate the salt yourself

HashPassword(password, salt) hashes with a salt you provide, which makes the result reproducible. Generate the salt with GenerateSalt: it contains the revision and the work factor.

// The salt includes the revision and the work factor
var salt = Bcrypt.GenerateSalt(workFactor: 10);
Assert.StartsWith("$2b$10$", salt);

// The same password and salt always give the same hash
var hash = Bcrypt.HashPassword("my password", salt);
Assert.Equal(hash, Bcrypt.HashPassword("my password", salt));
Assert.True(Bcrypt.Verify("my password", hash));

HashPassword throws a FormatException when the salt is not valid.

Password length

BCrypt processes the password as UTF-8 and only uses its first 72 bytes (Bcrypt.MaxPasswordLengthInBytes). Two passwords that share their first 72 bytes verify against the same hash. A character outside ASCII takes 2 to 4 bytes, so the limit can be reached with fewer than 72 characters.

A password that contains an unpaired surrogate cannot be encoded as UTF-8: HashPassword throws an ArgumentException, and Verify returns false.