---
title: "Hash passwords with Argon2 in C#"
url: https://recursiveai.org/onecrypto/cookbook/argon2-password-hashing/
description: "Hash and verify passwords with Argon2id in C# on .NET 10: a random salt, the OWASP minimum parameters, a standard $argon2id$ string to store, and verification. Checked against OpenSSL. Runnable code with OneCrypto 0.1.0-preview.1."
version: "0.1.0-preview.1"
---

# Hash passwords with Argon2 in C#

.NET has PBKDF2 but no Argon2. OneCrypto’s `Argon2.DeriveKey` implements RFC 9106 (Argon2d, Argon2i and Argon2id), and `Argon2.Verify` recomputes a hash and compares it. The program below hashes a password with Argon2id, stores it in the usual `$argon2id$v=19$…` string, and verifies a right and a wrong password.

```shell
dotnet new console -o Argon2Demo && cd Argon2Demo
dotnet add package RecursiveAI.OneCrypto --version 0.1.0-preview.1
```

OneCrypto is a preview, so `dotnet add package` needs `--version` or `--prerelease`. Put this in `Program.cs` and run `dotnet run`:

```csharp
using System.Security.Cryptography;
using System.Text;
using RecursiveAI.OneCrypto.Primitives;

// OWASP's minimum for Argon2id: 19 MiB of memory, 2 passes, 1 lane
const int MemoryKiB = 19 * 1024, Passes = 2, Lanes = 1;

string stored = Hash("correct horse battery staple");
Console.WriteLine(stored);
Console.WriteLine($"Right password: {Verify("correct horse battery staple", stored)}");
Console.WriteLine($"Wrong password: {Verify("correct horse battery staplr", stored)}");

static string Hash(string password)
{
    byte[] salt = RandomNumberGenerator.GetBytes(16);
    byte[] pwd = Encoding.UTF8.GetBytes(password);
    byte[] tag = Argon2.DeriveKey(Argon2Type.Argon2id, pwd, salt,
        MemoryKiB, Passes, Lanes, tagLength: 32);
    // Saved only for the OpenSSL check below
    File.WriteAllText("salt.hex", Convert.ToHexString(salt));
    File.WriteAllBytes("tag.bin", tag);
    return $"$argon2id$v=19$m={MemoryKiB},t={Passes},p={Lanes}${B64(salt)}${B64(tag)}";
}

static bool Verify(string password, string stored)
{
    // $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>
    string[] f = stored.Split('$');
    var p = f[3].Split(',').ToDictionary(kv => kv[..1], kv => int.Parse(kv[2..]));
    return Argon2.Verify(Argon2Type.Argon2id, Encoding.UTF8.GetBytes(password),
        FromB64(f[4]), p["m"], p["t"], p["p"], FromB64(f[5]));
}

// The $argon2id$ string uses base64 without padding
static string B64(byte[] b) => Convert.ToBase64String(b).TrimEnd('=');
static byte[] FromB64(string s) =>
    Convert.FromBase64String(s.PadRight(s.Length + (4 - s.Length % 4) % 4, '='));
```

```text
$argon2id$v=19$m=19456,t=2,p=1$rQZ0z6SupSxVazCJ40LjWg$nRMXNg8kcaX52RRLnW3RSlBUUH19wyaBASnOVgY8FV8
Right password: True
Wrong password: False
```

The first line is what you store: the algorithm, version, parameters, salt and hash in one string, so you can raise the parameters later and still verify old hashes. `Argon2.Verify` compares the hashes with `CryptographicOperations.FixedTimeEquals`. Hashing takes tens of milliseconds by design; that cost is what slows down guessing.

## Check it with OpenSSL

OpenSSL 3.2 and later have Argon2. Given the same password, salt and parameters it must produce the same 32 bytes:

```shell
openssl kdf -keylen 32 -binary -out tag-openssl.bin \
  -kdfopt pass:"correct horse battery staple" -kdfopt hexsalt:$(cat salt.hex) \
  -kdfopt iter:2 -kdfopt memcost:19456 -kdfopt lanes:1 -kdfopt threads:1 ARGON2ID
cmp tag.bin tag-openssl.bin && echo "Same Argon2id hash"
```

```text
Same Argon2id hash
```

## Parameters and limits

`Argon2.DeriveKey` takes the memory in KiB, the passes, the lanes and the tag length, plus the optional secret and associated data of RFC 9106. Parameters read from someone else’s data are capped by default (2 GiB of memory, 64 passes, 64 lanes) and raise `KdfLimitExceededException` before anything is allocated; pass your own `KdfLimits` to change that. `Scrypt.DeriveKey` (RFC 7914) works the same way.

## Limits

- **Not audited.** This library has not been independently audited. It is a preview: 0.1.0-preview.1, and the API can change.
- **Side channels.** Argon2id “uses data-dependent memory access after its first half-pass by design (RFC 9106 §3.4); only Argon2i is data-independent.” The project makes no constant-time claim for Argon2id, Argon2d or scrypt: [Password-based key derivation](https://recursiveai.org/onecrypto/docs/kdf/).
- **No string format built in.** OneCrypto returns the raw hash; the `$argon2id$` string above is built by the program. Check the format against the other systems that will read it.
- **Speed.** Against the fastest managed .NET implementation, Argon2 takes 0.36 of its time on x64 and 0.46 on arm64, from the project’s benchmark table. The x64 run is marked load-flagged.

How Argon2, scrypt and bcrypt-pbkdf are implemented and tested: [Password-based key derivation](https://recursiveai.org/onecrypto/docs/kdf/).
