Skip to content

systemslibrarian/postquantum-file-encryption

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

PostQuantum.FileEncryption

CI CodeQL OpenSSF Scorecard codecov NuGet NuGet Hybrid License: MIT

A high-level, delightful, fail-closed way to encrypt files and streams on .NET.

PostQuantum.FileEncryption gives you two friendly classes — PqFileEncryptor and PqFileDecryptor — that handle authenticated, chunked, streaming encryption with strong, modern defaults. You should not have to read a cryptographic spec to protect a file. You just call a method, and the library does the careful, paranoid, fail-closed thing every time.

Status: 1.0.0-rc.2. The symmetric, passphrase-based engine is production-ready and thoroughly tested (106 tests, continuous fuzzing, cross-checked Rust/WASM byte-compatibility, AOT-published smoke test, OpenSSF Scorecard, public-API baseline locked by analyzer). The on-disk .pqfe container format is now FROZEN at v2 for the 1.x line: every byte is pinned by published conformance vectors, and any incompatible change requires a 2.0 major version. Post-quantum public-key (recipient) encryption ships as the separate PostQuantum.FileEncryption.Hybrid package (X25519 + ML-KEM-768, fully managed); see Post-quantum & the upgrade path and KNOWN-GAPS.md.


▶ Try it

Three ways to drive the library — all produce the same .pqfe format:

1. Command-line — runs the library natively (also AOT-publishable)

samples/Pqfe.Cli is a tiny pqfe encrypt | decrypt binary 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 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). So 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 AES-GCM is unsupported in browser WebAssembly, so the cryptography runs on the server runtime. (See "Why Blazor Server?" — and note the browser demo above sidesteps this with a Rust/WASM core.)


What it does

The stable core is a symmetric, passphrase-based file encryptor. Post-quantum public-key encryption is on the roadmap as a separate package — see Post-quantum & the upgrade path below.

  • Passphrase-based encryption (the stable, recommended path) with your choice of key-derivation function:
    • PBKDF2-HMAC-SHA256 (default, no extra cost), 600,000 iterations.
    • Argon2id (memory-hard, opt-in), defaulting to OWASP's 19 MiB / 2-pass setting.
  • AES-256-GCM for all data encryption — an authenticated cipher whose 256-bit key stays strong against a quantum adversary (Grover's algorithm only halves the effective strength).
  • Chunked streaming so you can encrypt a 50 GB file with bounded memory — with optional progress reporting.
  • Fail-closed by construction. A wrong key, a flipped bit, a spliced or truncated container, a hostile header demanding gigabytes of KDF memory — all rejected with a PqEncryptionException, and no plaintext is ever written to your destination.
  • Atomic file output. File operations write to a temporary sibling file and are moved into place only on full success.
  • Zeroable secrets. Passphrases can be passed as bytes you control and zero yourself; derived keys and private keys are wiped from memory after use.

Install

dotnet add package PostQuantum.FileEncryption --version 1.0.0-rc.2

Targets .NET 10 (net10.0). Depends on Konscious.Security.Cryptography.Argon2 for the Argon2id KDF; everything else is from .NET's System.Security.Cryptography.


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.

Encrypt to a recipient's public key (DEPRECATED — use PostQuantum.FileEncryption.Hybrid)

Deprecated in 1.0.0-rc.2 (PQFE002). The inline ML-KEM-768-only recipient mode in the core remains for source-compatibility but emits a deprecation warning. For new code use PostQuantum.FileEncryption.Hybrid — X25519 + ML-KEM-768 hybrid combiner, multi-recipient, fully managed (no platform ML-KEM requirement). The example below is preserved for migrators; new readers should skim it and skip to the Hybrid section.

if (PqKeyPair.IsSupported)
{
    using var keyPair = PqKeyPair.Generate();      // the recipient does this once
    byte[] publish   = keyPair.PublicKey.Export(); // share this freely

    // Anyone with the public key can encrypt:
    var recipient = PqRecipientPublicKey.Import(publish);
    await new PqFileEncryptor().EncryptFileAsync("secret.bin", "secret.pqfe", recipient);

    // Only the holder of the private key can decrypt:
    await new PqFileDecryptor().DecryptFileAsync("secret.pqfe", "secret.out", keyPair.PrivateKey);
}

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);
}

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 */ }

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);

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; cloud providers (AWS KMS, Azure Key Vault, …) implement the same IContentKeyProvider interface in separate packages.

using var provider = LocalKekContentKeyProvider.Generate();   // or new(kek), or a KMS-backed provider
byte[] container = await new PqFileEncryptor().EncryptBytesAsync(secret, provider);
byte[] plaintext = await new PqFileDecryptor().DecryptBytesAsync(container, provider);

See docs/KEY-MANAGEMENT.md.

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 recipients. ML-KEM-768 encapsulates a shared secret; HKDF-SHA256 derives 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.
  • 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 and the Konscious Argon2id implementation; this library only composes them.

For the reporting process and current limitations, read SECURITY.md and KNOWN-GAPS.md. Deeper references:

Cryptographic software earns trust slowly. This is a preview, and it has not been independently audited. Please review the code, the format, and the gaps before depending on it.


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 release.
  • What's deprecated: the inline ML-KEM-768-only recipient mode in the core (PqKeyPair, PqRecipientPublicKey, PqRecipientPrivateKey, recipient overloads on PqFileEncryptor/PqFileDecryptor). Marked [Obsolete] with diagnostic id PQFE002 in 1.0.0-rc.2, kept for source-compatibility only. Migrate to the Hybrid package below.
  • What's shipped for public-key: the PostQuantum.FileEncryption.Hybrid package — a hybrid X25519 + ML-KEM-768 combiner plus multiple recipients. It's 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.
dotnet add package PostQuantum.FileEncryption.Hybrid --version 1.0.0-rc.2
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);

So: passphrase encryption is the stable core; hybrid public-key encryption is the recommended public-key path via the Hybrid package; the core's inline ML-KEM-only recipient mode is deprecated (PQFE002) and retained only for source-compatibility. Design and format details: docs/ROADMAP-v3.md.


Documentation

Topic Doc
Roadmap (now / v0.2 / v0.3 / 1.0) ROADMAP.md
Changelog CHANGELOG.md
Security policy & disclosure SECURITY.md
Threat model (assets, adversaries, audit focus) docs/THREAT-MODEL.md
Security architecture & crypto inventory (+ FIPS) docs/SECURITY-ARCHITECTURE.md
On-disk container format docs/FILE-FORMAT.md
Conformance spec (re-implementer's contract) docs/CONFORMANCE.md
Known-answer test vectors docs/TEST-VECTORS.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
Comparison vs age / libsodium / OpenSSL docs/COMPARISON.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 one KDF derivation), measured with the included BenchmarkDotNet project on a shared GitHub Codespace — treat as rough, not lab-grade:

Operation KDF Approx. throughput
Encrypt PBKDF2 (100k) ~210 MiB/s
Decrypt PBKDF2 (100k) ~300 MiB/s
Encrypt Argon2id (8 MiB, 1 pass) ~390 MiB/s
Decrypt Argon2id (8 MiB, 1 pass) ~450 MiB/s

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.


Project layout

src/        PostQuantum.FileEncryption        — the library (symmetric core)
src/        PostQuantum.FileEncryption.Hybrid — X25519 + ML-KEM-768 hybrid public-key package
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, and more

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 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. A fully client-side build would require a browser-native crypto core (WebCrypto or a WASM crypto module) that re-implements this container format — tracked as a possible future sample in KNOWN-GAPS.md.

Building from source

dotnet build -c Release
dotnet test  -c Release

License

MIT.


To God be the glory — 1 Corinthians 10:31.

About

Simple, high-security post-quantum file encryption library for .NET 10. Built on PostQuantum.FileFormat with passphrase and hybrid support.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages