How to Use Argon2 for Password Hashing in C#

In the past couple of blog posts, I’ve been discussing various password hashing algorithms and how Argon2 (specifically Argon2id) is the algorithm that leading cryptographers recommend. In this post, I want to discuss how to consume the Argon2id algorithm from a C# application using .NET Core.

You have two libraries to choose from if you want Argon2 in the .NET world: Konscious.Security.Cryptography and Libsodium.

This is the third part in a series of blog posts concerning storing a password securely. For background information, see the following posts:

Using Konscious.Security.Cryptography for Argon2

If you want to use Argon2 in a .NET application, the Argon2 documentation recommends the Konscious.Security.Cryptography library.

This library is incredibly simple to consume from a .NET application. Further, it offers all of the flexibility that we need in order to tune Argon2 to our environment. The biggest downside with this library is that it isn’t widely used. As of the time of this post, the project has a mere 50 stars on GitHub and is used by only 29 projects (one of them being my own). Still, it appears to be actively maintained and it works in .NET Core.

So let’s get started. First thing’s first. Add a NuGet package reference to your project. Simply search for “Konscious.Security.Cryptography” and it should be the first one to pop up.

Create a function that will return a random salt. We will use the RNGCryptoServiceProvider to generate a cryptographically secure random number.

private byte[] CreateSalt()
{
    var buffer = new byte[16];
    var rng = new RNGCryptoServiceProvider();
    rng.GetBytes(buffer);
    return buffer;
}

Create a function to create a password hash taking a password and salt as parameters. In this function, instantiate a new Argon2id object and initialize it with the parameters discussed in my previous post [citation needed]. I am using the sensitive values from Libsodium’s implementation here, with degrees of parallelism with twice the number of CPU cores that I have.

private byte[] HashPassword(string password, byte[] salt)
{
    var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password));

    argon2.Salt = salt;
    argon2.DegreeOfParallelism = 8; // four cores
    argon2.Iterations = 4;
    argon2.MemorySize = 1024 * 1024; // 1 GB

    return argon2.GetBytes(16);
}

In order to verify the password hash, we simply need to recalculate the hash given the same inputs. Next, we compare the old hash with the newly calculated hash. This is pretty easy using LINQ:

private bool VerifyHash(string password, byte[] salt, byte[] hash)
{
    var newHash = HashPassword(password, salt);
    return hash.SequenceEqual(newHash);
}

Finally, we can put it all together:

public void Run()
{
    var password = "Hello World!";
    var stopwatch = Stopwatch.StartNew();

    Console.WriteLine($"Creating hash for password '{ password }'.");

    var salt = CreateSalt();
    Console.WriteLine($"Using salt '{ Convert.ToBase64String(salt) }'.");

    var hash = HashPassword(password, salt);
    Console.WriteLine($"Hash is '{ Convert.ToBase64String(hash) }'.");

    stopwatch.Stop();
    Console.WriteLine($"Process took { stopwatch.ElapsedMilliseconds / 1024.0 } s");

    stopwatch = Stopwatch.StartNew();
    Console.WriteLine($"Verifying hash...");

    var success = VerifyHash(password, salt, hash);
    Console.WriteLine(success ? "Success!" : "Failure!");

    stopwatch.Stop();
    Console.WriteLine($"Process took { stopwatch.ElapsedMilliseconds / 1024.0 } s");
}

Of course, you never want to log a password or its hash. This is simply for demonstration purposes…

Creating hash for password 'Hello World!'.
Using salt 'Wn6hLboKBwVII10VDspD+Q=='.
Hash is 'C7ti2i+6lX9g+6oNhs3n5Q=='.
Process took 4.4951171875 s
Verifying hash...
Success!
Process took 3.7197265625 s

Using Libsodium for Argon2

NaCl (pronounced “salt”) is one of the most highly respected cryptographic libraries available today. Perhaps the most well-respected cross-platform supported fork of NaCl is Libsodium. A compiled library of Libsodium is available for practically any platform.

Libsodium provides numerous APIs to make working with cryptography easy, thereby preventing a developer from proverbially shooting himself/herself in the foot. However, these are still low-level APIs designed to be cross-platform. So, consuming it comes at a bit of a cost. There are .NET-specific wrappers for Libsodium, but they are either outdated or haven’t garnered enough attention to be fully trusted. And, some of the ones that look to be promising unnecessarily remove critical parameters and make blanket assumptions about others.

Because of this, consuming Libsodium from .NET will probably result in a bit more technical debt than you want. It’s got a great following, though, with close to 7,000 stars at the time of this writing.

First, add a NuGet package reference to libsodium. Don’t be tempted to add a reference to “libsodium-net” or “libsodium-netcore.” Neither of those packages are supported anymore.

Next, we need to add a new class to contain some P/Invoke functions. I’ll borrow from some of the semantics I’ve seen shared among .NET implementations of Libsodium. Note that this isn’t a full-fledged implementation. But, this should be enough to get you going:

public class SodiumLibrary
{
    private const string Name = "libsodium";
    public const int crypto_pwhash_argon2id_ALG_ARGON2ID13 = 2;
    public const long crypto_pwhash_argon2id_OPSLIMIT_SENSITIVE = 4;
    public const int crypto_pwhash_argon2id_MEMLIMIT_SENSITIVE = 1073741824;

    static SodiumLibrary()
    {
        sodium_init();
    }

    [DllImport(Name, CallingConvention = CallingConvention.Cdecl)]
    internal static extern void sodium_init();

    [DllImport(Name, CallingConvention = CallingConvention.Cdecl)]
    internal static extern void randombytes_buf(byte[] buffer, int size);

    [DllImport(Name, CallingConvention = CallingConvention.Cdecl)]
    internal static extern int crypto_pwhash(byte[] buffer, long bufferLen, byte[] password, long passwordLen, byte[] salt, long opsLimit, int memLimit, int alg);
}

Now, create a function that will return a random salt. This will simply wrap the call to Libsodium’s randombytes_buf() function:

private byte[] CreateSalt()
{
    var buffer = new byte[16];
    SodiumLibrary.randombytes_buf(buffer, buffer.Length);
    return buffer;
}

Next, create a function to create a password hash. This is a wrapper for the call to Libsodium’s crypto_pwhash() function. In this example, I’m using Libsodium’s sensitive values for iterations and memory usage. Note that Libsodium doesn’t allow you to specify a degree of parallelism here. Libsodium uses a single degree of parallelism. I’m not quite sure why…

private byte[] HashPassword(string password, byte[] salt)
{
    var hash = new byte[16];

    var result = SodiumLibrary.crypto_pwhash(
        hash,
        hash.Length,
        Encoding.UTF8.GetBytes(password),
        password.Length,
        salt,
        SodiumLibrary.crypto_pwhash_argon2id_OPSLIMIT_SENSITIVE,
        SodiumLibrary.crypto_pwhash_argon2id_MEMLIMIT_SENSITIVE,
        SodiumLibrary.crypto_pwhash_argon2id_ALG_ARGON2ID13
        );

    if (result != 0)
        throw new Exception("An unexpected error has occurred.");

    return hash;
}

You can use the same code above to write the results to the console window. Here are my results:

Creating hash for password 'Hello World!'.
Using salt 'EQRK407Y4+4XWKxkERFmYw=='.
Hash is 't7NZI/QpYYJkWQtObOLNRg=='.
Process took 2.5537109375 s
Verifying hash...
Success!
Process took 2.525390625 s

Conclusion

I recommend storing the iterations, memory usage, and degree of parallelism used alongside the salt and hash. That way, if you ever need to modify these parameters (which you will), you’ll be able to migrate your users to more secure values as they log into the system.

Both options above are fairly simple to implement with Konscious.Security.Cryptography requiring slightly less code and more flexibility. Libsodium requires a bit more code and isn’t as flexible, but it comes with the support of quite a large community. I personally feel that whatever library you choose, you’ll be off to a great start at protecting your passwords in the event of a data breach!

8 Comments

  1. vzdades October 18, 2019
  2. Thusy January 2, 2020
  3. Thusy January 2, 2020
  4. Bjoern June 24, 2020
  5. lois May 20, 2021
    • Bryan Burman May 21, 2021
  6. Jose May 28, 2021
  7. Siva November 3, 2021

Leave a Reply