SecureFileUpload.Core
1.0.0-preview.2
See the version list below for details.
dotnet add package SecureFileUpload.Core --version 1.0.0-preview.2
NuGet\Install-Package SecureFileUpload.Core -Version 1.0.0-preview.2
<PackageReference Include="SecureFileUpload.Core" Version="1.0.0-preview.2" />
<PackageVersion Include="SecureFileUpload.Core" Version="1.0.0-preview.2" />
<PackageReference Include="SecureFileUpload.Core" />
paket add SecureFileUpload.Core --version 1.0.0-preview.2
#r "nuget: SecureFileUpload.Core, 1.0.0-preview.2"
#:package SecureFileUpload.Core@1.0.0-preview.2
#addin nuget:?package=SecureFileUpload.Core&version=1.0.0-preview.2&prerelease
#tool nuget:?package=SecureFileUpload.Core&version=1.0.0-preview.2&prerelease
secure-file-upload-dotnet
Defense-in-depth file upload pipeline for ASP.NET Core 10+ — AES-256-GCM envelope encryption with Argon2id key derivation
An 8-layer file upload validation and storage pipeline derived from a production ASP.NET Core document-intake workflow. The code has been de-branded, generalized, and published for reuse — it is the same pipeline structure used in production, not a toy example.
The goal of this repo is to show what a measured, fail-closed upload pipeline looks like in real C#: every claim below is backed by code in src/, and every known limitation is documented in KNOWN-GAPS.md.
SECURITY-ANALYSIS.md records a structured adversarial AI red-team review of this exact code — original findings, current resolution status, and the residual gaps that remain.
"So whether you eat or drink or whatever you do, do it all for the glory of God." — 1 Corinthians 10:31
Why This Exists
Secure file upload is one of the most consistently mishandled areas in web development. Most tutorials show you how to receive a file. Very few show you how to defend against:
- Polyglot files (valid image + embedded executable)
- Double-extension attacks (
photo.pdf.exe) - MIME spoofing
- Magic-byte forgery
- Path traversal via filename manipulation
- PDF JavaScript injection
- ZIP bomb / pixel flood attacks
- Log poisoning via crafted filenames
- Disk exhaustion attacks
This codebase addresses all of them, and the red-team analysis tells you where it still falls short.
The 8-Layer Validation Pipeline
Every uploaded file passes through all layers in order. Failure at any validation layer rejects the file immediately. The pipeline is fail-closed on every content decision — unknown types, malformed files, and validation exceptions all result in rejection. The single deliberate exception is virus-scanner availability (Layer 7), which is fail-open by design and explicitly tracked; see Key Design Decisions below.
┌─────────────────────────────────────────────────────────────────┐
│ INCOMING FILE UPLOAD │
└─────────────────────────┬───────────────────────────────────────┘
│
┌───────────────▼────────────────┐
│ Layer 1: File Size Check │ Rejects oversized files before any buffering
│ (per-file and total batch) │ Also enforces minimum size per format
└───────────────┬────────────────┘
│
┌───────────────▼────────────────┐
│ Layer 2: Extension Allowlist │ Strict allowlist: .jpg .jpeg .png .webp .pdf
│ │ Everything else is rejected
└───────────────┬────────────────┘
│
┌───────────────▼────────────────┐
│ Layer 3: MIME + Extension │ Browser-reported MIME must match extension
│ Cross-Validation │ Catches extension-spoofed uploads
└───────────────┬────────────────┘
│
┌───────────────▼────────────────┐
│ Layer 4: Magic Bytes │ File signature read from actual bytes
│ (File Signature Check) │ Not from filename or Content-Type header
└───────────────┬────────────────┘
│
┌───────────────▼────────────────┐
│ Layer 5: Filename Inspection │ Double-extension, Unicode tricks,
│ │ path traversal, reserved names (NUL, COM1...)
└───────────────┬────────────────┘
│
┌───────────────▼────────────────┐
│ Layer 6: Deep Content │ Format-specific structural walking:
│ Validation (FileContentValidator) │ JPEG segment walker, PNG chunk walker,
│ │ WebP RIFF tree, PDF pattern scan,
│ │ PDF FlateDecode stream inspection.
│ │ Detects embedded executables, scripts,
│ │ JavaScript in PDF, dangerous PDF objects
└───────────────┬────────────────┘
│
┌───────────────▼────────────────┐
│ Layer 7: Virus Scan │ Windows Defender (Windows) OR
│ (IVirusScanService) │ ClamAV via clamd zINSTREAM (Linux/cross-platform).
│ │ Fail-closed on signature hit (infected → reject).
│ │ Fail-open on scanner availability (timeout/down →
│ │ accept as NotScanned; tracked in result, never
│ │ silently "clean"). Only runs when VirusScan:Enabled=true.
└───────────────┬────────────────┘
│
┌───────────────▼────────────────┐
│ Layer 8: Encrypted Storage │ AES-256-GCM envelope encryption (v2):
│ │ per-file random DEK wrapped under master KEK.
│ │ Image recompression strips polyglot tails.
│ │ Randomized filename, outside wwwroot,
│ │ path traversal re-checked before write
└───────────────┴────────────────┘
Source Files
src/FileUploadService.cs— Orchestrates the full 8-layer pipeline. Handles batch limits, disk capacity checks, image recompression (Gap 1 mitigation), envelope-encrypted write (v2), decryption for retrieval, log-poisoning-safe filename handling.src/FileContentValidator.cs— Layer 6 deep content validation. Format-specific structural walking for JPEG, PNG, WebP, PDF. Pattern-based threat detection. FlateDecode-compressed PDF stream inspection (Gap 2 mitigation). Fail-closed on unknown types.src/WindowsDefenderScanService.cs— Layer 7 virus scanning via Windows DefenderMpCmdRun.exe. Includes temp-file secure delete (zero-before-delete). Use on Windows.src/ClamAvScanService.cs— Layer 7 virus scanning viaclamdover TCP using thezINSTREAMprotocol. No temp file written — patron bytes never touch disk. Use on Linux / containers / macOS.src/SecureFileDownloadController.cs— Reference staff-side download handler. ForcesContent-Disposition: attachment, locks down response headers (CSPsandbox,nosniff,X-Frame-Options: DENY, COOP/COEP/CORP, no-store), and re-checks path traversal at read time.src/ReplacementCardInputModel.cs— Example model showing how file uploads are bound viaList<IFormFile>in a multipart form alongside validated patron fields.tests/Fuzz/— SharpFuzz + AFL++ harness forFileContentValidator.ValidateAsync. Catches unhandled exceptions, hangs, and runaway allocation in attacker-crafted inputs. Seetests/Fuzz/README.md.
Key Design Decisions (and Why)
Fail-Closed on Content Decisions
Unknown file types, malformed structures, deep-validation exceptions, missing or placeholder encryption secrets, and storage paths inside wwwroot all result in rejection or refusal to start. The default for any content decision is deny, not allow.
The one deliberate exception is virus-scanner availability (Layer 7). A scanner that returns infected always rejects the file. A scanner that is unreachable, times out, or throws is treated as fail-open with explicit NotScanned tracking — the file is accepted only because Layers 1–6 have already cleared it, and the outcome is surfaced in FileUploadResult.ScanNotScannedCount and logged as VIRUS_SCAN_OPERATIONAL_FAILURE. This is documented in KNOWN-GAPS.md and is the right trade-off for a patron-document workflow where a clamd outage must not block legitimate registrations; deployments that need scanner-availability to be hard-blocking should switch to a queued-scan model (see docs/hardening-roadmap.md §1.3).
Signature-First Classification (FileContentValidator)
The deep validator detects the actual file type from magic bytes before dispatching to the format-specific validator. A file claiming to be .jpg that opens with %PDF gets caught as a type mismatch before any format-specific logic runs.
Extension ↔ MIME Cross-Validation (Layer 3)
The browser-reported Content-Type header is validated against the claimed extension. A .pdf file arriving with image/jpeg MIME is rejected. Neither the extension nor the MIME type is trusted independently.
Storage Outside wwwroot (Layer 8)
The storage root is validated at construction time to be outside wwwroot. If someone misconfigures the path to resolve inside the web root (where files would be directly servable), the application refuses to start. This is enforced at the IWebHostEnvironment level, not just as documentation.
Randomized Filenames
Files are stored as {sanitizedLastName}{dateStamp}{formType}Doc{n}{randomSuffix}.ext. The original filename is never used on disk. This prevents filename-based path traversal and removes any attacker control over the final storage path.
AES-256-GCM with Argon2id (Envelope Encryption)
When encryption is enabled, files are stored using envelope encryption (format v2):
- A fresh random 256-bit Data Encryption Key (DEK) is generated per file.
- The file payload is encrypted under the DEK with AES-256-GCM.
- The DEK itself is then wrapped (encrypted) under the master Key Encryption Key (KEK), which is derived from
EncryptionSecretvia Argon2id — the memory-hard KDF recommended by OWASP and RFC 9106 for password-based key derivation in 2026. - Layout on disk:
marker || dek_nonce || dek_tag || wrapped_dek || file_nonce || file_tag || ciphertext.
This means rotating the master key requires only re-wrapping each file's DEK — the file payloads themselves don't need to be re-encrypted. Legacy single-key v1 files and v2 files wrapped under older PBKDF2-derived KEKs remain readable for backward compatibility (see Implementation & Crypto Posture). The application refuses to start if EncryptionEnabled=true but the secret is missing or still set to the placeholder.
Image Recompression (Polyglot Defence)
When FileUpload:RecompressImages=true (default), JPEG / PNG / WebP uploads are decoded and re-encoded through ImageSharp before encryption. This strips any data appended after the image's structural end (the polyglot vector — a JPEG that's also a valid PHP/EXE). PDFs and other formats are untouched.
FlateDecode-compressed PDF Stream Inspection
The PDF validator walks every stream … endstream block, attempts DeflateStream decompression, and re-runs the dangerous-pattern scan against the decompressed bytes. This catches /JavaScript, /Launch, etc. hidden inside compressed object streams. Bounded by MaxCompressedStreamsToInspect and MaxDecompressedStreamBytes for zip-bomb safety.
Log-Poisoning-Safe Filename Handling
Every attacker-controlled filename is run through SanitizeForLog before being written to logs or echoed in user-facing error messages. Strips ANSI escape sequences, control characters, structured-log placeholders ({, }, |), CRLF, and Unicode bidi/zero-width tricks.
Cross-Platform Virus Scanning
IVirusScanService has two production implementations:
WindowsDefenderScanService— invokesMpCmdRun.exe; requires Windows.ClamAvScanService— talks toclamddirectly over TCP using the documentedzINSTREAMprotocol. No temp file is written. Cross-platform (Linux, macOS, containers).
Detection is fail-closed: any clear malware signature rejects the upload. Operational failures (timeout, daemon down, unparseable response) are fail-open with explicit NotScanned tracking — the file is accepted only because it already passed Layers 1–6, and the outcome is counted in FileUploadResult.ScanNotScannedCount and emitted as a VIRUS_SCAN_OPERATIONAL_FAILURE log event. The result is never silently relabelled as "clean".
Hardened Download Surface (SecureFileDownloadController)
Serving decrypted patron documents safely is a separate problem from accepting them safely. The reference download controller:
- Re-checks path traversal at read time (defence in depth on top of upload-time check).
- Forces every response to
Content-Type: application/octet-stream+Content-Disposition: attachmentso the browser cannot render the file inline — PDFs never invoke Adobe Reader, images never get MIME-sniffed as HTML. - Sends a strict header set:
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Content-Security-Policy: default-src 'none'; … sandbox,Cache-Control: no-store, private,Cross-Origin-{Resource,Opener,Embedder}-Policy,Referrer-Policy: no-referrer, restrictivePermissions-Policy. - Encodes the filename via
ContentDispositionHeaderValue.SetHttpFileName(RFC 6266 UTF-8) to defeat header injection.
Wire it under an authenticated, MFA-gated staff route. Do not expose it anonymously.
Secure Temp File Deletion (WindowsDefenderScanService)
The virus scanner writes files to a temp directory for scanning. After scanning, the temp file is overwritten with zeros before deletion. This reduces (though does not guarantee) recovery of sensitive content from freed disk sectors.
ArrayPool + Buffer Zeroing (FileContentValidator)
Content validation uses ArrayPool<byte> for the read buffer. The buffer is zeroed before being returned to the pool to prevent patron document content (IDs, utility bills) from leaking into subsequent requests.
PathHelper.IsPathUnderBase (Not StartsWith)
Path traversal checks use a proper base-path check rather than string.StartsWith. The StartsWith approach has a well-known prefix confusion bug: /uploads_evil matches /uploads when doing naive prefix checking. The helper uses canonicalized paths and directory separator boundary checks.
Implementation & Crypto Posture
This section states plainly what crypto primitives are used today, what the parameters are, and what the honest residual risks are. Marketing copy is elsewhere; this section is for the security engineer reviewing whether to adopt the library.
| Aspect | Current implementation (v1.0.0) | Notes |
|---|---|---|
| Symmetric encryption | AES-256-GCM via System.Security.Cryptography.AesGcm |
96-bit nonce, 128-bit auth tag — both NIST SP 800-38D / RFC 5288 recommendations. |
| Encryption scheme | Envelope (v2) — per-file random 256-bit DEK wrapped under a master KEK | Compromise of one wrapped DEK does not reveal the KEK; KEK rotation rewraps DEKs without re-encrypting payloads. |
| Key derivation (KEK) | Argon2id via Konscious.Security.Cryptography.Argon2 1.3.x |
Memory-hard, GPU/ASIC-resistant. OWASP & RFC 9106 recommendation for 2026. |
| Argon2id parameters | m=64 MiB, t=3, p=4, salt = "SecureFileUpload.kdf.argon2id.v1" (16+ bytes, fixed) |
Above OWASP server-side minimum (m=19 MiB, t=2). Targets ~250–500 ms on a modern x64 server core. |
| Salt | Fixed application salt (not per-record) | KEK is one-per-deployment, so a per-record salt is not applicable. The salt is in source — the secret must be in a real secret store, not committed config. |
| RNG | RandomNumberGenerator (CSPRNG) for DEKs, nonces, filename suffixes |
No use of System.Random, Guid.NewGuid(), or DateTime.Ticks in any security-sensitive path. |
| Storage marker | ENCGCM\0\x02 (v2 envelope) |
0x01 legacy single-key files remain decrypt-capable. |
| Backward compatibility | Legacy PBKDF2-SHA256 KEKs (600 000 and 210 000 iterations) are still tried during decryption only |
On a _legacyKekFallback=true (default) deployment, files encrypted before the Argon2id upgrade remain readable. New writes always use the Argon2id KEK. |
| Buffer hygiene | Plaintext, DEK, and KEK buffers are zeroed via CryptographicOperations.ZeroMemory after use |
Not a guarantee against GC copies, but reduces the in-memory exposure window. |
| TLS / transport | Not provided by this library | Enforce HSTS and HTTPS at the infrastructure / Kestrel level. |
| At-rest device encryption | Not provided by this library | BitLocker / LUKS / dm-crypt is still strongly recommended on the storage volume. |
| FIPS posture | Not FIPS-validated. Argon2id is not part of FIPS 140-3 ASMs as of 2026 | Deployments that require FIPS-only primitives should configure KeyDerivation: Algorithm = Pbkdf2 (see below) and consult their compliance officer. |
Honest limitations
- Argon2id is not in FIPS 140-3. If your environment mandates FIPS-validated primitives, the library lets you opt into PBKDF2-SHA256 with a configurable iteration count via
FileUpload:KeyDerivation:Algorithm = "Pbkdf2". Set this knowingly. - The salt is in source. It is the same value for every deployment of this version of the library. The protection model assumes the secret is in a real secrets manager (Key Vault, AWS Secrets Manager, env var injected by the platform). Committing the secret to
appsettings.jsondefeats the KDF regardless of which algorithm is selected. - The KEK lives in process memory. A memory-dump-capable attacker on the host can read it. Mitigations are deployment-level (least privilege, ASLR, sealed VMs, confidential compute) — not in scope for this library.
- Argon2id parameters are CPU/RAM-bound, not wall-clock-pinned. On a constrained container (small VM, low-memory worker), startup KEK derivation may take longer than the ~250–500 ms target. The library logs the elapsed milliseconds so you can tune
MemoryKiB/Iterations/Parallelismfor your hardware. - No support for hardware HSM / KMS in v1. A KMS-backed KEK is the right next step for high-assurance deployments; tracked in
docs/hardening-roadmap.md.
See SECURITY-ANALYSIS.md for the full code-traced security review, and KNOWN-GAPS.md for things this pipeline does not protect against.
Configuration Reference
{
"FileUpload": {
"StorageRoot": "../uploads",
"MaxFileSizeBytes": 10485760,
"MaxFileCount": 5,
"MaxTotalUploadBytes": 52428800,
"MinStorageFreeBytes": 536870912,
"MinTempFreeBytes": 536870912,
"LowDiskWarningBytes": 2147483648,
"RecompressImages": true,
"JpegRecompressQuality": 95,
"EncryptionEnabled": false,
"EncryptionSecret": "CHANGE_THIS_TO_A_REAL_SECRET_MINIMUM_32_CHARS",
"KeyDerivation": {
"Algorithm": "Argon2id",
"Argon2id": {
"MemoryKiB": 65536,
"Iterations": 3,
"Parallelism": 4
},
"Pbkdf2": {
"Iterations": 600000
},
"LegacyKekFallback": true
}
},
"FileContent": {
"InspectCompressedPdfStreams": true,
"MaxCompressedStreamsToInspect": 64,
"MaxDecompressedStreamBytes": 16777216,
"RejectEncryptedPdfs": true,
"RejectInteractivePdfs": false,
"MaxImageWidth": 10000,
"MaxImageHeight": 10000,
"MaxImagePixels": 40000000
},
"VirusScan": {
"Enabled": false,
"WindowsDefender": {
"MpCmdRunPath": "C:\\Program Files\\Windows Defender\\MpCmdRun.exe",
"TempScanPath": "C:\\Temp\\VirusScan",
"TimeoutSeconds": 30
},
"ClamAv": {
"Host": "localhost",
"Port": 3310,
"TimeoutSeconds": 30,
"MaxStreamBytes": 26214400
}
}
}
StorageRoot is resolved relative to ContentRootPath (not wwwRootPath). A relative path like ../uploads is typical to ensure it lands outside the web root.
EncryptionSecret must be at least 32 characters and must not contain the string CHANGE_THIS. If EncryptionEnabled is true and the secret fails this check, the application will not start. Treat the secret like any other production credential — store it in a secrets manager (Azure Key Vault, AWS Secrets Manager, environment variable injected by the platform), never in checked-in config.
KeyDerivation:Algorithm defaults to Argon2id. Set it to Pbkdf2 only if you are bound by a compliance regime (e.g., strict FIPS 140-3) that disallows Argon2id. Pbkdf2 uses SHA-256 and the Iterations value from configuration.
KeyDerivation:Argon2id parameters target ~250–500 ms KEK derivation on a modern x64 server core at startup. On constrained containers, tune MemoryKiB / Iterations / Parallelism to fit your CPU and RAM budget; the library logs the elapsed derivation time so you can measure.
KeyDerivation:LegacyKekFallback (default true) keeps PBKDF2-SHA256 fallback KEKs available for decryption only. This lets you upgrade from earlier preview versions of this library without re-encrypting every file on disk. New writes always use the configured primary algorithm. Set to false once all files have been re-wrapped under an Argon2id KEK.
RecompressImages defaults to true. Set it to false only if byte-exact preservation of patron-uploaded images is a hard requirement (you accept the polyglot risk).
ClamAv:MaxStreamBytes must align with the StreamMaxLength setting in your clamd.conf.
Dependencies
The NuGet package declares these dependencies — no manual installation needed:
- ASP.NET Core 10+ shared framework (via
FrameworkReference) - SixLabors.ImageSharp 3.1.x — image structural validation and polyglot-tail recompression
- Konscious.Security.Cryptography.Argon2 1.3.x — Argon2id key derivation for the master KEK
At runtime, one scanner backend is also required (not a NuGet package):
- Windows Defender (
MpCmdRun.exe) — Windows only, used byWindowsDefenderScanService - ClamAV (
clamdlistening on TCP) — Linux/macOS/containers, used byClamAvScanService
The scanner is selected automatically by AddSecureFileUpload() based on OperatingSystem.IsWindows(). Virus scanning can be disabled entirely via VirusScan:Enabled: false in appsettings (other 7 layers still run).
Installation
dotnet add package SecureFileUpload.Core
Requires .NET 10+ with ASP.NET Core. The package targets net10.0 and depends on the ASP.NET Core shared framework, which ships with every ASP.NET Core 10+ runtime — nothing extra needs to be installed. Earlier .NET targets are no longer supported in this version; pin to 1.0.x-preview.0 if you need a net8.0-only build.
Release Process
NuGet publishing is handled by GitHub Actions via .github/workflows/nuget-publish.yml.
- Push changes to
mainto run the build, pack, and fuzz-harness checks without publishing. - Ensure the repository or
nuget-publishenvironment has aNUGET_API_KEYsecret scoped toSecureFileUpload.Core. - Push a version tag to publish to NuGet.org. The workflow derives the package version directly from the tag name:
v1.0.1publishes package version1.0.1v1.0.1-preview.1publishes package version1.0.1-preview.1
- The workflow pushes both the
.nupkgand.snupkgartifacts with--skip-duplicate, so a re-run is non-destructive if the version already exists.
Example:
git tag v1.0.1-preview.1
git push origin v1.0.1-preview.1
The project file's <Version> remains useful for local packs and non-tag CI artifacts, but tag builds are the source of truth for published NuGet versions.
Integration Pattern
Minimal registration (recommended)
// Program.cs
using SecureFileUpload.Services;
// Registers FileContentValidator, the platform-appropriate IVirusScanService,
// and IFileUploadService in one call. Scanner options are read from appsettings
// ("FileContent" section). Pass a lambda to override in code.
builder.Services.AddSecureFileUpload();
// Size limit must match FileUpload:MaxTotalUploadBytes in appsettings.
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = 53_477_376; // 51 MB — adjust to match your config
});
Manual registration (if you need full control)
// Program.cs / Startup registration
builder.Services.AddSingleton<FileContentValidator>();
// Pick ONE virus scanner based on platform:
if (OperatingSystem.IsWindows())
builder.Services.AddSingleton<IVirusScanService, WindowsDefenderScanService>();
else
builder.Services.AddSingleton<IVirusScanService, ClamAvScanService>();
builder.Services.AddSingleton<IFileUploadService, FileUploadService>();
// Configure multipart body size limit to match your FileUpload:MaxTotalUploadBytes
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = 53_477_376; // 51 MB
});
Controller usage
[HttpPost]
[RequestSizeLimit(53_477_376)]
public async Task<IActionResult> Submit(MyInputModel model)
{
if (!ModelState.IsValid)
return View(model);
var files = Request.Form.Files;
var result = await _fileUploadService.UploadFilesAsync(files, model.LastName, "remote");
if (!result.Success)
{
// result.Errors contains user-safe messages
// result.WorkflowOutcome: AllSaved | PartialSaved | AllRejected | NoFiles
foreach (var error in result.Errors)
ModelState.AddModelError(string.Empty, error);
return View(model);
}
// ...
}
Docs
docs/threat-model.md— What attack each layer defeatsdocs/hardening-roadmap.md— Recommendations to reach the strongest realistic postureSECURITY-ANALYSIS.md— AI red-team adversarial findings (with current resolution status)KNOWN-GAPS.md— Honest limitations and what this does NOT protect againsttests/attack-vectors.md— Per-layer attack test cases (manual + automation guide)tests/Fuzz/— SharpFuzz + AFL++ harness for the deep content validator
License
MIT. Use freely. Attribution appreciated but not required.
Contributing
Issues and PRs welcome, especially:
- Unit test coverage for the validation layers
- Additional format validators (GIF, BMP deep content validation)
- Async/queued virus-scan worker for high-volume deployments
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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)
- SixLabors.ImageSharp (>= 3.1.11)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.3 | 101 | 6/2/2026 |
| 3.0.2 | 103 | 5/30/2026 |
| 3.0.1 | 101 | 5/30/2026 |
| 3.0.0 | 99 | 5/30/2026 |
| 2.0.0 | 100 | 5/30/2026 |
| 1.0.0 | 115 | 4/21/2026 |
| 1.0.0-preview.3 | 49 | 5/30/2026 |
| 1.0.0-preview.2 | 41 | 5/30/2026 |
v1.0.0-preview.2
• Crypto modernization: master Key Encryption Key (KEK) is now derived via Argon2id
(m=64 MiB, t=3, p=4) — the memory-hard KDF recommended by RFC 9106 and the OWASP
2024+ Password Storage Cheat Sheet. Replaces PBKDF2-SHA256 (600 000 iterations).
• Backward-compatible reads: PBKDF2-derived legacy KEKs (600k and 210k iterations) are
still tried during decryption when FileUpload:KeyDerivation:LegacyKekFallback=true
(default). New writes always use the Argon2id KEK. No file on disk is bricked by
the upgrade.
• Configurable KDF: select Argon2id (default) or Pbkdf2 (FIPS-restricted environments)
via FileUpload:KeyDerivation:Algorithm. Argon2id parameters and PBKDF2 iteration
count are both tunable from configuration.
• Target framework: net10.0 only. Earlier .NET targets are dropped in this version.
• Packaging: deterministic build, Source Link, embedded untracked sources, snupkg
symbols, README + LICENSE + SECURITY-ANALYSIS + KNOWN-GAPS bundled in the package.
• Docs: new "Implementation & Crypto Posture" section in README and SECURITY-ANALYSIS
with honest accounting of FIPS posture, salt handling, and KMS-integration gap.