Updated on
BCrypt.Net-Next is a .NET port of BCrypt, a password-hashing function that generates its own random salt and lets us raise its cost as hardware gets faster. The common case needs one NuGet package and two static methods.
BCrypt.Net.BCrypt.HashPassword() turns a password into a 60-character string that already carries the salt and the cost setting inside it, and BCrypt.Net.BCrypt.Verify() checks a password against that string. Most of this article builds on those two calls.
Let’s start.
Why Should We Never Store Passwords in Plain Text?
Databases get copied: backups leave the building and support tools dump whole tables. Any password stored as plain text then works as a credential everywhere else its owner used it.
So we hash it. We store the output of a one-way function, and at login we run the same function over what the user typed and compare the two results. Nothing on our side can turn a stored value back into a password.
We also salt it. Without a salt, two people who pick the same password get identical hashes, which makes precomputed tables and side-by-side comparison cheap. A random salt per password breaks both.
A general-purpose hash is still the wrong tool here, because it is built to be fast, and fast is what an attacker wants. Password hashing functions such as BCrypt are built to be slow on purpose, and to get slower on demand.
For the wider picture, see our article on hashing and salting passwords in C#.
What Is BCrypt?
BCrypt is a password-hashing function built on the Blowfish cipher’s key schedule. Niels Provos and David Mazières presented it in 1999 for OpenBSD, where it is still the password hash the system uses by default.
Two properties keep it in use. First, it generates a random salt for every password and stores that salt inside the resulting hash, so we never manage salts ourselves or need a second database column for them.
The second is the work factor. BCrypt runs its key schedule 2^workFactor times, so raising the number by one doubles the time a single hash takes. Hardware gets faster every year, the work factor goes up to match, and old hashes keep working because each one carries the factor it was made with.
BCrypt.Net-Next is a .NET implementation, an MIT-licensed package, and the same algorithm has implementations in nearly every other major language.
If we already use ASP.NET Core Identity, its built-in hasher does this job without an extra package (we explain how the default ASP.NET Core Identity password hasher works separately), and Argon2 packages exist for .NET too. BCrypt is still a sound choice, and it is the one this article uses.
BCrypt Variants
Over the years, BCrypt implementations were updated several times, mainly to accommodate bug fixes. So, if we check the algorithm version in the generated hashes, we could find several different identifiers depending on the language, library, or version we use: $2$, $2a$, $2x$, $2y$, $2b$.
BCrypt.Net-Next generates $2a$ hashes, and it verifies $2a$, $2b$, $2y$ and, for ASCII-only passwords, $2x$. So a hash produced by PHP’s password_hash() or by a Java or Python library verifies here without any conversion. A version character outside those four throws a SaltParseException, and GenerateSalt() accepts the same four if we ever need to write one of the others deliberately.
How Do We Hash and Verify Passwords With BCrypt.Net-Next?
Two calls cover it. BCrypt.Net.BCrypt.HashPassword() takes the plain-text password and returns a 60-character hash, and BCrypt.Net.BCrypt.Verify() takes a password and a stored hash and returns a bool.
Both are static methods on the BCrypt class, which lives in the BCrypt.Net namespace, so the class and the namespace share a name. With using BCrypt.Net; placed inside our own namespace, as the sample’s test file does, BCrypt resolves to the class, which is why the snippets below call BCrypt.HashPassword(). The same using at the top of the file leaves BCrypt meaning the namespace.
We never generate a salt, store a salt, or pass a salt to Verify(). HashPassword() draws one from a cryptographic random number generator and writes it into the returned string, and Verify() reads it back out of the stored hash to repeat the calculation.
There is nothing to configure and no object to construct. A password column of 60 characters and these two calls are a complete implementation.
The salt comes from .NET’s RandomNumberGenerator, which has its own article, RandomNumberGenerator in C#: Secure Random Numbers.
To use the BCrypt hashing function for .NET we must include the BCrypt.Net-Next package in our project:
dotnet add package BCrypt.Net-Next
Once we add the package, we can generate a hash from a clear-text password using the HashPassword() static method in the BCrypt class:
var passwordHash = BCrypt.HashPassword("Password123!");
Moreover, verifying a hash is equally simple using the Verify() static method in the same class:
var result = BCrypt.Verify("Password123!", passwordHash);
Assert.True(result);
The snippets here come from the sample’s test project, which is why some of them end in an assertion.
Increasing Computational Cost
We can create hashes more resilient to brute force by making the hash calculation purposely slow so the many attempts attackers must perform to break a hash take as long as possible.
As computers become faster, a password-hashing function that used to be slow a few years ago, today may take very little time to compute.
Provos and Mazières wrote in the abstract of A Future-Adaptable Password Scheme, the 1999 paper that introduced BCrypt, that “hardware improvements constantly give attackers increasing computational power.”
BCrypt can adapt to the increase of available computational power by iterating over the input data several times. The more iterations the slower the calculation will be. We can control this with the workFactor parameter of the HashPassword() method:
var passwordHash = BCrypt.HashPassword("Password123!", workFactor: 13);
Here, we instruct BCrypt to apply the hashing function 2^13 times to the input string for a total of 8,192 iterations. In case we do not specify a work factor BCrypt uses a default value of 11.
The work factor has to be between 4 and 31, and HashPassword() throws an ArgumentOutOfRangeException outside that range.
Analyzing a BCrypt Hash
Whenever we use the HashPassword() method or one of its variants without passing a salt, BCrypt will generate a random salt and include it in the returned hash along with other data needed for the verification process.
This is what a BCrypt hash looks like. Essentially, it is a set of three values separated by a $ character:
$2a$13$gdtRuVYzDLFBUGnN1WxK/.1OFFoD7CbDZjRYGknrOwT9rus5AsqTu
2a stands for the BCrypt algorithm version used to generate the hash.
13 is the work factor.
gdtRuVYzDLFBUGnN1WxK/.1OFFoD7CbDZjRYGknrOwT9rus5AsqTu contains both the salt and the hashed input password concatenated and Base64 encoded. Both values have fixed lengths so they are easy to tell apart. You can decode a hash like this one, or generate and check a new one at a chosen work factor, with the bcrypt generator and checker.
Every hash BCrypt.Net-Next writes is exactly 60 characters, and the first seven of them are the settings.

BCrypt.Net-Next Enhanced Entropy Mode
Despite BCrypt being a fairly secure hashing function, the default BCrypt implementation truncates the input password to 72 bytes. This, potentially, reduces the brute-force attempts needed to break a hash.
To overcome this limitation, BCrypt.Net-Next offers an enhanced entropy mode that pre-hashes the input password using SHA384. In this way, the input password turns into a fixed-length string that reflects all the variability of the original password regardless of its length.
The SHA384 digest is Base64-encoded before it reaches BCrypt, so whatever the user typed arrives as 64 Base64 characters plus the terminating null byte the library appends, 65 bytes in all, comfortably inside the 72-byte limit.
Let’s use the enhanced entropy mode with EnhancedHashPassword() and EnhancedVerify() static methods instead of the basic HashPassword() and Verify():
var passwordHash = BCrypt.EnhancedHashPassword("Password123!");
var result = BCrypt.EnhancedVerify("Password123!", passwordHash);
Assert.True(result);
As mentioned, BCrypt uses SHA384 by default for the pre-hash step. However, there may be situations where we need to specify a different algorithm:
var passwordHash = BCrypt.EnhancedHashPassword("Password123!", HashType.SHA512);
var result = BCrypt.EnhancedVerify("Password123!", passwordHash, HashType.SHA512);
Nothing in the stored hash records that enhanced entropy was used or which algorithm pre-hashed it, so that choice has to be a fixed decision in our code, made once, before the first user registers.
How Do We Rehash a Password When the Work Factor Changes?
A BCrypt hash carries the work factor it was created with, so raising the default does nothing to the hashes already in the database. They keep verifying at their old cost.
We cannot rehash them in a background job either, because we no longer have the passwords. We hold the plain text only when the user types it, most often at login, so that is where the upgrade happens.
PasswordNeedsRehash() takes a stored hash and our current minimum work factor and returns true when the hash is below it. The check only parses the string and computes no hash, so it costs almost nothing.
The rest is ordinary code. After a successful Verify(), if the hash is under-cost, we call HashPassword() on the password we already have in hand and write the new hash back.
InterrogateHash() returns the version, work factor, settings and raw hash as a HashInformation object, which is useful for reporting on what is still in the database.
Here are the static methods on BCrypt.Net.BCrypt that this article uses, plus a few it has not shown yet:
| To do this | Call on BCrypt.Net.BCrypt | Notes |
|---|---|---|
| Hash a password | HashPassword(inputKey) | Random salt, work factor 11 |
| Hash at a chosen cost | HashPassword(inputKey, workFactor) | Work factor 4 to 31 |
| Hash with a salt we generated | HashPassword(inputKey, salt) | Pair with GenerateSalt() |
| Verify a password | Verify(text, hash) | Compares in constant time |
| Hash with enhanced entropy | EnhancedHashPassword(inputKey) | SHA384 pre-hash by default |
| Choose the pre-hash algorithm | EnhancedHashPassword(inputKey, hashType) | HashType.SHA256, SHA384 or SHA512 |
| Verify an enhanced hash | EnhancedVerify(text, hash, hashType) | The HashType must match the one used to hash |
| Generate a salt | GenerateSalt(workFactor, bcryptMinorRevision) | Revision must be a, b, x or y |
| Ask whether a hash is under-cost | PasswordNeedsRehash(hash, newMinimumWorkLoad) | true when the hash's own work factor is lower |
| Read a hash's parts | InterrogateHash(hash) | Returns HashInformation |
| Change a password, checking the old one first | ValidateAndReplacePassword(currentKey, currentHash, newKey, workFactor, forceWorkFactor) | Throws BcryptAuthenticationException when the current password does not verify |
Password upgrades belong in the login path, because that is where returning users hand us the plain-text password. In the sample, it is a method of a PasswordLoginService class that declares public const int CurrentWorkFactor = 11;, in its own file with no using BCrypt.Net;, so it spells out BCrypt.Net.BCrypt:
public string? LoginAndUpgrade(string password, string storedHash)
{
if (!BCrypt.Net.BCrypt.Verify(password, storedHash))
return null;
return BCrypt.Net.BCrypt.PasswordNeedsRehash(storedHash, CurrentWorkFactor)
? BCrypt.Net.BCrypt.HashPassword(password, CurrentWorkFactor)
: null;
}
The method returns a new hash only when one is needed, and the caller writes it back to the user’s row. A null result means either a wrong password or a hash that is already at or above the current cost, so in a real login path the result of Verify() stays the login decision and the returned hash is only the upgrade.
Conclusion
In this article, we’ve learned what is BCrypt and how it uses salts and configurable complexity to create secure password hashes, also, we’ve learned what BCrypt variants are.
We’ve learned how to use BCrypt.Net-Next to secure passwords in our .NET projects and reviewed the anatomy of a BCrypt hash.
Finally, we learned how to make hashes even more secure using the enhanced entropy mode and increased computational cost, and how to rehash stored passwords at login when that cost goes up.
Tested with .NET 10.0.10 and BCrypt.Net-Next 4.2.0.

Can anyone beat bcrypt
There are certainly other tools to secure passwords, but I can’t say whether those are better or not.