PostQuantum.FileEncryption
1.4.1
Requires NuGet 6.0.0 or higher.
dotnet add package PostQuantum.FileEncryption --version 1.4.1
NuGet\Install-Package PostQuantum.FileEncryption -Version 1.4.1
<PackageReference Include="PostQuantum.FileEncryption" Version="1.4.1" />
<PackageVersion Include="PostQuantum.FileEncryption" Version="1.4.1" />
<PackageReference Include="PostQuantum.FileEncryption" />
paket add PostQuantum.FileEncryption --version 1.4.1
#r "nuget: PostQuantum.FileEncryption, 1.4.1"
#:package PostQuantum.FileEncryption@1.4.1
#addin nuget:?package=PostQuantum.FileEncryption&version=1.4.1
#tool nuget:?package=PostQuantum.FileEncryption&version=1.4.1
PostQuantum.FileEncryption
Open-source (MIT), fail-closed file and stream encryption for .NET 8 and .NET 10 — constant-memory streaming for files of any size, a frozen and publicly specified container format, and a production post-quantum upgrade path.
Two friendly classes — PqFileEncryptor and PqFileDecryptor — handle authenticated,
chunked, streaming encryption with strong, modern defaults. A 10 GB backup encrypts in
roughly 130 KB of working memory, stream-to-stream or file-to-file. You should not have to
read a cryptographic spec to protect a file: call a method, and the library does the
careful, paranoid, fail-closed thing every time. And because the code, the format
specification, the test vectors, the threat model, and the gaps ledger are all public,
you never have to take that on faith.
Status:
1.4.1— stable release. The symmetric, passphrase-based engine is production-ready and the.pqfev2 container format is FROZEN for the1.xline. The companionPostQuantum.FileEncryption.Hybridpackage provides production X25519 + ML-KEM-768 hybrid public-key encryption with multi-recipient support. The inline ML-KEM-only recipient mode in the core is deprecated (PQFE002) — see Post-quantum & the upgrade path.
Why this library
- Production-ready core. Authenticated AES-256-GCM with constant-memory chunked streaming — files of any size, multi-gigabyte included, in ~130 KB of working memory — plus atomic file output, cancellation, progress, and zeroable secrets. 150 tests on both target frameworks, continuous fuzzing, byte-compatible Rust/WASM reference, native-AOT smoke-tested in CI.
- Frozen format.
.pqfev2 is pinned by cross-checked known-answer vectors and a conformance specification. A file you encrypt today opens with every1.xbuild, on every platform, in either implementation. - Locked public API.
Microsoft.CodeAnalysis.PublicApiAnalyzersbaselines every public member;<EnablePackageValidation>checks binary compatibility against the previous release at pack time. Accidental breakage fails the build. - Honest supply chain. Every release artifact ships with a CycloneDX
SBOM, a SLSA-style build-provenance
attestation, and SourceLink. The
release workflow runs
Meziantou.Framework.NuGetPackageValidationagainst every.nupkgbefore publish. - Honest about limits. The Known Gaps ledger lists everything that is not yet done. The library has not been independently audited; engagements are welcome.
When to use this
- You're on .NET 8 or .NET 10 and want a drop-in, fail-closed file/stream encryptor with excellent defaults and no FFI.
- You stream large files — backups, media, exports — and need encryption that runs in constant memory regardless of size, with progress and cancellation.
- You want your cryptography open: MIT-licensed code, a published format specification, cross-implementation test vectors, and a public threat model — not a proprietary binary with a license-activation call.
- You need post-quantum data confidentiality today (AES-256 against a harvest-now- decrypt-later adversary) and a clear path to post-quantum public-key encryption via the Hybrid package.
- You want enterprise affordances: telemetry, atomic output, a documented format with test vectors, a published threat model, signed releases, and a locked API.
- You want a comparison vs.
age, libsodium, and OpenSSL before committing.
For a side-by-side with other encryption libraries and migration guidance, see docs/MIGRATION.md.
Not the right tool if
Being clear about scope is part of the security contract. Reach for something else when:
- You need full-disk or volume encryption — use BitLocker, FileVault, LUKS, or VeraCrypt.
- You need to hide metadata — file names, paths, sizes, and timestamps are not protected; plaintext length is revealed to within a chunk (see KNOWN-GAPS.md).
- You want key management at rest (generation, storage, rotation of long-lived keys) — this
library encrypts data; pair it with a KMS/HSM via
IContentKeyProvider. - You expect a standardized, cross-tool container —
.pqfeis its own documented format, not PGP, CMS, age, or JOSE, and will not open in those tools. - You need binary content typing, compression, or de-duplication — all out of scope by design.
Install
# Core (passphrase + envelope-key engine)
dotnet add package PostQuantum.FileEncryption --version 1.4.1
# Add this only if you need public-key (recipient) encryption
dotnet add package PostQuantum.FileEncryption.Hybrid --version 1.4.1
# Optional: detached Ed25519 + ML-DSA-65 signatures (sender authenticity)
dotnet add package PostQuantum.FileEncryption.Signing --version 1.4.1
# Optional: cloud envelope-key providers (the master key stays in your KMS/HSM)
dotnet add package PostQuantum.FileEncryption.Aws # AWS KMS
dotnet add package PostQuantum.FileEncryption.AzureKeyVault # Azure Key Vault / Managed HSM
# Optional: Microsoft.Extensions.DependencyInjection integration
# (AddPqFileEncryption() / AddPqHybridFileEncryption())
dotnet add package PostQuantum.FileEncryption.Extensions.DependencyInjection --version 1.4.1
Targets .NET 8 and .NET 10 (net8.0; net10.0), with an identical public API on
both. Core depends only on
Konscious.Security.Cryptography.Argon2 (and only when you select Argon2id); everything
else is from .NET's System.Security.Cryptography. The Hybrid package additionally pulls in
BouncyCastle.Cryptography so it runs on every platform without a native ML-KEM dependency.
▶ Try it
Three ways to drive the library — all produce the same .pqfe format:
1. Command-line — install the pqfe dotnet tool
No code required:
dotnet tool install -g PostQuantum.FileEncryption.Tool
pqfe encrypt report.pdf report.pdf.pqfe --argon2id # prompts for a passphrase
pqfe decrypt report.pdf.pqfe report.pdf
pqfe keygen me.key # Ed25519 + ML-DSA-65 signing key pair
pqfe sign report.pdf.pqfe me.key # writes report.pdf.pqfe.sig
pqfe verify report.pdf.pqfe me.key.pub # exit 0 = authentic, 65 = reject
The source lives at samples/Pqfe.Cli and is built on the public
API. It's also the canary that proves IsAotCompatible=true end-to-end: CI publishes it
with PublishAot=true and round-trips a real file as the smoke test.
# Run from source via dotnet:
PQFE_PASS='correct horse battery staple' \
dotnet run -c Release --project samples/Pqfe.Cli -- \
encrypt report.pdf report.pdf.pqfe --argon2id --passphrase-env PQFE_PASS
# Or publish a single-file native binary:
dotnet publish samples/Pqfe.Cli -c Release -p:PublishAot=true -o ./bin
./bin/pqfe --help
2. Browser demo — fully client-side (Rust → WebAssembly)
samples/pqfe-web is a static page whose file never leaves your
browser: a small Rust core compiled to WebAssembly does passphrase-based AES-256-GCM
locally. It's hostable on GitHub Pages with no server (see the
Pages workflow).
cd samples/pqfe-wasm
rustup target add wasm32-unknown-unknown
wasm-pack build --target web --release --out-dir ../pqfe-web/pkg
cd ../pqfe-web && python3 -m http.server 8080 # open http://localhost:8080
This Rust core is an independent re-implementation of the format, kept byte-compatible
with the .NET library: the Rust tests decrypt the .NET known-answer vectors, and the .NET
tests decrypt a Rust-produced container (CrossImplementationTests). A file encrypted in
the browser opens with the library, and vice versa.
3. .NET demo — runs the real library (Blazor Server)
samples/PostQuantum.FileEncryption.Demo
exercises the actual library through a web UI. Files are processed in memory and never
written to disk.
dotnet run --project samples/PostQuantum.FileEncryption.Demo
# then open the printed http://localhost:<port> URL
It's a Blazor Server app on purpose: .NET's AesGcm is unsupported in browser
WebAssembly, so the cryptography runs on the server runtime. (The browser demo above
sidesteps this with the Rust/WASM core.)
Public API at a glance
The surface is small on purpose — these are the types you actually touch:
| Type | Package | What it does |
|---|---|---|
PqFileEncryptor |
core | Encrypts files, streams, and bytes with a passphrase or an envelope-key provider. |
PqFileDecryptor |
core | Fail-closed decryption, including DecryptAtomicAsync (all-or-nothing streams). |
PqEncryptionOptions |
core | Immutable options (KDF choice, work factor, chunk size) with WithArgon2id / WithPbkdf2 / WithChunkSize. |
PqProgress |
core | Progress reporting payload for IProgress<PqProgress>. |
IContentKeyProvider / LocalKekContentKeyProvider |
core | Envelope encryption seam (KMS/HSM) and a built-in local-KEK implementation. |
PqDecryptionException / PqFormatException |
core | Fail-closed signals — generic by design, never a decryption oracle. |
PqHybridEncryptor / PqHybridDecryptor |
Hybrid | X25519 + ML-KEM-768 public-key encryption, single- or multi-recipient. |
PqHybridKeyPair / PqHybridPublicKey / PqHybridPrivateKey |
Hybrid | Hybrid recipient key pair; Export() / Import() for storage and transport. |
AddPqFileEncryption() / AddPqHybridFileEncryption() |
DI Extensions | IServiceCollection registration for the encryptor/decryptor pairs. |
Every member is XML-documented; the generated reference lives under Documentation.
Usage
Quick start — encrypt some bytes in memory
using PostQuantum.FileEncryption;
byte[] secret = "meet me at dawn"u8.ToArray();
byte[] container = await new PqFileEncryptor().EncryptBytesAsync(secret, "correct horse battery staple");
byte[] recovered = await new PqFileDecryptor().DecryptBytesAsync(container, "correct horse battery staple");
// recovered.SequenceEqual(secret) == true
That's the whole happy path. Everything below is the same idea for files, streams, and options.
Encrypt and decrypt a file with a passphrase
using PostQuantum.FileEncryption;
await new PqFileEncryptor().EncryptFileAsync("report.pdf", "report.pdf.pqfe", "correct horse battery staple");
await new PqFileDecryptor().DecryptFileAsync("report.pdf.pqfe", "report.restored.pdf", "correct horse battery staple");
Use Argon2id instead of PBKDF2
// Quickest — preset with OWASP-recommended defaults:
await new PqFileEncryptor(PqEncryptionOptions.Argon2id)
.EncryptFileAsync("in", "out.pqfe", passphrase);
// Or tune the work factor (returns a new options instance — leave the others as-is):
var stronger = PqEncryptionOptions.Default.WithArgon2id(memoryKiB: 64 * 1024);
await new PqFileEncryptor(stronger).EncryptFileAsync("in", "out.pqfe", passphrase);
// Decryption needs no options — the KDF and its parameters travel in the container header.
await new PqFileDecryptor().DecryptFileAsync("out.pqfe", "in.copy", passphrase);
PqEncryptionOptions is immutable; WithArgon2id, WithPbkdf2, and WithChunkSize each
return a new instance with the requested change and the rest carried through, so you can
compose them without re-stating every field.
Public-key encryption — use PostQuantum.FileEncryption.Hybrid
using PostQuantum.FileEncryption.Hybrid;
// Recipient generates a key pair once:
using var keyPair = PqHybridKeyPair.Generate();
byte[] publish = keyPair.PublicKey.Export(); // share this freely
// Sender encrypts to the public key — X25519 + ML-KEM-768 combined:
var recipient = PqHybridPublicKey.Import(publish);
byte[] container = await new PqHybridEncryptor().EncryptBytesAsync(secret, recipient);
// Only the holder of the private key can decrypt:
byte[] plaintext = await new PqHybridDecryptor().DecryptBytesAsync(container, keyPair.PrivateKey);
The Hybrid package supports multiple recipients in a single container and a hybrid combiner that keeps the content key safe if either X25519 or ML-KEM is later broken. See Post-quantum & the upgrade path below.
The inline ML-KEM-768-only recipient overloads on
PqFileEncryptor/PqFileDecryptorin the core package are deprecated (PQFE002) and retained for source-compatibility only. Migrate to the Hybrid package shown above.
Detached signatures — use PostQuantum.FileEncryption.Signing
Encryption proves a container wasn't altered; a signature proves who produced it. The
Signing package signs any file or stream with Ed25519 + ML-DSA-65 (FIPS 204) together
and writes a small detached .sig sidecar — unforgeable even if either algorithm is later
broken, and constant-memory for files of any size (streaming SHA-512 pre-hash).
using PostQuantum.FileEncryption.Signing;
using var keyPair = PqSigningKeyPair.Generate();
// Sign the finished container; verification is fail-closed (throws on any mismatch):
await new PqSigner().SignFileAsync("report.pdf.pqfe", "report.pdf.pqfe.sig", keyPair.PrivateKey);
await new PqVerifier().VerifyFileAsync("report.pdf.pqfe", "report.pdf.pqfe.sig", keyPair.PublicKey);
The sidecar format is versioned and byte-exactly specified in
docs/SIGNATURE-FORMAT.md;
the .pqfe v2 container format itself is unchanged and stays FROZEN.
Streams
await using var source = File.OpenRead("video.mp4");
await using var sink = File.Create("video.mp4.pqfe");
await new PqFileEncryptor().EncryptAsync(source, sink, passphrase);
Zeroable passphrase (bytes you control)
byte[] passphrase = GetPassphraseUtf8Bytes();
try
{
await new PqFileEncryptor().EncryptFileAsync("in", "out.pqfe", passphrase); // ReadOnlyMemory<byte> overload
}
finally
{
System.Security.Cryptography.CryptographicOperations.ZeroMemory(passphrase);
}
Synchronous span overload (no async, stack-friendly)
// Useful in CLIs and tight loops; the span is UTF-8 encoded into a temporary buffer
// that is zeroed before this method returns. True sync code path — no deadlock surface.
byte[] container = new PqFileEncryptor().EncryptBytes(plaintext, "correct horse battery staple".AsSpan());
byte[] plaintext = new PqFileDecryptor().DecryptBytes(container, "correct horse battery staple".AsSpan());
Report progress
var progress = new Progress<PqProgress>(p =>
Console.WriteLine($"{p.Fraction:P0} ({p.BytesProcessed:N0} bytes)"));
await new PqFileEncryptor().EncryptFileAsync("big.iso", "big.iso.pqfe", passphrase, progress);
Handle failure (fail-closed)
try
{
await new PqFileDecryptor().DecryptFileAsync("in.pqfe", "out.bin", passphrase);
}
catch (PqDecryptionException) { /* wrong key, or altered/corrupted/truncated — no output written */ }
catch (PqFormatException) { /* not a PostQuantum.FileEncryption container at all */ }
Every authentication failure raises the same generic PqDecryptionException with the same
message — the library never tells an attacker why decryption failed, so it can never act
as a decryption oracle.
All-or-nothing stream decryption
// Writes to `output` only if the WHOLE container authenticates — nothing on a truncated input.
await new PqFileDecryptor().DecryptAtomicAsync(input, output, passphrase);
Prefer this (or the file API, which is atomic via temp-file-plus-rename) for stream input
you don't control: plain DecryptAsync(Stream, Stream, …) writes each chunk as it
authenticates, so a truncated container can emit an authentic plaintext prefix before the
failure is raised (see KNOWN-GAPS.md).
Decrypting untrusted input — cost ceilings
A container's KDF cost and chunk size live in its header and are honored before anything authenticates, so a hostile file could legally demand the format maximum (2 GiB of Argon2id memory) from a few dozen bytes. If you open files from sources you don't control, cap it:
var decryptor = new PqFileDecryptor(PqDecryptionLimits.Untrusted); // or your own ceilings
// Headers demanding more than the limits throw PqFormatException before any KDF work.
await decryptor.DecryptFileAsync("untrusted.pqfe", "out.bin", passphrase);
The default new PqFileDecryptor() keeps the permissive format maxima, so every legal
container still opens.
Envelope encryption (KMS / HSM)
Encrypt under an external key provider so the master key never enters your process. A built-in, dependency-free local-KEK provider is included, and production cloud providers ship as companion packages: PostQuantum.FileEncryption.Aws (AWS KMS) and PostQuantum.FileEncryption.AzureKeyVault (Azure Key Vault / Managed HSM).
using var provider = LocalKekContentKeyProvider.Generate(); // or new(kek)...
byte[] container = await new PqFileEncryptor().EncryptBytesAsync(secret, provider);
byte[] plaintext = await new PqFileDecryptor().DecryptBytesAsync(container, provider);
// ...or keep the master key in AWS KMS / Azure Key Vault — rotation re-wraps the small
// content key; the multi-gigabyte payload is never re-encrypted:
var kmsProvider = new AwsKmsContentKeyProvider(new AmazonKeyManagementServiceClient(), "alias/my-app-key");
await new PqFileEncryptor().EncryptFileAsync("backup.tar", "backup.tar.pqfe", kmsProvider);
Telemetry (SIEM / OpenTelemetry)
The library emits non-sensitive events on an EventSource named
PostQuantum.FileEncryption (operation, KDF/key-source label, byte counts, elapsed time,
failure category — never keys or plaintext). Subscribe via EventListener,
dotnet-trace, EventPipe, or OpenTelemetry. See docs/DEPLOYMENT.md.
Security posture
PostQuantum.FileEncryption is built to be boring and predictable where it matters:
- Authenticated encryption everywhere. Every chunk is sealed with AES-256-GCM. The header (key-establishment parameters, chunk size) and each chunk's ordinal position and final-chunk marker are bound into the authenticated additional data, so reordering, splicing, header tampering, and truncation are all detected as authentication failures.
- Hybrid KEM-DEM for public-key recipients. The Hybrid package combines X25519 and ML-KEM-768 (FIPS 203) via HKDF-SHA256 to derive a key-wrapping key; AES-256-GCM wraps a fresh random content key. The data itself is always AES-256-GCM.
- Unique nonces by construction, fresh key material per file, and no decryption
oracle — every authentication failure raises the same generic
PqDecryptionException. - Bounded work on untrusted input. KDF cost parameters read from a container are
range-checked, so a malicious header cannot force unbounded memory or CPU — and
PqDecryptionLimitslets callers who open untrusted files lower those ceilings further (the buffer for a container of known length is additionally capped by what the container could actually hold). - Key hygiene. Derived keys, wrapped secrets, and private keys are zeroed with
CryptographicOperations.ZeroMemory. - No novel cryptography. Primitives come from .NET's
System.Security.Cryptography, the Konscious Argon2id implementation, and BouncyCastle (for the Hybrid package); this library only composes them in standard patterns.
For deeper references:
- SECURITY.md — supported versions, disclosure process, and the explicit "does NOT defend against" list
- KNOWN-GAPS.md — the honest open-issues ledger
- docs/AUDIT-GUIDE.md — the reviewer's entry point: the ~1,700-line attack surface, the invariants to attack, and how to run the evidence
- docs/THREAT-MODEL.md — assets, adversaries, trust boundaries
- docs/FILE-FORMAT.md — the on-disk container specification
- docs/SIGNATURE-FORMAT.md — the detached
.sigsidecar specification (Ed25519 + ML-DSA-65) - docs/HYBRID-COMBINER.md — the X25519 + ML-KEM-768 combiner, vs. X-Wing / HPKE / RFC 9794
- docs/CONFORMANCE.md — the contract another implementation must meet
- docs/TEST-VECTORS.md — pinned known-answer vectors
- docs/ROADMAP-2.0.md — the candidate feature set for the next container format revision (embedded signatures, metadata protection, SLH-DSA)
Cryptographic software earns trust slowly. This library has not been independently audited; please review the code, the format, and KNOWN-GAPS.md before depending on it. Funded audit engagements are welcome — contact the maintainer. A criteria-by-criteria self-assessment — including what's still missing — is published at docs/GOLD-STANDARD.md.
Post-quantum & the upgrade path
Be clear-eyed about what post-quantum means here today:
- What's stable now: the symmetric, passphrase-based engine. AES-256 is
quantum-resistant for the confidentiality of your data (≈128-bit security under
Grover), so a passphrase-encrypted file is sound against a harvest-now-decrypt-later
adversary. This is the engine being finalized for
1.0. - What's the recommended public-key path: the
PostQuantum.FileEncryption.Hybridpackage — a hybrid X25519 + ML-KEM-768 combiner plus multiple recipients. Fully managed (BouncyCastle for both primitives), so it runs anywhere with no native ML-KEM requirement, and the content key stays safe if either X25519 or ML-KEM is later broken. - What's deprecated: the inline ML-KEM-768-only recipient mode in the core
(
PqKeyPair,PqRecipientPublicKey,PqRecipientPrivateKey, recipient overloads onPqFileEncryptor/PqFileDecryptor). Marked[Obsolete]with diagnostic idPQFE002since1.0.0-rc.2, kept for source-compatibility only. Migrate to the Hybrid package.
dotnet add package PostQuantum.FileEncryption.Hybrid --version 1.4.1
using PostQuantum.FileEncryption.Hybrid;
using var keyPair = PqHybridKeyPair.Generate(); // recipient
byte[] publish = keyPair.PublicKey.Export();
var recipient = PqHybridPublicKey.Import(publish); // sender
byte[] container = await new PqHybridEncryptor().EncryptBytesAsync(secret, recipient);
byte[] plaintext = await new PqHybridDecryptor().DecryptBytesAsync(container, keyPair.PrivateKey);
Design and format details: docs/ROADMAP-v3.md.
Supply chain & verification
Every release tag attaches a CycloneDX SBOM and a SLSA-style build-provenance attestation
to the .nupkg artifacts. The release workflow runs
Meziantou.Framework.NuGetPackageValidation against every produced .nupkg before
nuget push, with the strict icon-must-be-set rule enabled. Coverage-guided fuzzers
(cargo-fuzz + SharpFuzz) run nightly against both parsers with a cached corpus.
Quick verification of any release:
# Verify the build-provenance attestation on a downloaded .nupkg:
gh attestation verify PostQuantum.FileEncryption.1.4.1.nupkg \
--owner systemslibrarian
# Inspect the CycloneDX SBOM bundled with the release:
gh release download v1.4.1 -p 'sbom.core.cdx.json' && jq . sbom.core.cdx.json
# Confirm the conformance vectors decrypt locally:
dotnet test --filter "FullyQualifiedName~KnownAnswerVector|FullyQualifiedName~CrossImplementation"
The full verification recipe — including how to re-run conformance vectors against the Rust/WASM reference implementation — is in docs/SUPPLY-CHAIN.md.
Documentation
| Topic | Doc |
|---|---|
Roadmap (1.0 / 1.x / beyond) |
ROADMAP.md |
| Changelog | CHANGELOG.md |
| Migrating from other libraries (age / libsodium / OpenSSL / .NET) | docs/MIGRATION.md |
| Comparison vs. age / libsodium / OpenSSL | docs/COMPARISON.md |
| Benchmarks (methodology + reproduce-it-yourself) | docs/BENCHMARKS.md |
| Security policy & disclosure | SECURITY.md |
| Threat model (assets, adversaries, audit focus) | docs/THREAT-MODEL.md |
| Auditor's guide (attack surface, invariants, evidence) | docs/AUDIT-GUIDE.md |
| Security reviews (reports + per-finding dispositions) | docs/audits/ |
| Security architecture & crypto inventory (+ FIPS) | docs/SECURITY-ARCHITECTURE.md |
| On-disk container format | docs/FILE-FORMAT.md |
| Detached-signature sidecar format | docs/SIGNATURE-FORMAT.md |
| Hybrid combiner rationale (vs. X-Wing, HPKE, RFC 9794) | docs/HYBRID-COMBINER.md |
| Conformance spec (re-implementer's contract) | docs/CONFORMANCE.md |
| Known-answer test vectors | docs/TEST-VECTORS.md |
| Supply chain (SBOM, attestations, verification) | docs/SUPPLY-CHAIN.md |
| Gold-standard self-assessment (incl. open gaps) | docs/GOLD-STANDARD.md |
| Reproducible builds (verify the .nupkg against the source) | docs/REPRODUCIBLE-BUILDS.md |
| Deployment & hardening | docs/DEPLOYMENT.md |
| Versioning & compatibility policy | docs/VERSIONING.md |
| Key management (KMS/HSM, rotation) — design | docs/KEY-MANAGEMENT.md |
| Hybrid & multi-recipient — design | docs/ROADMAP-v3.md |
| Fuzzing (cargo-fuzz + SharpFuzz + OSS-Fuzz) | docs/FUZZING.md |
| Known gaps (the honest ledger) | KNOWN-GAPS.md |
| Support & lifecycle | SUPPORT.md · Contributing: CONTRIBUTING.md |
API reference (DocFX) is generated from the XML docs — see docfx/.
Performance
Throughput is dominated by two things: the AES-256-GCM data plane (which uses hardware AES and runs at multiple GB/s) and a one-time key derivation per file (a deliberate cost that hardens passphrases). The bigger the file, the more the KDF amortizes.
Indicative end-to-end numbers (16 MiB, including full key establishment), measured with the included BenchmarkDotNet project on one Windows 11 x64 machine in one session — treat as rough, not lab-grade:
| Operation | Key establishment | Approx. throughput |
|---|---|---|
| Encrypt | PBKDF2 (100k) | ~675 MiB/s |
| Decrypt | PBKDF2 (100k) | ~755 MiB/s |
| Encrypt | Argon2id (8 MiB, 1 pass) | ~360 MiB/s |
| Decrypt | Argon2id (8 MiB, 1 pass) | ~550 MiB/s |
| Encrypt | Hybrid (X25519 + ML-KEM-768), 1 recipient | ~955 MiB/s |
| Decrypt | Hybrid, 1 recipient | ~1.16 GiB/s |
The post-quantum "hybrid tax" is sub-millisecond. The entire hybrid key establishment — ML-KEM-768 plus X25519, HKDF, and the key wrap — measures ~0.5 ms per recipient, a fixed per-file cost independent of payload size. Hybrid public-key encryption is faster end-to-end than passphrase mode, because a KEM is cheap while a KDF is expensive on purpose.
Run it yourself (and tune the KDF cost):
dotnet run -c Release --project benchmarks/PostQuantum.FileEncryption.Benchmarks -- --filter '*'
The default PBKDF2 cost is 600,000 iterations (OWASP), so small files are KDF-bound by
design; raise/lower it (or pick Argon2id) via PqEncryptionOptions to trade hardening for
speed.
Full methodology, hybrid/multi-recipient numbers, and how to compare fairly against other tools: docs/BENCHMARKS.md.
Project layout
src/ PostQuantum.FileEncryption — the library (symmetric core)
src/ PostQuantum.FileEncryption.Hybrid — X25519 + ML-KEM-768 hybrid public-key package
src/ PostQuantum.FileEncryption.Extensions.DependencyInjection — IServiceCollection integration
tests/ PostQuantum.FileEncryption.Tests — round-trip, KDF, recipient, hybrid, known-answer, cross-impl, property, fuzz tests
benchmarks/ PostQuantum.FileEncryption.Benchmarks — BenchmarkDotNet throughput suite
samples/ Pqfe.Cli — minimal CLI (encrypt/decrypt; AOT-publishable)
samples/ PostQuantum.FileEncryption.Demo — .NET demo (Blazor Server, runs the library)
samples/ pqfe-wasm — Rust → WASM re-implementation of the .pqfe format
samples/ pqfe-web — fully client-side browser demo (GitHub Pages)
docs/ *.md — format spec, threat model, test vectors, roadmap, supply chain, migration
Why Blazor Server?
A pure client-side WebAssembly demo would be lovely — files would never leave the browser
— but .NET's AesGcm is annotated [UnsupportedOSPlatform("browser")] and throws in
WebAssembly. Rather than ship a demo that breaks the moment you click Encrypt, or quietly
swap in a different (non-library) cipher, the .NET demo runs as Blazor Server so the
real library performs the encryption on the server runtime. Uploaded bytes are held in
memory only and are never persisted. The browser demo (samples/pqfe-web) sidesteps the
problem with a Rust/WASM core that re-implements the format byte-compatibly.
Building from source
dotnet build -c Release
dotnet test -c Release
dotnet pack src/PostQuantum.FileEncryption -c Release
License
MIT.
To God be the glory — 1 Corinthians 10:31.
| 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 was computed. 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)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on PostQuantum.FileEncryption:
| Package | Downloads |
|---|---|
|
PostQuantum.FileEncryption.Hybrid
Open-source (MIT) post-quantum hybrid public-key encryption for PostQuantum.FileEncryption, for .NET 8 and .NET 10. Adds X25519 + ML-KEM-768 (FIPS 203) hybrid recipient encryption and multi-recipient support on top of the FROZEN, publicly specified .pqfe v2 container format — a file's content key stays safe if either primitive is later broken. Fully managed via BouncyCastle for both primitives (no platform ML-KEM dependency); runs anywhere .NET 8 or later does, including Linux, Windows, macOS, and constrained environments. Constant-memory streaming AES-256-GCM data plane via the core package handles files of any size. Public API surface locked by Microsoft.CodeAnalysis.PublicApiAnalyzers; CycloneDX SBOM and SLSA-style build-provenance attestation on every release. Recommended path for new code; supersedes the deprecated inline ML-KEM-only recipient mode (PQFE002) in the core package. |
|
|
PostQuantum.FileEncryption.Signing
Open-source (MIT) post-quantum hybrid detached signatures for PostQuantum.FileEncryption, for .NET 8 and .NET 10. Signs any file or stream — typically a .pqfe container — with Ed25519 + ML-DSA-65 (FIPS 204) together and writes a small detached .sig sidecar, so a signature stays unforgeable if either primitive is later broken. Verification is fail-closed: both signatures must verify or PqSignatureException is thrown, with no oracle distinguishing why. Constant-memory streaming via SHA-512 pre-hash handles files of any size. The sidecar format is versioned and publicly specified (docs/SIGNATURE-FORMAT.md). Fully managed via BouncyCastle (no platform ML-DSA dependency); runs anywhere .NET 8 or later does. Adds sender authenticity on top of the encryption packages: AES-GCM proves a container was not altered, a detached signature proves who produced it. Public API surface locked by Microsoft.CodeAnalysis.PublicApiAnalyzers; CycloneDX SBOM and SLSA-style build-provenance attestation on every release. |
|
|
PostQuantum.FileEncryption.Aws
AWS KMS envelope-key provider for PostQuantum.FileEncryption, for .NET 8 and .NET 10. AwsKmsContentKeyProvider implements the IContentKeyProvider seam over AWS KMS GenerateDataKey/Decrypt: every file is encrypted under a fresh per-file content key that KMS wraps under your customer master key — the master key never leaves AWS. The wrap is bound to the configured key id and a library-specific encryption context, and unwrap fails closed (PqDecryptionException, no oracle) on any invalid or foreign ciphertext. Works with every PqFileEncryptor/PqFileDecryptor overload that accepts a key provider; rotation re-wraps the small content key instead of re-encrypting the file. Public API surface locked by Microsoft.CodeAnalysis.PublicApiAnalyzers; CycloneDX SBOM and SLSA-style build-provenance attestation on every release. |
|
|
PostQuantum.FileEncryption.AzureKeyVault
Azure Key Vault / Managed HSM envelope-key provider for PostQuantum.FileEncryption, for .NET 8 and .NET 10. AzureKeyVaultContentKeyProvider implements the IContentKeyProvider seam over Key Vault wrap/unwrap (RSA-OAEP-256 by default): every file is encrypted under a fresh per-file content key wrapped by your Key Vault key — the key-encryption key never leaves the vault or HSM. Unwrap is pinned to the configured key id and algorithm and fails closed (PqDecryptionException, no oracle) on any invalid or foreign wrapped key. Works with every PqFileEncryptor/PqFileDecryptor overload that accepts a key provider; rotation re-wraps the small content key instead of re-encrypting the file. Public API surface locked by Microsoft.CodeAnalysis.PublicApiAnalyzers; CycloneDX SBOM and SLSA-style build-provenance attestation on every release. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.4.1 | 328 | 6/13/2026 |
| 1.4.0 | 322 | 6/13/2026 |
| 1.3.0 | 465 | 6/13/2026 |
| 1.2.1 | 346 | 6/12/2026 |
| 1.2.0 | 310 | 6/12/2026 |
| 1.1.0 | 381 | 6/10/2026 |
| 1.0.1 | 299 | 6/6/2026 |
| 1.0.0 | 443 | 6/6/2026 |
| 1.0.0-rc.3 | 68 | 6/4/2026 |
| 1.0.0-rc.2 | 70 | 6/2/2026 |
| 1.0.0-rc.1 | 83 | 5/31/2026 |
| 0.2.0 | 388 | 5/31/2026 |
1.4.1 — documentation and packaging patch. Corrects the package README install snippets and version references that still cited 1.3.0 after the 1.4.0 bump, so the version shown on nuget.org now matches the package. No change to code, public API, or the .pqfe v2 container format, which remains FROZEN for the 1.x line. See CHANGELOG.md.