Security.SecretProtector 1.1.0

dotnet add package Security.SecretProtector --version 1.1.0
                    
NuGet\Install-Package Security.SecretProtector -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Security.SecretProtector" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Security.SecretProtector" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Security.SecretProtector" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Security.SecretProtector --version 1.1.0
                    
#r "nuget: Security.SecretProtector, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Security.SecretProtector@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Security.SecretProtector&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Security.SecretProtector&version=1.1.0
                    
Install as a Cake Tool

Security.SecretProtector

AES-256-GCM at-rest secret protection for multi-tenant SaaS services. One audited implementation, shared across services, with a fixed on-disk format so stored secrets keep decrypting across upgrades.

Consolidates two previously-duplicated copies:

  • AML's AesGcmCredentialCipher (external-provider API keys, webhook signing secrets)
  • Kefi's ConfigAesGcmSecretProtector (per-tenant Stripe credentials)

Surface

public interface ISecretProtector
{
    string Protect(string plaintext);   // → base64( nonce(12) ‖ tag(16) ‖ ciphertext )
    string Unprotect(string token);     // throws on tamper / wrong key / not configured
    bool IsConfigured { get; }          // false in soft mode when the key is unset
}

On-disk format (stable — do not change)

A protected token is base64( nonce(12 bytes) ‖ tag(16 bytes) ‖ ciphertext ). AES-256-GCM authenticated encryption: a tampered or wrong-key token throws on Unprotect rather than returning garbage. The key is a 32-byte value decoded from base64 or hex (hex is detected first for 64-char all-hex strings). Rotating the key invalidates every stored ciphertext, so the key is catastrophic-if-lost.

Quick start

Register the protector, pointing it at the configuration key that holds your 32-byte key:

using Security.SecretProtector.Extensions;

// Mandatory secret → fail fast at startup if the key is missing/invalid (Kefi style):
builder.Services.AddConfigSecretProtector("KEFI_SECRETS_ENC_KEY", failFast: true);

// Optional secret → soft mode; ISecretProtector.IsConfigured gates callers (AML style):
builder.Services.AddConfigSecretProtector("Providers:CredentialKey", failFast: false);

Then consume it:

public sealed class MyService(ISecretProtector protector)
{
    public string Store(string apiKey) => protector.Protect(apiKey);
    public string? Use(string ciphertext)
        => protector.IsConfigured ? protector.Unprotect(ciphertext) : null;
}

The key value lives only in the app's runtime configuration / secret (env var in dev, Kubernetes secret in prod) — never in this package or any committed file. Generate one with openssl rand -base64 32.

Modes

Mode failFast Missing / invalid key Use when
Fail-fast true (default) throws at construction/startup the secret is mandatory
Soft false IsConfigured == false; Protect/Unprotect throw only if called the secret is optional

Per-tenant keys (ITenantSecretProtector, 1.1.0+)

For credentials that must be cryptographically isolated per tenant — a client's EMI / bank API credentials in FINREG, where one tenant reading another's would be catastrophic — use ITenantSecretProtector. It keeps a versioned master keyring and derives each tenant's key on demand:

perTenantKey = HKDF-SHA256(masterKey_v, salt = UTF8(tenantId), info = "finreg-tenant-credential-v1")

No per-tenant key material is ever stored — it is recomputed for each operation and zeroed after. A token protected for one tenant cannot be decrypted under another's id (the derived keys differ → GCM authentication fails).

using Security.SecretProtector.Extensions;

// Eager, fail-fast at STARTUP (unlike the single-key registration, the keyring is built now):
builder.Services.AddTenantSecretProtector(builder.Configuration, "Providers:CredentialKeyring");
// Configuration shape (keys sourced from a secrets manager, never an inline manifest value):
"Providers": {
  "CredentialKeyring": {
    "CurrentVersion": 1,
    "Keys": { "1": "<base64/hex 32-byte key>" }   // add "2" during a rotation
  }
}
public sealed class VaultService(ITenantSecretProtector protector)
{
    public string Store(string tenantId, string credential) => protector.Protect(tenantId, credential);
    public string Use(string tenantId, string token)        => protector.Unprotect(tenantId, token);
}

Token layout: base64( keyVersion(2) ‖ nonce(12) ‖ tag(16) ‖ ciphertext ) — a distinct type from the single-key token; the two are never interchanged.

Zero-downtime rotation: add a new key under a higher version and set CurrentVersion to it. New secrets protect under the new version; already-stored tokens keep decrypting because their version selects the retained old key. Re-encrypt lazily, then drop the old key. Removing a version before its tokens are migrated makes those tokens fail closed (CryptographicException).

License

MIT

Product 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 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Security.SecretProtector:

Package Downloads
Dloizides.Webhooks.AspNetCore

Durable outbound-webhook delivery for multi-tenant ASP.NET Core SaaS. A per-endpoint transactional outbox: each (event x subscribed endpoint) is delivered and retried independently on its own back-off, so one failing endpoint never re-delivers to healthy ones. HMAC-signed POSTs with configurable headers, scale-out-safe xmin lease processor, dead-lettering, and per-attempt delivery history. The consumer plugs in an endpoint resolver + their DbContext; delivery moves off the request path in ~5 lines of Program.cs.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 53 7/26/2026
1.0.1 821 7/1/2026