Project | Package |
---|---|
Singulink.Cryptography.PasswordHasher | |
Singulink.Cryptography.PasswordHasher.Argon2 |
PasswordHasher greatly simplifies implementing security best practices with upgradable hash algorithm passwords. Hashes are upgradable in the sense that you can easily transition them to a different algorithm or increase the total number of iterations as periodically required in order to maintain the desired level of password security.
Support for PBKDF2 (SHA256/SHA384/SHA512) and Argon2 (Argon2i/Argon2d/Argon2id) is included out-of-the-box. PBKDF2 with SHA1 is also supported but only for reading/upgrading legacy hashes. Other algorithms (i.e. bcrypt, scrypt, etc) can be plugged in by adding a custom implementation of the PasswordHashAlgorithm
class.
An additional layer of security can be added by encrypting hashes with a master key that is stored outside of the database so that hashes are not compromised if an attacker gains access to the database. AES128 encryption is included, but other algorithms can be plugged in by adding a custom implementation of the HashEncryptionAlgorithm
class. Master keys can be updated or rotated with minimal effort and should be generated from a completely random source.
PasswordHasher implements RFC 8265 / RFC 7613 PRECIS Framework-compliant password normalization to ensure users don't have any Unicode encoding related issues when entering passwords. All spaces are replaced with standard ASCII spaces, invalid Unicode and control characters are blocked, and passwords are normalized to Normalization Form C
as per the spec. Normalization can be turned off with a simple boolean property if you don't want normalization or you want to pre-process passwords with your own normalization scheme.
We are a small team of engineers and designers dedicated to building beautiful, functional and well-engineered software solutions. We offer very competitive rates as well as fixed-price contracts and welcome inquiries to discuss any custom development / project support needs you may have.
This package is part of our Singulink Libraries collection. Visit https://github.com/Singulink to see our full list of publicly available libraries and other open-source projects.
The package is available on NuGet - simply install the Singulink.Cryptography.PasswordHasher
package. To add Argon2 support install the Singulink.Cryptography.PasswordHasher.Argon2
package.
Supported Runtimes: Anywhere .NET Standard 2.1+ is supported, including:
- .NET Core 3.0+
- Mono 6.4+
- Xamarin.iOS 12.16+
- Xamarin.Android 10.0+
You can view the API on FuGet. The main functionality is exposed via the PasswordHasher
class in the Singulink.Cryptography
namespace.
Newer versions of PasswordHasher are backwards-compatible and thus can always read and verify hashes from previous versions and continue to work as expected. The RequiresRehash()
method will return true
if a rehash should be performed because settings have changed (i.e. normalization is now on by default) but old hashes will still continue to verify just fine.
New features added in version 2 include:
- Added Argon2 support via the
Singulink.Cryptography.PasswordHasher.Argon2
package - Added hash encryption support
- Added password normalization support (enabled by default)
API changes:
- Legacy hash algorithms are no longer passed into the
PasswordHasher
constructor - usePasswordHasherOptions
to add legacy algorithms and pass that into the constructor instead. RequiresHashChainUpgrade()
has been renamed toRequiresUpdate
.UpgradeHashChain()
has been renamed toUpdate
.RequiresRehash()
requires an additionalpassword
parameter now.- Rehashing existing passwords should be done with
Rehash()
instead ofHash()
. - Methods that accept passwords in
byte[]
format have been removed in order to properly facilitate normalization functionality.
To create a PasswordHasher
you use the following constructor:
public PasswordHasher(PasswordHashAlgorithm algorithm, int iterations, PasswordHasherOptions? options);
The algorithm
and iterations
parameters specify what the main algorithm and total number of iterations should be. The options
parameter specifies any additional options, i.e. normalization, salt size, or any legacy algorithms and encryption parameters that the hasher must still be capable of reading.
PasswordHasher
is thread-safe so instances can be safely shared between threads. It contains the following primary methods:
string Hash(string password);
bool Verify(string hash, string password);
bool RequiresRehash(string hash, string password);
string Rehash(string password);
bool RequiresUpdate(string hash);
string? Update(string hash);
The first two methods should be self-explanatory: Hash
produces a password hash, where-as Verify
is used to verify a hash/password combo.
The last four methods are where it gets interesting:
RequiresRehash
: Returns a value indicating whether a hash string should be regenerated from the known password. Returns true if the hash string contains chained hashes, the main algorithm / number of iterations does not match, the main encryption parameters do not match, or normalization settings do not match.Rehash
: Safely rehashes an existing password by falling back to previous normalization settings if normalization fails with current settings.RequiresUpdate
: Returns a value indicating whether the hash string needs to be updated, meaning the main encryption parameters do not match or an additional entry needs to be added to the hash chain so that it uses the main algorithm and total required number of iterations.Update
: Gets an updated hash string that uses the main encryption parameters and main hash algorithm with the total number of required iterations without knowing the password, or returnsnull
if the hash string does not require an update. Changing hash algorithms or adding iterations without knowing the password is achieved by hash chaining. If the hash algorithm or number of iterations has changed then the resulting hash string will returntrue
when passed into theRequiresRehash()
method, which should be tested on successful user login so that a new hash string without chaining can be generated with theRehash()
method.
The format of the hash string is as follows:
!normalization_version #encryption_parameters_id hash_algorithm_id:iterations:salt hash
The first two parts are optional, so if normalization or encryption is not enabled then those elements are omitted.
Example 1:
!1 SHA256:1000:FV6nVAAqg1exolA+9fY2Nw== eqko5aiXBc+1BIBMKNi3VIhK9iPPW/dX85FcsVd1ITs=
Normalization: V1 algorithm (PRECIS RFC 8265)
Hash Encryption: None
Hash Algorithm: SHA256 (PBKDF2)
Iterations: 1000
Salt (Base64): FV6nVAAqg1exolA+9fY2Nw==
Hash (Base64): eqko5aiXBc+1BIBMKNi3VIhK9iPPW/dX85FcsVd1ITs=
Example 2:
#123 Argon2idV19-128-4P-512MB:5:1KmmrJ5fTKXOUWqlYCD7zQ== Nw0DxzZnXhe531IhEoE3ziqRJLxQiqh7Ovcs6H8IZVNqiKHilbhYKAJnBYJIyVybtc8U93P1Kr8gvIK18HtkboQYdnpFShbnEVCnjRXiF076kMxf4FtX4+kA+wUHVuzR
Normalization: None
Hash Encryption: Using parameters with ID# 123
Hash Algorithm: Argon2id V19[1.3] (128bit hash, parallelism: 4, memory: 512MB)
Iterations: 5
Salt (Base64): 1KmmrJ5fTKXOUWqlYCD7zQ==
Hash (Base64): Nw0DxzZnXhe531IhEoE3ziqRJLxQiqh7Ovcs6H8IZVNqiKHilbhYKAJnBYJIyVybtc8U93P1Kr8gvIK18HtkboQYdnpFShbnEVCnjRXiF076kMxf4FtX4+kA+wUHVuzR
The hash string in example 2 is the result of encrypting the hash algorithm output using the encyption parameters with ID 123
.
If the hash chain was updated at some point (i.e. had additional algorithms or iterations applied to it), then those are added to the list. Each chained algorithm has its own salt value and the output bytes from the previous algorithm are fed into the next one. For example, if we started with SHA256 with 1000 iterations and upgraded to SHA512 with 20,000 iterations, the hash string might look something like this:
SHA256:1000:9QTkU8cSJ8xXkUdrx8qQVg== SHA512:20000:dlZfZk6CQstiyUAnZH5L7w== 07qYVKg1yx+AiRP+2oLxv3ozRmJ4tvb/IkgnsCO40LXT8Pm+bXXQnHoqKTQMy7e4IbMbTzOVH7cDqqBZ5RyygA==
Usage of the library is best demonstrated with some examples:
using Singulink.Cryprography;
string password = "12345678";
// Create hasher that uses SHA256 with 10,000 PBKDF2 iterations
var hasher = new PasswordHasher(PasswordHashAlgorithm.SHA256, 10000);
// Create a password hash
string hash = hasher.Hash(password);
// Verify the password
bool success = hasher.Verify(hash, password); // true
PasswordHasherOptions
can be used to specify additional options:
var options = new PasswordHasherOptions {
Normalize = false,
SaltSize = 20,
};
var hasher = new PasswordHasher(PasswordHashAlgorithm.SHA256, 10000, options);
Hashes can be mass-updated with more iterations or new agorithms by writing a script (i.e. a CSX script) or a small utility program that does something like the following:
// Upgrade hashes in the database to SHA512 with 20,000 iterations. The SHA256 algorithm must be
// passed into the LegacyHashAlgorithms property so the hasher can read the current SHA256 hashes.
var options = new PasswordHasherOptions {
LegacyHashAlgorithms = { PasswordHashAlgorithm.SHA256 },
};
var hasher = new PasswordHasher(PasswordHashAlgorithm.SHA512, 20000, options);
foreach (var user in database.GetUsers())
{
if (hasher.RequiresUpdate(user.PasswordHash))
user.PasswordHash = hasher.Update(user.PasswordHash);
}
database.SaveChanges();
After running the script above, the hash strings in the database would now be composed of a SHA256 10,000 iteration hash chained to a SHA512 20,000 iteration hash. You will then want to rehash passwords to eliminate the chaining upon successful authentication using login code similar to the following:
// The SHA256 algorithm must still be passed into the LegacyHashAlgorithms property since the chained
// hashes contain a SHA256 component until they are rehashed.
var options = new PasswordHasherOptions {
LegacyHashAlgorithms = { PasswordHashAlgorithm.SHA256 },
};
var hasher = new PasswordHasher(PasswordHashAlgorithm.SHA512, 20000, options);
bool Login(string username, string password)
{
var user = database.GetUser(username);
if (user == null || !hasher.Verify(user.PasswordHash, password))
return false;
if (hasher.RequiresRehash(user.PasswordHash, password))
{
// Generate a "pure" SHA512 hash since it is currently chained with the old SHA256 hash
user.PasswordHash = hasher.Rehash(password);
database.SaveChanges();
}
return true;
}
If you don't want to mass-update all the hashes up-front, you can simply skip that step and use the code above to rehash passwords incrementally when users successfully login, or a combination of both approaches (i.e. rehash on login for a period of time before mass-updating any leftover old hashes).
Adding new hash encryption parameters is done in a similar manner as updating the hash algorithm or number of iterations:
// Set main encryption parameters to ID 10, AES128 algorithm and MasterKey10.
// GetMasterKey10() should get the key from somewhere other than the database
// (i.e. secure storage, config file, hard-coded, etc).
var options = new PasswordHasherOptions {
EncryptionParameters = new HashEncryptionParameters(10, HashEncryptionAlgorithm.AES128, GetMasterKey10()),
};
var hasher = new PasswordHasher(PasswordHashAlgorithm.SHA512, 20000, options);
// Update all hashes to use the encryption parameters:
foreach (var user in database.GetUsers())
{
if (hasher.RequiresUpdate(user.PasswordHash))
user.PasswordHash = hasher.Update(user.PasswordHash);
}
database.SaveChanges();
Updating the master key is done by adding another set of encryption parameters with a new ID and putting the old parameters into the LegacyEncryptionParameters
collection so the hasher can still decrypt the old values:
var options = new PasswordHasherOptions {
EncryptionParameters = new HashEncryptionParameters(11, HashEncryptionAlgorithm.AES128, GetMasterKey11()),
LegacyEncryptionParameters = {
new HashEncryptionParameters(10, HashEncryptionAlgorithm.AES128, GetMasterKey10()),
},
};
var hasher = new PasswordHasher(PasswordHashAlgorithm.SHA512, 20000, options);
// Update all hashes to use the new encryption parameters:
foreach (var user in database.GetUsers())
{
if (hasher.RequiresUpdate(user.PasswordHash))
user.PasswordHash = hasher.Update(user.PasswordHash);
}
database.SaveChanges();
After adding the Singulink.Cryptography.PasswordHasher.Argon2
package, you can do the following:
// Create Argon2id v19/1.3 with parallelism: 4, memory: 512MB and 256-bit output.
var argon2Algorithm = new Argon2HashAlgorithm(Argon2Type.Argon2id, Argon2Version.V19, 4, 512, 256);
// Create a hasher that uses the above algorithm with 5 iterations
var hasher = new PasswordHasher(argon2Algorithm, 5);
string hash = hasher.Hash(password);
All the features of PasswordHasher
work as you would expect with Argon2. Note that all the Argon2 parameters must stay the same for incremental iteration chaining to be utilized when Update()
is called. If any of the parameters change, it is considered a new algorithm and the full number of iterations will be chained to the previous hash. Since every set of parameters is considered a different algorithm, make sure you add the Argon2HashAlgorithm
instance with the old parameters to PasswordHasherOptions.LegacyHashAlgorithms
so the hasher knows how to read those hashes.
Argon2 support is provided via a dependency to the excellent Isopoh.Cryptography.Argon2 package.