PostQuantum.FileEncryption is security-sensitive software. This document describes what it defends against, what it does not, and how to report a problem. It is written to be honest rather than reassuring.
The symmetric, passphrase-based engine is production-ready. The .pqfe v2 container format
is frozen for the 1.x line — every byte is pinned by published conformance vectors, and
any incompatible change requires a 2.0 major version.
| Version | Supported | Notes |
|---|---|---|
1.x (current: 1.7.1) |
✅ | Current line. Security fixes land here. |
0.x |
❌ | Pre-1.0. Format was not yet frozen; please upgrade. |
A file produced by any 1.x build is readable by every other 1.x build. There is no
0.x ↔ 1.x format migration path; re-encrypt with a 1.x build instead.
Please report suspected vulnerabilities privately:
- Use GitHub Security Advisories ("Report a vulnerability") on the repository, or
- email the maintainer at systemslibrarian@gmail.com with
SECURITYin the subject.
Please include a description, affected version, and a minimal reproduction if you can. We aim to acknowledge within 5 business days. Please do not open a public issue for a security report until a fix is available and coordinated.
- Confidentiality of file contents against an attacker who obtains the ciphertext but not the key, using AES-256-GCM with a unique per-file content key.
- Integrity and authenticity. Any modification of the authenticated envelope — the header, every ciphertext chunk, and the framing through the final frame — is detected and rejected. The authenticated additional data binds the key-establishment parameters, the chunk ordinal, and the final-chunk marker, defeating bit-flipping, header tampering, chunk reordering, splicing between containers, and truncation. (Bytes appended after the authenticated final frame are outside the envelope and are ignored, not rejected — see KNOWN-GAPS.md.)
- Quantum-resistant confidentiality of the data, via AES-256 (≈128-bit security against
a Grover attacker). For quantum-resistant key establishment with a hybrid combiner that
remains secure if either primitive is later broken, use the companion
PostQuantum.FileEncryption.Hybridpackage (X25519 + ML-KEM-768, multi-recipient). - Bounded work on untrusted input. KDF cost parameters carried in a container (PBKDF2
iterations, Argon2id memory/iterations) are range-checked against the format's maxima
before use, so a malicious header cannot force unbounded memory or CPU — it fails closed
as a
PqFormatException. The format maxima are still substantial (up to 2 GiB of Argon2id memory or 100M PBKDF2 iterations) and the default limits accept every legal container, so callers decrypting untrusted input should passPqDecryptionLimits.Untrusted(or custom ceilings) for tight bounds. - No decryption oracle. Every authentication failure raises the same exception type,
PqDecryptionException, and every key-dependent failure (wrong passphrase vs. tampered ciphertext) carries one identical message — the library never distinguishes "wrong key" from "tampered data" at the public surface, and tests pin those messages byte-identical. This holds across stages as well: a recipient/KEK unwrap failure and a body-authentication failure throw the identical message with no distinguishing inner exception, so a service exposing raw error text does not reveal whether its key could unwrap a submitted container. Structural failures that an attacker can already compute from the ciphertext alone (truncation, corrupt framing) carry distinct but key-independent diagnostics, and cloud-KMS providers surface their own provider-specific operational errors — see KNOWN-GAPS.md before surfacing exception text across a trust boundary. - No partial output on failure. File APIs stage every byte to a sibling temp file and
only
File.Moveit into place on full success. Stream callers can opt into the same all-or-nothing guarantee viaDecryptAtomicAsync.
This list is intentionally explicit. See KNOWN-GAPS.md for the open ledger.
- Weak passphrases. PBKDF2 and Argon2id raise the cost of guessing, but cannot rescue a weak secret. Argon2id (memory-hard) is the stronger choice for human-chosen passphrases.
- Metadata confidentiality. The container does not hide the approximate plaintext size and does not encrypt file names or paths. Chunk boundaries reveal a coarse size.
- Endpoint compromise. A compromised machine or a keylogged passphrase defeats any file
encryptor. Passphrases passed as
stringmay be retained by the runtime beyond our control; use theReadOnlyMemory<byte>orReadOnlySpan<char>overloads to keep secrets in memory you can zero. - Side-channel resistance beyond the platform's. We rely on the underlying
implementations (.NET
System.Security.Cryptography, Konscious Argon2id) for constant-time behavior. - Independent audit. This code has not been independently audited. Funded engagements are welcome — please contact the maintainer.
The core package still carries an inline ML-KEM-768-only recipient mode for
source-compatibility. It is marked [Obsolete] with diagnostic id PQFE002 as of
1.0.0-rc.2 and is retained only so existing callers still build. It is not a hybrid
combiner — it relies on ML-KEM-768 alone for the asymmetric step, and its security collapses
to the symmetric core if ML-KEM is later weakened. New code must use
PostQuantum.FileEncryption.Hybrid (X25519 + ML-KEM-768 combiner, multi-recipient, fully
managed, runs anywhere). Removal of the inline mode is targeted for a future major release.
Konscious.Security.Cryptography.Argon2provides the Argon2id KDF. It is a widely used managed implementation but is not formally audited; this is noted in KNOWN-GAPS.md. The default KDF (PBKDF2) uses only the .NET runtime.- All symmetric primitives, HKDF, and ML-KEM (when used by the deprecated inline mode) come
from .NET's
System.Security.Cryptography. - The Hybrid package additionally uses
BouncyCastle.Cryptographyfor X25519 and ML-KEM, selected so the package runs on every .NET 10 platform without a native ML-KEM dependency. - The Signing package uses
BouncyCastle.Cryptographyfor Ed25519 and ML-DSA-65, under the same fully-managed rationale as the Hybrid package. - The Rust/WASM demo core (
samples/pqfe-wasm— a byte-compatibility reference, not a shipped NuGet package) uses the pre-1.0 RustCrypto crates (aes-gcm,ml-kem,argon2,pbkdf2,hkdf,sha2), which are not independently audited; its lockfile is committed andcargo-auditruns in CI.
- No homegrown cryptography. Primitives are composed in standard patterns (KEM-DEM, password hashing, AEAD) only.
- Authenticated encryption only. There is no unauthenticated mode.
- Fail closed. On any doubt about authenticity, the library throws and emits no plaintext. There is no partial-success path and no error oracle.
- Transparent format. The container is fully specified in docs/FILE-FORMAT.md and pinned by cross-implementation known-answer test vectors, so it can be reviewed and re-implemented independently.
Every release tag publishes:
- CycloneDX SBOMs, one per package (
sbom.core.cdx.json,sbom.hybrid.cdx.json,sbom.signing.cdx.json, the three cloud providers, DI extensions, Analyzers, and the tool — see docs/SUPPLY-CHAIN.md), - a SLSA-style build-provenance attestation over the
.nupkgartifacts (verifiable withgh attestation verify), - the
.nupkgfiles themselves (also published to nuget.org).
See docs/SUPPLY-CHAIN.md for verification commands.
To God be the glory — 1 Corinthians 10:31.