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.
Package structure
Section titled “Package structure”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
| Package | Role | Depends on |
|---|---|---|
Granit.Encryption | IStringEncryptionService, AES-256-CBC provider | Granit.Core |
Granit.Encryption.EntityFrameworkCore | [Encrypted] attribute, EncryptedStringConverter, ApplyEncryptionConventions | Granit.Encryption, Granit.Persistence |
Granit.Encryption.ReEncryption | IReEncryptionJob, DefaultReEncryptionJob<TContext> | Granit.Encryption.EntityFrameworkCore |
Granit.Vault | Abstraction layer — ITransitEncryptionService, IDatabaseCredentialProvider, ReEncryptionOptions, RetiredKeyVersionException | Granit.Encryption |
Granit.Vault.HashiCorp | HashiCorp Vault provider — VaultSharp client, Transit encryption, dynamic database credentials | Granit.Vault |
Granit.Vault.Azure | Azure Key Vault encryption (RSA-OAEP-256), dynamic credentials from Key Vault Secrets | Granit.Vault |
Granit.Vault.Aws | AWS KMS encryption, dynamic credentials from Secrets Manager | Granit.Vault |
Granit.Vault.GoogleCloud | Google Cloud KMS symmetric encryption, dynamic credentials from Secret Manager | Granit.Vault |
Dependency graph
Section titled “Dependency graph”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
Granit.Encryption
Section titled “Granit.Encryption”[DependsOn(typeof(GranitEncryptionModule))]public class AppModule : GranitModule { }Registers IStringEncryptionService with the AES provider by default. No external
dependencies required.
IStringEncryptionService
Section titled “IStringEncryptionService”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);}AES-256-CBC provider
Section titled “AES-256-CBC provider”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" }}Provider switching
Section titled “Provider switching”The ProviderName option selects the active encryption provider at runtime:
| Provider | Value | Backend |
|---|---|---|
| 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
Section titled “Granit.Vault”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.
ITransitEncryptionService
Section titled “ITransitEncryptionService”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); }}Key rotation
Section titled “Key rotation”RewrapAsync — server-side re-encryption
Section titled “RewrapAsync — server-side re-encryption”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 = nullUse 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;}| Property | Type | Default | Description |
|---|---|---|---|
RetiredKeyVersions | ISet<string> | {} | Versions that trigger RetiredKeyVersionException on decrypt |
BatchSize | int | 500 | Default batch size for IReEncryptionJob |
Complete rotation workflow
Section titled “Complete rotation workflow”See the Field-level Encryption
page for the end-to-end workflow diagram combining IReEncryptionJob and RewrapAsync.
Granit.Vault.HashiCorp
Section titled “Granit.Vault.HashiCorp”[DependsOn(typeof(GranitVaultHashiCorpModule))]public class AppModule : GranitModule { }Dynamic database credentials
Section titled “Dynamic database credentials”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.
Authentication
Section titled “Authentication”{ "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": "http://localhost:8200", "AuthMethod": "Token", "Token": "hvs.dev-only-token" }}Token auth is intended for local Vault dev servers only.
Configuration reference
Section titled “Configuration reference”{ "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" }}| Property | Default | Description |
|---|---|---|
Vault.Address | — | Vault 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.LeaseRenewalThreshold | 0.75 | Renew lease at this fraction of TTL |
Encryption.PassPhrase | — | AES passphrase (via secrets manager) |
Encryption.KeySize | 256 | AES key size in bits |
Encryption.ProviderName | "Aes" | Active provider: "Aes" or "Vault" |
Encryption.VaultKeyName | "string-encryption" | Transit key name for IStringEncryptionService |
Health check
Section titled “Health check”AddGranitVaultHashiCorpHealthCheck registers a Vault health check that verifies
connectivity and authentication status, tagged ["readiness"] for Kubernetes probe
integration.
Granit.Vault.Azure
Section titled “Granit.Vault.Azure”[DependsOn(typeof(GranitVaultAzureModule))]public class AppModule : GranitModule { }Azure Key Vault encryption
Section titled “Azure Key Vault encryption”Uses Azure Key Vault’s CryptographyClient for RSA-OAEP-256 encrypt/decrypt
operations. Registered as an IStringEncryptionProvider with key "AzureKeyVault".
Dynamic database credentials
Section titled “Dynamic database credentials”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.
Authentication
Section titled “Authentication”{ "Vault": { "Azure": { "VaultUri": "https://my-vault.vault.azure.net/" } }}Uses DefaultAzureCredential — Managed Identity in Kubernetes, az login locally.
{ "Vault": { "Azure": { "VaultUri": "https://my-vault.vault.azure.net/", "EncryptionKeyName": "string-encryption", "EncryptionAlgorithm": "RSA-OAEP-256", "DatabaseSecretName": "db-credentials", "RotationCheckIntervalMinutes": 5, "TimeoutSeconds": 30 } }}| Property | Default | Description |
|---|---|---|
Vault:Azure:VaultUri | — | Azure 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:DatabaseSecretName | null | Secret name for DB credentials (omit to disable) |
Vault:Azure:RotationCheckIntervalMinutes | 5 | Secret rotation polling interval |
Vault:Azure:TimeoutSeconds | 30 | Azure SDK operation timeout |
Health check
Section titled “Health check”Granit.Vault.Azure registers a health check that retrieves the configured key to
verify connectivity and key availability, tagged ["readiness"].
Granit.Vault.GoogleCloud
Section titled “Granit.Vault.GoogleCloud”[DependsOn(typeof(GranitVaultGoogleCloudModule))]public class AppModule : GranitModule { }Google Cloud KMS encryption
Section titled “Google Cloud KMS encryption”Uses Cloud KMS CryptoKeyVersion for symmetric AES-256-GCM encrypt/decrypt operations.
Registered as an IStringEncryptionProvider with key "CloudKms".
Dynamic database credentials
Section titled “Dynamic database credentials”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.
Authentication
Section titled “Authentication”{ "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.
{ "Vault": { "GoogleCloud": { "ProjectId": "my-project", "Location": "europe-west1", "KeyRing": "my-keyring", "CryptoKey": "string-encryption", "DatabaseSecretName": "db-credentials", "RotationCheckIntervalMinutes": 5, "CredentialFilePath": null, "TimeoutSeconds": 30 } }}| Property | Default | Description |
|---|---|---|
Vault:GoogleCloud:ProjectId | — | GCP project ID (required) |
Vault:GoogleCloud:Location | — | Cloud KMS location, e.g. "europe-west1" (required) |
Vault:GoogleCloud:KeyRing | — | Cloud KMS key ring name (required) |
Vault:GoogleCloud:CryptoKey | — | Cloud KMS crypto key for transit encryption (required) |
Vault:GoogleCloud:DatabaseSecretName | null | Secret Manager secret name for DB credentials (omit to disable) |
Vault:GoogleCloud:RotationCheckIntervalMinutes | 5 | Secret rotation polling interval |
Vault:GoogleCloud:CredentialFilePath | null | Service account key JSON; ADC when null |
Vault:GoogleCloud:TimeoutSeconds | 30 | API call timeout |
Health check
Section titled “Health check”AddGranitCloudKmsHealthCheck registers a health check that verifies KMS key
accessibility, tagged ["readiness"].
Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Modules | GranitEncryptionModule, GranitVaultHashiCorpModule, GranitVaultAzureModule, GranitVaultAwsModule, GranitVaultGoogleCloudModule | — |
| Encryption | IStringEncryptionService (Encrypt / Decrypt) | Granit.Encryption |
| Provider | IStringEncryptionProvider, AesStringEncryptionProvider | Granit.Encryption |
| Transit | ITransitEncryptionService (EncryptAsync / DecryptAsync) | Granit.Vault |
| Credentials | IDatabaseCredentialProvider | Granit.Vault |
| HashiCorp Provider | VaultStringEncryptionProvider, VaultCredentialLeaseManager | Granit.Vault.HashiCorp |
| HashiCorp Options | HashiCorpVaultOptions | Granit.Vault.HashiCorp |
| Options | StringEncryptionOptions | Granit.Encryption |
| Azure Provider | AzureKeyVaultStringEncryptionProvider, IAzureKeyVaultTransitEncryptionService, IAzureDatabaseCredentialProvider | Granit.Vault.Azure |
| Azure Options | AzureKeyVaultOptions | Granit.Vault.Azure |
| Google Cloud Provider | CloudKmsTransitEncryptionService, CloudKmsStringEncryptionProvider, SecretManagerCredentialProvider | Granit.Vault.GoogleCloud |
| Google Cloud Options | GoogleCloudVaultOptions | Granit.Vault.GoogleCloud |
| Extensions | AddGranitEncryption(), AddGranitVaultHashiCorp(), AddGranitVaultAzure(), AddGranitVaultAws(), AddGranitVaultGoogleCloud() | — |
See also
Section titled “See also”- Caching module — AES-256 encryption for cached GDPR-sensitive data
- Security module — Authorization and current user abstractions
- Persistence module — EF Core interceptors, dynamic credentials integration