Argon2id.PasswordHasher
1.0.0
dotnet add package Argon2id.PasswordHasher --version 1.0.0
NuGet\Install-Package Argon2id.PasswordHasher -Version 1.0.0
<PackageReference Include="Argon2id.PasswordHasher" Version="1.0.0" />
<PackageVersion Include="Argon2id.PasswordHasher" Version="1.0.0" />
<PackageReference Include="Argon2id.PasswordHasher" />
paket add Argon2id.PasswordHasher --version 1.0.0
#r "nuget: Argon2id.PasswordHasher, 1.0.0"
#:package Argon2id.PasswordHasher@1.0.0
#addin nuget:?package=Argon2id.PasswordHasher&version=1.0.0
#tool nuget:?package=Argon2id.PasswordHasher&version=1.0.0
<div align="center">
๐ Argon2id.PasswordHasher
The opinionated, secure-by-default Argon2id password hasher for .NET 8, 9, and 10.
Memory-hard password hashing that's hard to get wrong.
</div>
Argon2id.PasswordHasher wraps a vetted Argon2id implementation in a tiny, hard-to-misuse API with strong defaults out of the box. You get memory-hard resistance to GPU/ASIC cracking (RFC 9106 / OWASP-aligned), self-describing hashes that carry their own parameters, constant-time verification, an optional keyed pepper with rotation, and a one-line ASP.NET Core Identity integration โ without having to become a cryptographer first.
using Argon2id.PasswordHasher;
var hasher = new Argon2idPasswordHasher();
string stored = hasher.HashPassword("correct horse battery staple");
// $argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>
bool ok = hasher.VerifyPassword("correct horse battery staple", stored); // true
Try the demo
There are three runnable samples, each aimed at a different question:
| Sample | Where it runs | When to use it |
|---|---|---|
| Live WASM demo | In your browser, no install | Quickest way to try the library. Hashing runs on your CPU via WebAssembly. Auto-deployed to GitHub Pages on every push. |
| API documentation | DocFx, browser | Every public type + member, generated from XML doc comments. Co-published with the live demo. |
samples/Argon2id.PasswordHasher.IdentityMigrationSample |
Console (local) | The adoption path. Watch a real UserManager verify a stock PBKDF2 hash and transparently rewrite it as peppered Argon2id on first login โ zero resets, zero downtime. Copy its wiring into your app. |
samples/Argon2id.PasswordHasher.Demo |
Blazor Server (local) | Shows production-shape integration: DI, antiforgery, rate limiting, HSTS, CSP, constant-time login, memory-cost DoS gate. |
samples/Argon2id.PasswordHasher.WasmDemo |
Blazor WebAssembly (local) | Same UX as the live demo, but running against the in-tree library. Edit and refresh. |
Run any locally:
git clone https://github.com/systemslibrarian/argon2id-passwordhasher.git
cd argon2id-passwordhasher
# The migration walkthrough (PBKDF2 โ peppered Argon2id via real Identity):
dotnet run --project samples/Argon2id.PasswordHasher.IdentityMigrationSample
# Server flavor (production-shape, hardened):
dotnet run --project samples/Argon2id.PasswordHasher.Demo
# WASM flavor (same UX as the live demo):
dotnet run --project samples/Argon2id.PasswordHasher.WasmDemo
All samples use a ProjectReference to the in-tree library, so they always
exercise the version you're working on. They are demos, not starter
templates โ users live in memory and hash internals are shown on screen for
educational clarity (see each sample's own README).
Documentation
Documentation is layered so you only read the depth you need:
| Audience / question | Doc |
|---|---|
| "How do I use it?" | This README, plus API reference |
| "How do I migrate from Identity's default PasswordHasher?" | MIGRATION.md |
| "How should I run this in production?" | OPERATIONS.md โ capacity planning, monitoring, alerting, failure modes |
| "What about FIPS / SOC 2 / vendor questionnaires?" | COMPLIANCE.md |
| "What is the security threat model?" | THREAT-MODEL.md |
| "How do I store the pepper in Azure Key Vault / AWS / Vault?" | docs/pepper-key-management.md |
| "What's the support and lifecycle policy?" | SUPPORT.md |
| "How do I tune the work factor?" | docs/parameter-tuning.md |
| "What does the library deliberately NOT do?" | KNOWN-GAPS.md |
| "How do I report a vulnerability?" | SECURITY.md |
| "How do I contribute?" | CONTRIBUTING.md |
| "What changed in this version?" | CHANGELOG.md |
Table of contents
- Why this library
- Packages
- Install
- Quick start
- The hash format
- Configuration
- Upgrading work factor (rehash on login)
- Avoiding
stringpasswords (span overloads) - Pepper (secret key) with rotation
- ASP.NET Core Identity
- Trimming & Native AOT
- Security posture
- API reference
- FAQ
- Building, testing & benchmarks
- Versioning & status
- Contributing & security
- License & acknowledgements
Why this library
| ๐ก๏ธ Secure by default | The no-arg constructor gives you 64 MiB / t=3 / p=1 โ comfortably above the OWASP minimum. No footguns to configure. |
| ๐ฆ Self-describing hashes | Every hash is a standard PHC string containing its own parameters, so verification never breaks when you raise the work factor. |
| โป๏ธ Built-in upgrade path | NeedsRehash tells you when a stored hash is weaker than your current settings (or uses an old pepper) so you can upgrade users transparently. |
| โฑ๏ธ Constant-time | Final comparison uses CryptographicOperations.FixedTimeEquals. |
| ๐ถ๏ธ Pepper with rotation | Optional keyed secret kept outside your database, with first-class key rotation. |
| ๐งฉ ASP.NET Core ready | Drop-in IPasswordHasher<TUser> + a one-line DI extension. |
| ๐ Trim + AOT compatible | Marked IsTrimmable and IsAotCompatible so Native AOT consumers just work. |
| ๐งผ Tiny & honest | One runtime dependency. Every real limitation is documented in KNOWN-GAPS.md. |
Packages
| Package | Purpose | Depends on |
|---|---|---|
Argon2id.PasswordHasher |
Core hasher โ no web/framework dependency | Konscious.Security.Cryptography.Argon2 |
Argon2id.PasswordHasher.AspNetCore |
IPasswordHasher<TUser> adapter + DI extension |
the core package + Microsoft.Extensions.Identity.Core |
Both packages target net8.0, net9.0, and net10.0.
Install
# Core
dotnet add package Argon2id.PasswordHasher --prerelease
# Optional: ASP.NET Core Identity integration
dotnet add package Argon2id.PasswordHasher.AspNetCore --prerelease
Quick start
Registration โ hash the password and store the returned string:
var hasher = new Argon2idPasswordHasher();
user.PasswordHash = hasher.HashPassword(password);
await db.SaveChangesAsync();
Login โ verify against the stored string:
if (!hasher.VerifyPassword(password, user.PasswordHash))
return Unauthorized();
// Optionally strengthen the stored hash if your settings have grown:
if (hasher.NeedsRehash(user.PasswordHash))
{
user.PasswordHash = hasher.HashPassword(password);
await db.SaveChangesAsync();
}
VerifyPassword never throws on bad input: a null, empty, malformed, or non-Argon2id
stored value simply returns false.
Argon2idPasswordHasher is stateless and thread-safe. Create one and reuse it (e.g. register
it as a singleton) rather than constructing one per request.
The hash format
Hashes are emitted in the standard PHC string format used by libsodium and the Argon2 reference implementation:
$argon2id$v=19$m=65536,t=3,p=1$<base64 salt>$<base64 hash>
โโโ alg โโโโ ver โโโโ cost params โโโโโโ salt โโโโโโ hash โโโ
Because the parameters live inside the string, the hash is portable and self-verifying:
you can change your defaults whenever you like and old hashes keep validating with the parameters
they were created with. When a pepper is used, an extra keyid=<id> parameter is added so
verification can select the right secret.
Configuration
Pass an Argon2idOptions to tune the work factor. Invalid values (below the safe minimums) throw
ArgumentOutOfRangeException at construction โ fail fast, not silently weak.
var hasher = new Argon2idPasswordHasher(new Argon2idOptions
{
MemorySizeKib = 131072, // 128 MiB
Iterations = 4,
DegreeOfParallelism = 1,
});
| Option | Default | Minimum | Notes |
|---|---|---|---|
MemorySizeKib |
65536 (64 MiB) |
8192 |
Primary GPU/ASIC defense. Raise this first. |
Iterations |
3 |
1 |
Passes over memory (linear CPU cost). |
DegreeOfParallelism |
1 |
1 |
Lanes/threads per hash. Keep low on shared servers. |
SaltSizeBytes |
16 (128-bit) |
16 |
RFC 9106 recommendation. |
HashSizeBytes |
32 (256-bit) |
16 |
Output (tag) length. |
Why these defaults? They're a strong, general-purpose baseline for 2026 server hardware that
exceeds the current OWASP minimum (Argon2id, 19 MiB, t=2, p=1) while keeping p=1 so per-hash CPU
stays predictable under concurrent logins. They are not tuned to your machine โ measure and
adjust. See docs/parameter-tuning.md and the
benchmark project.
Argon2idOptions.Recommended exposes the defaults explicitly.
Upgrading work factor (rehash on login)
Security guidance gets stronger over time. When you raise your parameters, existing users upgrade themselves the next time they sign in โ no mass migration, no broken logins:
if (hasher.VerifyPassword(password, user.PasswordHash))
{
if (hasher.NeedsRehash(user.PasswordHash))
user.PasswordHash = hasher.HashPassword(password); // re-hashed with current settings
// sign the user in...
}
NeedsRehash returns true when the stored hash is unparseable, any stored parameter is below
your current configuration, or (with a pepper ring) the hash doesn't use your active pepper.
Avoiding string passwords (span overloads)
HashPassword / VerifyPassword also accept ReadOnlySpan<char> and ReadOnlySpan<byte>, so you
can hash a credential without ever materializing it as a string. The hasher zeroes every
password-derived buffer it owns.
ReadOnlySpan<char> pw = GetPasswordChars();
string hash = hasher.HashPassword(pw);
bool ok = hasher.VerifyPassword(pw, hash);
.NET cannot reliably wipe an immutable string from memory. Prefer the span overloads when the
password doesn't otherwise need to be a string. See KNOWN-GAPS.md ยง1.
Pepper (secret key) with rotation
A pepper is an application secret mixed into every hash and kept outside the database (in a
key vault, KMS, or environment variable). If your password table leaks but the pepper doesn't, the
stolen hashes can't be cracked offline. Peppers here are keyed and rotatable โ each hash records
which pepper produced it (via the PHC keyid), and the key bytes are never stored.
Lose your pepper ring, lose your users. The library never persists pepper keys โ that's your
responsibility. Hashes made with a pepper key cannot be verified without that key's bytes; there
is no recovery path. If the active key is lost, every user whose hash was produced under it must
go through a password reset. Back up active and retired pepper material to a separate trust
domain before you enable peppering. See
docs/pepper-key-management.md and
KNOWN-GAPS.md ยง2.
byte[] key = GetPepperFromVault(); // โฅ 16 bytes, kept secret
var ring = new PepperRing(new Pepper("2026-05", key));
var hasher = new Argon2idPasswordHasher(Argon2idOptions.Recommended, ring);
string hash = hasher.HashPassword(password);
// $argon2id$v=19$m=65536,t=3,p=1,keyid=<id>$<salt>$<hash>
Rotation โ promote a new active key and keep the old one as retired so existing hashes still
verify; NeedsRehash then upgrades them on the next login:
var rotated = new PepperRing(
active: new Pepper("2026-11", newKey),
retired: new Pepper("2026-05", oldKey));
For end-to-end examples of loading peppers from Azure Key Vault, AWS Secrets Manager,
Google Cloud Secret Manager, HashiCorp Vault, or environment variables, plus the full
rotation playbook, see docs/pepper-key-management.md.
ASP.NET Core Identity
Install Argon2id.PasswordHasher.AspNetCore and register the hasher in one line:
builder.Services
.AddIdentityCore<IdentityUser>()
.Services
.AddArgon2idPasswordHasher<IdentityUser>(); // optional: pass Argon2idOptions and/or a PepperRing
This registers an IPasswordHasher<TUser> backed by Argon2id and shares a single core hasher as a
singleton. Verification maps cleanly onto Identity's contract:
| Result | When |
|---|---|
Success |
Password matches and the hash is up to date. |
SuccessRehashNeeded |
Password matches but the hash is weaker than current settings / uses an old pepper. Identity rehashes it automatically. |
Failed |
Password doesn't match, or the stored value is malformed. |
Trimming & Native AOT
Both packages are marked IsTrimmable=true and IsAotCompatible=true. No reflection, no dynamic
codegen, no System.Reflection.Emit โ the library uses only BCL crypto primitives plus the
Konscious managed Argon2 implementation. Native AOT consumers can publish trimmed binaries without
warnings.
How this library compares
Quick orientation for the common alternatives in the .NET ecosystem:
| Library | Algorithm | Memory-hard | Self-describing hash | First-class pepper | ASP.NET Identity adapter | TFMs |
|---|---|---|---|---|---|---|
Argon2id.PasswordHasher (this library) |
Argon2id (RFC 9106) | โ | โ (PHC) | โ + rotation | โ + migration adapter | net8 / net9 / net10 |
Microsoft.AspNetCore.Identity.PasswordHasher<TUser> (the default) |
PBKDF2 HMAC-SHA-512, 100k iterations | โ | semi (version byte prefix) | โ | n/a (it is the default) | every .NET they support |
Konscious.Security.Cryptography.Argon2 (raw) |
Argon2i / Argon2d / Argon2id | โ | โ (raw bytes) | manual | โ | netstandard2.0 |
BCrypt.Net-Next |
bcrypt | โ | โ
($2a$โฆ) |
โ | community wrappers | netstandard2.0 |
Isopoh.Cryptography.Argon2 |
Argon2i / Argon2d / Argon2id | โ | โ (encoded) | โ | โ | netstandard2.0 |
When this library is the right choice: you want Argon2id specifically
(per OWASP's current top recommendation for password storage), with
self-describing PHC strings so you can raise the work factor without
breaking existing users, optional first-class pepper with rotation,
and a drop-in IPasswordHasher<TUser> so an existing ASP.NET Core
Identity app can migrate with one line.
When the default PasswordHasher<TUser> is the right choice: you're in
a FIPS-enforced deployment (Argon2id is not FIPS-approved; see
COMPLIANCE.md). For everything else, Argon2id is the
modern best answer.
Performance characteristics
The numbers below are starting points on typical 2024 server hardware
โ measure on yours before you ship. See
OPERATIONS.md for the full envelope, capacity model,
and monitoring guidance.
| Parameter set | Per-hash time | RAM per in-flight hash | Recommended for |
|---|---|---|---|
m = 19 456, t = 2 (OWASP minimum) |
~30โ60 ms | ~19 MiB | Floor; not recommended |
m = 65 536, t = 3 (library default) |
~150โ250 ms | ~64 MiB | Consumer SaaS, general-purpose |
m = 131 072, t = 4 |
~400โ600 ms | ~128 MiB | Internal / B2B, latency-tolerant |
m = 262 144, t = 5 |
~800 ms โ 1.2 s | ~256 MiB | High-value (banking, healthcare) |
Verification time tracks hash time within ~5%. The library emits
metrics under
Argon2idDiagnostics.MeterName
so you can chart this directly in your observability stack:
builder.Services.AddOpenTelemetry()
.WithMetrics(m => m.AddMeter(Argon2idDiagnostics.MeterName));
Trim and Native AOT publish is supported; both packages are marked
IsTrimmable and IsAotCompatible.
Security posture
| Concern | How this library handles it |
|---|---|
| GPU / ASIC cracking | Argon2id, memory-hard (64 MiB default) |
| Rainbow tables | 128-bit cryptographically random salt per hash (RandomNumberGenerator) |
| Parameter drift | Parameters embedded in the PHC string + NeedsRehash |
| Timing side channels | FixedTimeEquals on the final comparison |
| Sensitive memory | Password, salt, and candidate hash buffers zeroed with CryptographicOperations.ZeroMemory; Span overloads avoid string |
| Database-only leak | Optional keyed pepper (secret kept outside the DB), with rotation |
| Algorithm confusion | Verifier accepts only argon2id, version 19 |
| Insecure config | Below-minimum parameters throw at construction |
| Supply chain | SourceLink, deterministic builds, NuGetAudit at build time, CodeQL, build-provenance attestations on every release |
This library is one layer. It does not provide rate limiting, account lockout, breached-password
checks, or MFA โ those belong at your application/identity layer. For a frank account of everything
it does not do (plaintext string lifetime, memory-cost DoS, and more), read
KNOWN-GAPS.md. Transparency is a feature.
What we claim โ and what we don't
So you can calibrate trust precisely, here is the claim boundary in one place:
We claim: correct Argon2id per RFC 9106 โ continuously verified by a known-answer-vector corpus cross-checked against the reference C implementation, differential testing against an independent second implementation, coverage-guided fuzzing of the parser, property-based tests, and mutation testing of the test suite itself. Reproducible builds, provenance attestations, and SBOMs let you verify that what's on NuGet is what's in this repo.
We do not claim: an independent third-party cryptographic audit
(KNOWN-GAPS.md ยง11 โ none has been funded yet; the code is
structured and documented to make one cheap when it becomes possible), FIPS
validation (COMPLIANCE.md), or a vendor support contract
(SUPPORT.md). Until an audit happens, treat this as a
high-quality open-source dependency โ verify our work via the
threat model, the test suite, and the reproducible builds,
not our say-so.
API reference
Argon2idPasswordHasher
Argon2idPasswordHasher() // recommended defaults, no pepper
Argon2idPasswordHasher(Argon2idOptions options) // custom parameters
Argon2idPasswordHasher(Argon2idOptions options, PepperRing? pepper)
string HashPassword(string password)
string HashPassword(ReadOnlySpan<char> password)
string HashPassword(ReadOnlySpan<byte> password)
bool VerifyPassword(string password, string encodedHash)
bool VerifyPassword(ReadOnlySpan<char> password, string encodedHash)
bool VerifyPassword(ReadOnlySpan<byte> password, string encodedHash)
VerifyResult Verify(string password, string encodedHash) // single-parse: returns Success + NeedsRehash
VerifyResult Verify(ReadOnlySpan<char> password, string encodedHash)
VerifyResult Verify(ReadOnlySpan<byte> password, string encodedHash)
bool NeedsRehash(string encodedHash)
Argon2idOptions Options { get; }
VerifyResult (readonly record struct) โ Success, NeedsRehash,
static Failed. Returned by Verify(...).
Argon2idOptions (record) โ MemorySizeKib, Iterations, DegreeOfParallelism,
SaltSizeBytes, HashSizeBytes, Validate(), static Recommended.
Pepper โ Pepper(string id, byte[] key), string Id.
PepperRing โ PepperRing(Pepper active, params Pepper[] retired), Pepper Active.
Argon2id.PasswordHasher.AspNetCore โ Argon2idPasswordHasher<TUser> : IPasswordHasher<TUser>
and IServiceCollection.AddArgon2idPasswordHasher<TUser>(options?, pepper?).
The full public surface is locked by Microsoft.CodeAnalysis.PublicApiAnalyzers;
see PublicAPI.Shipped.txt / PublicAPI.Unshipped.txt next to each csproj.
FAQ
Which Argon2 variant? Argon2id only โ RFC 9106's recommended variant. Argon2i/Argon2d are intentionally not offered, and the verifier rejects them.
Are hashes interoperable with other Argon2 libraries? Yes for the standard form โ it's the same
PHC string libsodium and the reference implementation use. The optional keyid parameter is a PHC
extension some parsers may not expect.
Do I need to store the salt separately? No. The salt (and all parameters) are part of the returned string. Store that single value.
How slow should hashing be? Aim for roughly 100โ500 ms per hash on your production hardware for
interactive logins. Benchmark and tune โ see docs/parameter-tuning.md.
Can I hash API keys / tokens with this? It's designed for human passwords. High-entropy secrets don't need memory-hard hashing; a fast keyed hash (HMAC/SHA-256) is usually the right tool.
Building, testing & benchmarks
dotnet build -c Release
dotnet test -c Release
# Run the benchmarks (Release only)
dotnet run -c Release --project benchmarks/Argon2id.PasswordHasher.Benchmarks
# Run the Blazor demo
dotnet run --project samples/Argon2id.PasswordHasher.Demo
Repository layout:
src/Argon2id.PasswordHasher/ core library
src/Argon2id.PasswordHasher.AspNetCore/ IPasswordHasher<TUser> adapter + DI
tests/ xUnit test projects
benchmarks/ BenchmarkDotNet harness
samples/Argon2id.PasswordHasher.Demo/ runnable Blazor Server demo (hardened)
samples/Argon2id.PasswordHasher.WasmDemo/ Blazor WebAssembly demo (deployed to GH Pages)
docs/ tuning & design notes
.github/ CI, CodeQL, Dependabot, templates
CI builds and tests on every push/PR across Ubuntu, Windows, and macOS for all three TFMs.
CodeQL scans run weekly. Tagged releases (v*) pack both packages, generate CycloneDX SBOMs,
issue build-provenance attestations, and create a GitHub Release with all artifacts attached.
NuGet publication is a separate manual CLI step โ see PUBLISHING.md.
Versioning & status
1.0.0 โ stable. The public API surface and the PHC hash format are frozen under SemVer:
breaking changes only in a major version with a migration guide, and every hash emitted by a
1.x release verifies against every later 1.x release. The SUPPORT.md
supported-versions policy is in effect.
1.0.0 is a stability commitment, not an audit or assurance claim โ see
KNOWN-GAPS.md ยง9 for the precise boundary, which was stated before the
release rather than after.
See CHANGELOG.md for the full version history.
Contributing & security
Issues and PRs are welcome โ see CONTRIBUTING.md and
CODE_OF_CONDUCT.md. Security-relevant changes should update
SECURITY.md and/or KNOWN-GAPS.md.
Found a vulnerability? Please report it privately โ see SECURITY.md. Do not
open public issues for security reports.
License & acknowledgements
MIT ยฉ Paul Clark. See THIRD-PARTY-NOTICES.md for upstream
attributions.
Built on Konscious.Security.Cryptography.Argon2,
an MIT-licensed managed Argon2 implementation. We chose a managed port over the reference C /
libsodium so the package ships with no P/Invoke surface and runs unchanged under trimming, Native
AOT, and Blazor WebAssembly; that trade-off and how we mitigate it (version pin, NuGetAudit,
Dependabot, a pinned KAT cross-checked against the reference implementation) are documented in
SECURITY.md and
KNOWN-GAPS.md ยง12. Parameter guidance follows
RFC 9106 and the
OWASP Password Storage Cheat Sheet.
<div align="center">
To God be the glory โ 1 Corinthians 10:31.
</div>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Konscious.Security.Cryptography.Argon2 (>= 1.3.1)
-
net8.0
- Konscious.Security.Cryptography.Argon2 (>= 1.3.1)
-
net9.0
- Konscious.Security.Cryptography.Argon2 (>= 1.3.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Argon2id.PasswordHasher:
| Package | Downloads |
|---|---|
|
Argon2id.PasswordHasher.AspNetCore
ASP.NET Core Identity integration for Argon2id.PasswordHasher. Provides an IPasswordHasher<TUser> implementation backed by secure-by-default Argon2id, with automatic rehash-on-login (PasswordVerificationResult.SuccessRehashNeeded) and a one-line AddArgon2idPasswordHasher DI extension. Targets .NET 8, 9, and 10. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 131 | 7/2/2026 |
| 0.4.0-preview.5 | 185 | 6/11/2026 |
| 0.4.0-preview.4 | 207 | 6/2/2026 |
| 0.4.0-preview.3 | 63 | 5/31/2026 |
| 0.4.0-preview.2 | 63 | 5/30/2026 |