Skip to content

Vault & Encryption

Granit.Encryption provides a pluggable string encryption service with AES-256-CBC as the default provider. Granit.Vault is the abstraction layer defining ITransitEncryptionService and IDatabaseCredentialProvider. Provider packages implement those interfaces: Granit.Vault.HashiCorp wraps VaultSharp for HashiCorp Vault Transit encryption and dynamic database credentials, Granit.Vault.Azure uses Azure Key Vault for RSA-OAEP-256 encryption and dynamic credentials via Key Vault Secrets, Granit.Vault.Aws targets AWS KMS and Secrets Manager, and Granit.Vault.GoogleCloud uses Google Cloud KMS for symmetric encryption and Secret Manager for dynamic database credentials. All provider modules are automatically disabled in Development environments so no external vault server is required locally.

  • DirectoryGranit.Encryption/ AES-256-CBC default, IStringEncryptionService
    • DirectoryGranit.Vault/ Abstraction layer — ITransitEncryptionService, IDatabaseCredentialProvider, localization
      • Granit.Vault.HashiCorp HashiCorp Vault provider — VaultSharp client, Transit encryption, dynamic database credentials
      • Granit.Vault.Azure Azure Key Vault provider (RSA-OAEP-256), dynamic credentials
      • Granit.Vault.Aws AWS KMS + Secrets Manager provider
      • Granit.Vault.GoogleCloud Google Cloud KMS encryption, Secret Manager credentials
PackageRoleDepends on
Granit.EncryptionIStringEncryptionService, AES-256-CBC providerGranit.Core
Granit.Encryption.EntityFrameworkCore[Encrypted] attribute, EncryptedStringConverter, ApplyEncryptionConventionsGranit.Encryption, Granit.Persistence
Granit.Encryption.ReEncryptionIReEncryptionJob, DefaultReEncryptionJob<TContext>Granit.Encryption.EntityFrameworkCore
Granit.VaultAbstraction layer — ITransitEncryptionService, IDatabaseCredentialProvider, ReEncryptionOptions, RetiredKeyVersionExceptionGranit.Encryption
Granit.Vault.HashiCorpHashiCorp Vault provider — VaultSharp client, Transit encryption, dynamic database credentialsGranit.Vault
Granit.Vault.AzureAzure Key Vault encryption (RSA-OAEP-256), dynamic credentials from Key Vault SecretsGranit.Vault
Granit.Vault.AwsAWS KMS encryption, dynamic credentials from Secrets ManagerGranit.Vault
Granit.Vault.GoogleCloudGoogle Cloud KMS symmetric encryption, dynamic credentials from Secret ManagerGranit.Vault
graph TD
    VH[Granit.Vault.HashiCorp] --> V[Granit.Vault]
    VA[Granit.Vault.Azure] --> V
    VAW[Granit.Vault.Aws] --> V
    VGC[Granit.Vault.GoogleCloud] --> V
    V --> E[Granit.Encryption]
    E --> C[Granit.Core]
    EEFC[Granit.Encryption.EntityFrameworkCore] --> E
    EERE[Granit.Encryption.ReEncryption] --> EEFC
[DependsOn(typeof(GranitEncryptionModule))]
public class AppModule : GranitModule { }

Registers IStringEncryptionService with the AES provider by default. No external dependencies required.

The main abstraction for encrypting and decrypting strings:

public interface IStringEncryptionService
{
string Encrypt(string plainText);
string? Decrypt(string cipherText);
}

Encrypt returns a Base64-encoded ciphertext. Decrypt returns the original string, or null if decryption fails (wrong key, corrupted data).

public class PatientNoteService(IStringEncryptionService encryption)
{
public string ProtectNote(string note) =>
encryption.Encrypt(note);
public string? RevealNote(string encryptedNote) =>
encryption.Decrypt(encryptedNote);
}

The default provider derives the encryption key from a passphrase using PBKDF2 and encrypts with AES-256-CBC. Each encryption operation generates a random 16-byte IV.

{
"Encryption": {
"PassPhrase": "<from-vault-config-provider>",
"KeySize": 256,
"ProviderName": "Aes"
}
}

The ProviderName option selects the active encryption provider at runtime:

ProviderValueBackend
AES (default)"Aes"Local AES-256-CBC with PBKDF2 key derivation
Vault Transit"Vault"HashiCorp Vault Transit engine (requires Granit.Vault.HashiCorp)
Azure Key Vault"AzureKeyVault"Azure Key Vault RSA-OAEP-256 (requires Granit.Vault.Azure)

When ProviderName is "Vault", IStringEncryptionService delegates to VaultStringEncryptionProvider, which calls Vault Transit under the hood.


Granit.Vault is the abstraction package. It defines ITransitEncryptionService and IDatabaseCredentialProvider — provider packages (Granit.Vault.HashiCorp, Granit.Vault.Azure, Granit.Vault.Aws) supply the implementations.

The transit encryption abstraction for encrypt, decrypt, and key-rotation operations:

public interface ITransitEncryptionService
{
// Encrypt plaintext — returns provider-specific ciphertext
Task<string> EncryptAsync(
string keyName, string plaintext,
CancellationToken cancellationToken = default);
// Decrypt ciphertext — returns original plaintext
Task<string> DecryptAsync(
string keyName, string ciphertext,
CancellationToken cancellationToken = default);
// Re-encrypt ciphertext to the latest key version (no plaintext exposure on HashiCorp)
Task<string> RewrapAsync(
string keyName, string ciphertext,
CancellationToken cancellationToken = default);
// Parse the key version from a versioned ciphertext (e.g. "v1" from "vault:v1:...")
// Returns null for providers whose ciphertext does not embed a version
string? GetKeyVersion(string ciphertext);
}

HashiCorp Vault ciphertext format: vault:v{N}:base64data — the version number is embedded in the prefix. Azure Key Vault, AWS KMS, and Google Cloud KMS produce opaque Base64 ciphertext without a version prefix (GetKeyVersion returns null).

public class HealthRecordService(ITransitEncryptionService transit)
{
public async Task<string> EncryptDiagnosisAsync(
string diagnosis, CancellationToken cancellationToken)
{
// HashiCorp returns "vault:v3:AbCdEf..." — version embedded in prefix
return await transit.EncryptAsync(
"health-records", diagnosis, cancellationToken).ConfigureAwait(false);
}
public async Task<string> DecryptDiagnosisAsync(
string ciphertext, CancellationToken cancellationToken)
{
return await transit.DecryptAsync(
"health-records", ciphertext, cancellationToken).ConfigureAwait(false);
}
}

Vault Transit’s /transit/rewrap endpoint re-encrypts an existing ciphertext to the latest key version entirely server-side — the plaintext never leaves Vault:

public async Task MigrateHealthRecordAsync(
string oldCiphertext, CancellationToken cancellationToken)
{
// oldCiphertext = "vault:v1:..." → newCiphertext = "vault:v3:..."
string newCiphertext = await transit.RewrapAsync(
"health-records", oldCiphertext, cancellationToken).ConfigureAwait(false);
// Persist newCiphertext in the database
}

For AWS KMS, Azure Key Vault, and Google Cloud KMS (which have no server-side rewrap), RewrapAsync falls back to DecryptAsync + EncryptAsync. The effect is identical — a new ciphertext encrypted with the current key — but the plaintext passes through application memory momentarily.

GetKeyVersion — inspecting ciphertext age

Section titled “GetKeyVersion — inspecting ciphertext age”

GetKeyVersion parses the version prefix from a Vault Transit ciphertext string:

string? version = transit.GetKeyVersion("vault:v2:AbCdEf...");
// version = "v2"
string? version2 = transit.GetKeyVersion("not-a-vault-ciphertext");
// version2 = null

Use this to identify rows that need re-encryption:

foreach (string ciphertext in ciphertexts)
{
string? version = transit.GetKeyVersion(ciphertext);
if (version == "v1" || version == "v2")
{
string rewrapped = await transit.RewrapAsync("pii-data", ciphertext, ct)
.ConfigureAwait(false);
// update DB row with rewrapped
}
}

ReEncryptionOptions — retiring old key versions

Section titled “ReEncryptionOptions — retiring old key versions”

After re-encryption, configure RetiredKeyVersions to make any missed row fail loudly:

{
"ReEncryption": {
"RetiredKeyVersions": ["v1", "v2"],
"BatchSize": 500
}
}
builder.Services.Configure<ReEncryptionOptions>(
builder.Configuration.GetSection(ReEncryptionOptions.SectionName));

HashiCorpVaultStringEncryptionProvider checks RetiredKeyVersions before every decrypt call. If the ciphertext carries a retired version prefix, it throws RetiredKeyVersionException instead of delegating to Vault:

try
{
string plaintext = encryptionService.Decrypt(row.NationalId);
}
catch (RetiredKeyVersionException ex)
{
// A row escaped the re-encryption job — fix before retiring the Vault key
logger.LogError("Retired key version {Version} still in use", ex.KeyVersion);
throw;
}
PropertyTypeDefaultDescription
RetiredKeyVersionsISet<string>{}Versions that trigger RetiredKeyVersionException on decrypt
BatchSizeint500Default batch size for IReEncryptionJob

See the Field-level Encryption page for the end-to-end workflow diagram combining IReEncryptionJob and RewrapAsync.


[DependsOn(typeof(GranitVaultHashiCorpModule))]
public class AppModule : GranitModule { }

VaultCredentialLeaseManager obtains short-lived database credentials from the Vault Database engine and handles automatic lease renewal at a configurable threshold (default: 75% of TTL). The connection string is updated transparently — no application restart required.

{
"Vault": {
"Address": "https://vault.example.com",
"AuthMethod": "Kubernetes",
"KubernetesRole": "my-backend"
}
}

Uses the Kubernetes service account token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

{
"Vault": {
"Address": "https://vault.example.com",
"AuthMethod": "Kubernetes",
"KubernetesRole": "my-backend",
"KubernetesTokenPath": "/var/run/secrets/kubernetes.io/serviceaccount/token",
"DatabaseMountPoint": "database",
"DatabaseRoleName": "readwrite",
"TransitMountPoint": "transit",
"LeaseRenewalThreshold": 0.75
},
"Encryption": {
"PassPhrase": "<from-vault>",
"KeySize": 256,
"ProviderName": "Aes",
"VaultKeyName": "string-encryption"
}
}
PropertyDefaultDescription
Vault.AddressVault server URL
Vault.AuthMethod"Kubernetes""Kubernetes" or "Token"
Vault.KubernetesRole"my-backend"Vault Kubernetes auth role
Vault.DatabaseMountPoint"database"Database secrets engine mount point
Vault.DatabaseRoleName"readwrite"Database role for dynamic credentials
Vault.TransitMountPoint"transit"Transit secrets engine mount point
Vault.LeaseRenewalThreshold0.75Renew lease at this fraction of TTL
Encryption.PassPhraseAES passphrase (via secrets manager)
Encryption.KeySize256AES key size in bits
Encryption.ProviderName"Aes"Active provider: "Aes" or "Vault"
Encryption.VaultKeyName"string-encryption"Transit key name for IStringEncryptionService

AddGranitVaultHashiCorpHealthCheck registers a Vault health check that verifies connectivity and authentication status, tagged ["readiness"] for Kubernetes probe integration.


[DependsOn(typeof(GranitVaultAzureModule))]
public class AppModule : GranitModule { }

Uses Azure Key Vault’s CryptographyClient for RSA-OAEP-256 encrypt/decrypt operations. Registered as an IStringEncryptionProvider with key "AzureKeyVault".

AzureSecretsCredentialProvider reads database credentials from a Key Vault secret (JSON: {"username": "...", "password": "..."}) and polls for version changes at a configurable interval. Credential rotation is detected automatically.

{
"Vault": {
"Azure": {
"VaultUri": "https://my-vault.vault.azure.net/"
}
}
}

Uses DefaultAzureCredential — Managed Identity in Kubernetes, az login locally.

PropertyDefaultDescription
Vault:Azure:VaultUriAzure Key Vault URI
Vault:Azure:EncryptionKeyName"string-encryption"Key name for encrypt/decrypt
Vault:Azure:EncryptionAlgorithm"RSA-OAEP-256"Algorithm (RSA-OAEP-256, RSA-OAEP, RSA1_5)
Vault:Azure:DatabaseSecretNamenullSecret name for DB credentials (omit to disable)
Vault:Azure:RotationCheckIntervalMinutes5Secret rotation polling interval
Vault:Azure:TimeoutSeconds30Azure SDK operation timeout

Granit.Vault.Azure registers a health check that retrieves the configured key to verify connectivity and key availability, tagged ["readiness"].


[DependsOn(typeof(GranitVaultGoogleCloudModule))]
public class AppModule : GranitModule { }

Uses Cloud KMS CryptoKeyVersion for symmetric AES-256-GCM encrypt/decrypt operations. Registered as an IStringEncryptionProvider with key "CloudKms".

SecretManagerCredentialProvider reads database credentials from a Secret Manager secret (JSON: {"username": "...", "password": "..."}) and polls for version changes at a configurable interval. Credential rotation is detected automatically.

{
"Vault": {
"GoogleCloud": {
"ProjectId": "my-project",
"Location": "europe-west1",
"KeyRing": "my-keyring",
"CryptoKey": "string-encryption"
}
}
}

Uses Application Default Credentials — Workload Identity in GKE, gcloud auth locally.

PropertyDefaultDescription
Vault:GoogleCloud:ProjectIdGCP project ID (required)
Vault:GoogleCloud:LocationCloud KMS location, e.g. "europe-west1" (required)
Vault:GoogleCloud:KeyRingCloud KMS key ring name (required)
Vault:GoogleCloud:CryptoKeyCloud KMS crypto key for transit encryption (required)
Vault:GoogleCloud:DatabaseSecretNamenullSecret Manager secret name for DB credentials (omit to disable)
Vault:GoogleCloud:RotationCheckIntervalMinutes5Secret rotation polling interval
Vault:GoogleCloud:CredentialFilePathnullService account key JSON; ADC when null
Vault:GoogleCloud:TimeoutSeconds30API call timeout

AddGranitCloudKmsHealthCheck registers a health check that verifies KMS key accessibility, tagged ["readiness"].

CategoryKey typesPackage
ModulesGranitEncryptionModule, GranitVaultHashiCorpModule, GranitVaultAzureModule, GranitVaultAwsModule, GranitVaultGoogleCloudModule
EncryptionIStringEncryptionService (Encrypt / Decrypt)Granit.Encryption
ProviderIStringEncryptionProvider, AesStringEncryptionProviderGranit.Encryption
TransitITransitEncryptionService (EncryptAsync / DecryptAsync)Granit.Vault
CredentialsIDatabaseCredentialProviderGranit.Vault
HashiCorp ProviderVaultStringEncryptionProvider, VaultCredentialLeaseManagerGranit.Vault.HashiCorp
HashiCorp OptionsHashiCorpVaultOptionsGranit.Vault.HashiCorp
OptionsStringEncryptionOptionsGranit.Encryption
Azure ProviderAzureKeyVaultStringEncryptionProvider, IAzureKeyVaultTransitEncryptionService, IAzureDatabaseCredentialProviderGranit.Vault.Azure
Azure OptionsAzureKeyVaultOptionsGranit.Vault.Azure
Google Cloud ProviderCloudKmsTransitEncryptionService, CloudKmsStringEncryptionProvider, SecretManagerCredentialProviderGranit.Vault.GoogleCloud
Google Cloud OptionsGoogleCloudVaultOptionsGranit.Vault.GoogleCloud
ExtensionsAddGranitEncryption(), AddGranitVaultHashiCorp(), AddGranitVaultAzure(), AddGranitVaultAws(), AddGranitVaultGoogleCloud()