Explicitly-invoked cryptography toolkit for application code — AES-GCM and RSA encryption, RSA signing, HMAC and SHA hashing, and Base64/Base64URL helpers, all string-in/string-out.
[!NOTE] Looking for transparent EF Core column encryption instead — where an attribute on an entity property does the work automatically on save/load? That’s a different package:
DKNet.EfCore.Encryption. UseDKNet.Svc.Encryptionwhen you want to callEncrypt/Decryptyourself; useDKNet.EfCore.Encryptionwhen you want a column encrypted without touching the code that reads and writes the entity. The two don’t share implementation —DKNet.EfCore.Encryption’s AES-GCM provider has its own key-provider abstraction.
IAesGcmEncryption handles nonce generation, tag handling, and
packaging into a single Base64 string, so call sites never assemble a cipher envelope by hand.Verify* and on the AES-GCM base64Key overloads’ key check — the choices that are easy to get wrong are already
made.varchar column with no byte-array plumbing.Reach for it whenever application code needs to encrypt a value, hash a token, sign a webhook payload, or verify an HMAC — anywhere the call site should control exactly when encryption happens.
dotnet add package DKNet.Svc.Encryption
using DKNet.Svc.Encryption;
// IShaHashing and IHmacHashing — the keyless hashing services, transient
builder.Services.AddEncryptionServices();
// Every cipher is opt-in and takes the key you supply — one singleton per key
builder.Services.AddAesGcmEncryption(builder.Configuration["Crypto:AesKey"]!);
builder.Services.AddRsaEncryption(builder.Configuration["Crypto:RsaPrivateKey"]!);
public sealed class SecretStore(IAesGcmEncryption aesGcm)
{
public string Protect(string plainText) => aesGcm.EncryptString(plainText);
public string Reveal(string cipherPackage) => aesGcm.DecryptString(cipherPackage);
}
AddEncryptionServices registers no cipher — only the hashing services. Each cipher has its own key-taking
registration: AddAesGcmEncryption and AddRsaEncryption. All three methods are idempotent — calling any of them
twice does not double-register — and the two cipher methods throw ArgumentException for a null, empty, or
whitespace key.
[!IMPORTANT] A cipher’s key is yours to supply and persist.
AddAesGcmEncryption(base64Key)registersIAesGcmEncryptionas a singleton built from that key, so every resolution shares it and a value encrypted in one request is decryptable in the next. Source the key from configuration or a key vault — never hardcode it.new AesGcmEncryption()with no key still generates a random one, which is fine only for data that never outlives the process.
IAesGcmEncryption) — the encryption to reach forstring EncryptString(string plainText, byte[]? associatedData = null);
string DecryptString(string cipherPackage, byte[]? associatedData = null);
string Encrypt(string plainText, string base64Key, byte[]? associatedData = null);
string Decrypt(string cipherPackage, string base64Key, byte[]? associatedData = null);
string Key { get; } // Base64, persist this to decrypt later
new AesGcmEncryption() generates a random 256-bit key; new AesGcmEncryption(existingBase64Key) reconstructs an
instance from a previously persisted key (accepting 128/192/256-bit keys, and rejecting a key containing : — that
shape belonged to the removed AES-CBC type’s key:iv format). Every call produces a fresh random 12-byte nonce and
16-byte tag, so ciphertext is never deterministic even for the same plaintext:
using var aes = new AesGcmEncryption(); // keep aes.Key to decrypt later
var package = aes.EncryptString("4111-1111-1111-1111");
var plain = aes.DecryptString(package);
The returned package is Base64 of nonce:tag:cipher (each part itself Base64) — treat it as opaque and pass it around
whole. Pass associatedData (AAD) when tamper detection should cover context outside the ciphertext; decrypting with
different AAD throws CryptographicException. The Encrypt/Decrypt overloads that take a base64Key throw
InvalidOperationException when it does not match the instance’s own key — they never silently re-key. The
comparison decodes both keys and runs CryptographicOperations.FixedTimeEquals on the resulting bytes, so a wrong
key takes the same time to reject no matter how much of it happened to be right, and a base64 string that decodes
to the same bytes as the instance’s own key is accepted even if the two strings differ textually.
IRsaEncryption) — asymmetric encrypt and signnew RsaEncryption(int keySize = 2048); // generates a new key pair
new RsaEncryption(string privateKeyBase64); // loads an existing private key (PKCS#1 DER, Base64)
RsaEncryption.FromPublicKey(string publicKeyBase64); // public-only instance
string Encrypt(string plainText); // OAEP-SHA256
string Decrypt(string base64CipherText);
string Sign(string data); // PKCS#1 v1.5 + SHA256
bool Verify(string data, string base64Signature);
string PublicKey { get; }
string? PrivateKey { get; } // null on a public-only instance
A public-only instance can Encrypt and Verify but throws InvalidOperationException on Decrypt/Sign — deploy it
on the side that only sends or verifies, and keep the private key off that side entirely:
// signing side (has the private key)
using var signer = new RsaEncryption(privateKeyBase64);
var signature = signer.Sign(payload);
// verifying side (public key only)
using var verifier = RsaEncryption.FromPublicKey(publicKeyBase64);
var ok = verifier.Verify(payload, signature);
PublicKey and PrivateKey are Base64 of the raw PKCS#1 structures (ExportRSAPublicKey / ExportRSAPrivateKey) —
not PEM, so don’t paste them into a -----BEGIN block without re-encoding.
IHmacHashing)string ComputeSha256(string message, string secretKey, bool asBase64 = true);
string ComputeSha512(string message, string secretKey, bool asBase64 = true);
bool VerifySha256(string message, string secretKey, string expectedSignature, bool signatureIsBase64 = true);
bool VerifySha512(string message, string secretKey, string expectedSignature, bool signatureIsBase64 = true);
Set asBase64 = false to get upper-case hex instead, and match that choice with signatureIsBase64 on the verify call —
a mismatched encoding returns false rather than throwing. The key bytes are zeroed after each computation, and
comparison runs through CryptographicOperations.FixedTimeEquals:
// verifying an inbound webhook signature
var trusted = hmac.VerifySha256(rawBody, webhookSecret, header["X-Signature"]!);
Blank message, secretKey, or expectedSignature throws ArgumentException.
IShaHashing)string ComputeSha256(string input, bool upperCase = false);
string ComputeSha512(string input, bool upperCase = false);
bool VerifySha256(string input, string expectedHex, bool ignoreCase = true);
bool VerifySha512(string input, string expectedHex, bool ignoreCase = true);
Output is hex, lower-case unless upperCase is set. Verification is hex-only (expectedHex), constant-time, and returns
false — instead of throwing — when the expected value isn’t valid hex. Empty string input is allowed; null throws.
Base64StringExtensions exposes ToBase64String, FromBase64String, ToBase64UrlString, FromBase64UrlString, and
IsBase64String as this string extension methods, so both call styles work:
using DKNet.Svc.Encryption;
var token = payloadJson.ToBase64UrlString(); // extension-method call
var back = Base64StringExtensions.FromBase64UrlString(token); // static call — still compiles
The URL variants are useful for JWT-style payloads without pulling in a JWT library. Whitespace or empty input
returns string.Empty from both decode helpers, and IsBase64String returns false for it.
IAesEncryption) — removedIAesEncryption, AesEncryption, and EncryptionSetup.AddAesEncryption have been removed, not merely
deprecated. AES-CBC as this type used it kept a fixed IV embedded in the key, so encrypting the same plaintext twice
always produced the same ciphertext — a real leak of information about the data (e.g. two customers sharing a
password hash, or a repeated field, becomes visible from ciphertext alone) rather than a theoretical concern.
IAesGcmEncryption (AddAesGcmEncryption) is authenticated, uses a fresh random nonce per call, and is the only
symmetric cipher this package ships. There is no compatibility shim — code still calling AddAesEncryption or
constructing AesEncryption will not build; re-encrypt existing ciphertext under IAesGcmEncryption and switch the
call sites.
There is no options type and no IConfiguration binding path in this package. The entire customisation
surface is the four registration methods and the constructors behind them — everything a caller can vary
is an argument, so this table is the equivalent of a configuration reference.
EncryptionSetup)| Method | Argument | Registers | Lifetime | Throws |
|---|---|---|---|---|
AddEncryptionServices() |
— | IShaHashing, IHmacHashing |
Transient | — |
AddAesGcmEncryption(base64Key) |
Base64 128/192/256-bit key | IAesGcmEncryption |
Singleton | ArgumentException on a blank key |
AddRsaEncryption(privateKeyBase64) |
Base64 PKCS#1 private key | IRsaEncryption |
Singleton | ArgumentException on a blank key |
All three skip registration when the service type is already registered, so a second call is a no-op rather than a second instance — the first call’s key is the one the application uses.
| Constructor / factory | Parameter | Default | Effect |
|---|---|---|---|
new AesGcmEncryption(string? key = null) |
Base64 key, no : |
null |
null generates a random 256-bit key; a supplied key must decode to 16, 24, or 32 bytes. |
new RsaEncryption(int keySize = 2048) |
Key size in bits | 2048 |
Generates a fresh key pair at that size. |
new RsaEncryption(string privateKeyBase64) |
PKCS#1 private key | (required) | Loads an existing pair; the public key is derived. |
RsaEncryption.FromPublicKey(string publicKeyBase64) |
PKCS#1 public key | (required) | Public-only instance: Encrypt/Verify work, Decrypt/Sign throw. |
| Member | Parameter | Default | Effect |
|---|---|---|---|
IAesGcmEncryption.EncryptString / DecryptString |
associatedData |
null |
Extra bytes bound into the authentication tag; a mismatch on decrypt throws CryptographicException. |
IHmacHashing.ComputeSha256 / ComputeSha512 |
asBase64 |
true |
false returns upper-case hex instead of Base64. |
IHmacHashing.VerifySha256 / VerifySha512 |
signatureIsBase64 |
true |
Must match how the signature was produced; a mismatch returns false. |
IShaHashing.VerifySha256 / VerifySha512 |
ignoreCase |
true |
No effect — comparison runs on decoded bytes. (IHmacHashing’s Verify* overloads dropped this parameter entirely; it was never read.) |
IShaHashing.ComputeSha256 / ComputeSha512 |
upperCase |
false |
true returns upper-case hex. |
These are not configurable and are worth knowing before you design around them:
| Concern | Value |
|---|---|
| AES-GCM nonce / tag | 12 bytes / 16 bytes, fresh nonce per call |
| AES-GCM generated key size | 256-bit |
| AES-GCM package layout | Base64 of base64(nonce):base64(tag):base64(cipher) |
| RSA encryption padding | OAEP-SHA256 |
| RSA signature | SHA-256 with PKCS#1 v1.5 |
| Key export format | Raw PKCS#1 DER, Base64 — not PEM |
| Verification comparison | CryptographicOperations.FixedTimeEquals — also used for the AES-GCM base64Key overloads’ key check |
Which registration you call decides which service you can resolve — and only the cipher registrations take a key:
SaveAsync when the store itself must never see plaintext.Microsoft.Extensions.DependencyInjection.Abstractions,
so it can be called from a domain service, a handler, or a hosted worker without pulling in EF Core or messaging.AddAesGcmEncryption call means one key for
the whole application; rotating it means new configuration and a restart. A hand-built new AesGcmEncryption()
generates a throwaway key instead, so ciphertext produced from it dies with the instance.Rfc2898DeriveBytes) before constructing AesGcmEncryption. An earlier revision of this page documented a
PasswordAesEncryption type; it does not exist in source.ignoreCase on IShaHashing.Verify* is a no-op. Comparison is done on decoded bytes, so case never matters —
don’t read the parameter as a behavior switch. IHmacHashing.Verify* doesn’t have the parameter at all.IShaHashing and IHmacHashing are not IDisposable. Both are stateless wrappers over the static
System.Security.Cryptography hash APIs — no using needed, unlike IAesGcmEncryption/IRsaEncryption below.Key, PrivateKey, and PublicKey are yours to persist and rotate. The package stores nothing and has no key
rotation, versioning, or envelope-key support.byte[] overloads — a large payload is fully materialized as a UTF-8
string and again as Base64.Encrypt/Decrypt take a lock on the shared AesGcm handle, so
one instance is thread-safe but not concurrent; resolve per unit of work if throughput matters.AesGcmEncryption and RsaEncryption hold native handles; DI disposes the transient
and singleton registrations, but a hand-built instance needs a using.