Encrypt sensitive data
Granit provides two encryption layers — field-level encryption via IStringEncryptionService
and Vault Transit encryption via ITransitEncryptionService — so sensitive data
(national ID numbers, health records, API keys) is never stored in plaintext.
Prerequisites
Section titled “Prerequisites”- A .NET 10 project with
Granit.Core Granit.Encryptionfor AES-256 field-level encryptionGranit.Vaultfor vault abstractions (ITransitEncryptionService,IDatabaseCredentialProvider)Granit.Vault.HashiCorpfor HashiCorp Vault Transit encryption (production)Granit.Vault.Azurefor Azure Key Vault encryption (production, Azure environments)- A running HashiCorp Vault or Azure Key Vault instance (production only)
Step 1 — Install the packages
Section titled “Step 1 — Install the packages”dotnet add package Granit.Encryptiondotnet add package Granit.Vault.HashiCorpGranit.Vault.HashiCorp depends on Granit.Vault and Granit.Encryption — all are installed automatically.
dotnet add package Granit.Vault.AzureGranit.Vault.Azure depends on Granit.Vault and Granit.Encryption — all are installed automatically.
Uses DefaultAzureCredential (Managed Identity in production, az login locally).
Step 2 — Configure encryption
Section titled “Step 2 — Configure encryption”AES-256 provider (development / non-Vault environments)
Section titled “AES-256 provider (development / non-Vault environments)”Add the module dependency and configure the passphrase:
[DependsOn(typeof(GranitEncryptionModule))]public sealed class AppModule : GranitModule { }{ "Encryption": { "PassPhrase": "<derived-from-vault-or-secure-store>", "ProviderName": "Aes" }}Vault Transit provider (production)
Section titled “Vault Transit provider (production)”[DependsOn(typeof(GranitVaultHashiCorpModule))]public sealed class AppModule : GranitModule { }{ "Vault": { "Address": "https://vault.internal:8200", "AuthMethod": "Kubernetes", "KubernetesRole": "my-backend", "TransitMountPoint": "transit" }, "Encryption": { "ProviderName": "Vault" }}The ProviderName setting selects the active provider:
| Provider | ProviderName | Latency | When to use |
|---|---|---|---|
AesStringEncryptionProvider | "Aes" | < 1 ms | Settings, cache, high-frequency operations |
VaultStringEncryptionProvider | "Vault" | 10—20 ms | High-security operations, HashiCorp Vault |
AzureKeyVaultStringEncryptionProvider | "AzureKeyVault" | 10—30 ms | High-security operations, Azure environments |
Step 3 — Encrypt fields before persistence
Section titled “Step 3 — Encrypt fields before persistence”Inject IStringEncryptionService to encrypt and decrypt individual fields:
using Granit.Encryption;
namespace MyApp.Services;
public sealed class PatientService( AppDbContext db, IStringEncryptionService encryption){ public async Task<Guid> CreateAsync( string firstName, string lastName, string nirNumber, CancellationToken cancellationToken) { var patient = new Patient { FirstName = firstName, LastName = lastName, NirNumberEncrypted = encryption.Encrypt(nirNumber) };
db.Patients.Add(patient); await db.SaveChangesAsync(cancellationToken);
return patient.Id; }
public string? DecryptNir(string cipherText) => encryption.Decrypt(cipherText);}The AES-256-CBC provider generates a random IV for every encryption call
(RandomNumberGenerator.GetBytes(16)). The ciphertext format is
Base64(IV[16] || CipherText[N]) — the IV is extracted automatically during decryption.
Step 4 — Use Vault Transit for high-security operations
Section titled “Step 4 — Use Vault Transit for high-security operations”For data that requires centralized key management (encryption keys never leave Vault),
use ITransitEncryptionService:
using Granit.Vault;
namespace MyApp.Services;
public sealed class HealthRecordService(ITransitEncryptionService transit){ public async Task<string> EncryptDiagnosisAsync( string diagnosis, CancellationToken cancellationToken) { // Key name corresponds to a Transit key in Vault var encrypted = await transit.EncryptAsync( "health-records", diagnosis, cancellationToken); // Returns "vault:v1:..." -- version-tagged ciphertext return encrypted; }
public async Task<string> DecryptDiagnosisAsync( string cipherText, CancellationToken cancellationToken) => await transit.DecryptAsync( "health-records", cipherText, cancellationToken);}Step 5 — Encrypt cached values
Section titled “Step 5 — Encrypt cached values”For data stored in Redis or another distributed cache, use the [CacheEncrypted]
attribute to enable transparent AES-256 encryption of cached values:
using Granit.Caching;
namespace MyApp.Caching;
[CacheEncrypted]public sealed class PatientCacheItem{ public Guid Id { get; set; } public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty;}The ICacheService<PatientCacheItem> automatically encrypts before writing to Redis
and decrypts after reading. Even if the cache backend is compromised, the data is
unreadable.
public sealed class PatientCacheService( ICacheService<PatientCacheItem> cache, AppDbContext db){ public async Task<PatientCacheItem?> GetAsync( Guid id, CancellationToken cancellationToken) { var cacheKey = $"patient:{id}";
var cached = await cache.GetAsync(cacheKey, cancellationToken); if (cached is not null) { return cached; }
var patient = await db.Patients.FindAsync([id], cancellationToken); if (patient is null) { return null; }
var item = new PatientCacheItem { Id = patient.Id, FirstName = patient.FirstName, LastName = patient.LastName };
await cache.SetAsync(cacheKey, item, cancellationToken); return item; }}Step 6 — Encrypt settings automatically
Section titled “Step 6 — Encrypt settings automatically”Settings declared with IsEncrypted = true are encrypted at rest in the database.
The cache stores plaintext to avoid double encryption (Redis is already protected
by AesCacheValueEncryptor):
public sealed class AppSettingDefinitionProvider : ISettingDefinitionProvider{ public void Define(ISettingDefinitionContext context) { context.Add(new SettingDefinition("Integrations.ExternalApiKey") { IsEncrypted = true, IsVisibleToClients = false, Providers = { GlobalSettingValueProvider.ProviderName } }); }}Encryption architecture summary
Section titled “Encryption architecture summary”| Layer | Mechanism | Scope | Key management |
|---|---|---|---|
| Field-level (AES) | IStringEncryptionService | Individual entity properties | Passphrase via PBKDF2 |
| Field-level (Vault Transit) | ITransitEncryptionService | Individual entity properties | Vault-managed, auto-rotation |
| Field-level (Azure Key Vault) | IStringEncryptionService | Individual entity properties | Azure Key Vault RSA-OAEP-256 |
| Cache | [CacheEncrypted] | Entire cached object | Local AES-256 key |
| Settings | IsEncrypted = true | Setting values in database | IStringEncryptionService |
Next steps
Section titled “Next steps”- Manage application settings — encrypted settings with cascading resolution
- Vault and Encryption reference — full API and configuration reference
- Observability reference — monitor encryption operations with OpenTelemetry