Encryption Architecture
Excalibur provides comprehensive encryption capabilities for data at rest and in transit, supporting enterprise compliance requirements including FedRAMP, GDPR, SOC 2, and HIPAA.
Before You Start
- .NET 10.0
- Install the required packages:
dotnet add package Excalibur.Securitydotnet add package Excalibur.Compliance.Abstractions
- Familiarity with middleware concepts and encryption providers
Overview
The encryption architecture is built on three core components:
| Component | Purpose | Package |
|---|---|---|
IEncryptionProvider | Field-level encryption/decryption | Excalibur.Compliance.Abstractions |
IKeyManagementProvider | Key lifecycle and rotation | Excalibur.Compliance.Abstractions |
MessageEncryptionMiddleware | Pipeline message encryption | Excalibur.Security |
AES-256-GCM Encryption
The default encryption provider uses AES-256-GCM, providing authenticated encryption with associated data (AEAD).
Technical Specifications
| Parameter | Value | Description |
|---|---|---|
| Algorithm | AES-256-GCM | NIST-approved, FIPS 140-2 compatible |
| Key Size | 256 bits (32 bytes) | Maximum AES key strength |
| Nonce Size | 96 bits (12 bytes) | GCM standard, randomly generated |
| Auth Tag | 128 bits (16 bytes) | Maximum authentication tag size |
Basic Usage
using Excalibur.Compliance;
// Inject IEncryptionProvider
public class MyService
{
private readonly IEncryptionProvider _encryption;
public MyService(IEncryptionProvider encryption)
{
_encryption = encryption;
}
public async Task<EncryptedData> EncryptSensitiveDataAsync(
byte[] plaintext,
CancellationToken ct)
{
var context = new EncryptionContext
{
TenantId = "tenant-123",
Purpose = "pii-field"
};
return await _encryption.EncryptAsync(plaintext, context, ct);
}
public async Task<byte[]> DecryptSensitiveDataAsync(
EncryptedData encrypted,
CancellationToken ct)
{
var context = new EncryptionContext
{
TenantId = "tenant-123"
};
return await _encryption.DecryptAsync(encrypted, context, ct);
}
}
Encryption Context
The EncryptionContext provides tenant isolation, key selection, and associated authenticated data (AAD):
var context = new EncryptionContext
{
// Key selection
KeyId = "my-encryption-key", // Specific key (optional)
KeyVersion = 3, // Specific version (optional)
// Multi-tenant isolation
TenantId = "tenant-123", // Binds ciphertext to tenant
// Operational metadata
Purpose = "pii-field", // For audit and key selection
Classification = DataClassification.Confidential,
// Additional authenticated data
AssociatedData = Encoding.UTF8.GetBytes("order-123"),
// Compliance
RequireFipsCompliance = true // Enforce FIPS mode
};
TenantId is always included in the AAD (Associated Authenticated Data) as a length-prefixed field, even when empty or null. This ensures a stable, unambiguous AAD format across all tenancy modes. When a non-empty TenantId is provided, it cryptographically binds the ciphertext to the tenant -- attempting to decrypt with a different tenant ID will fail authentication, even with the correct key.
Key Management
Key Lifecycle
Keys follow a defined lifecycle:
| Status | Encrypt | Decrypt | Description |
|---|---|---|---|
Active | ✅ | ✅ | Current key for new encryption |
DecryptOnly | ❌ | ✅ | Rotated out, can still decrypt old data |
PendingDestruction | ❌ | ❌ | Awaiting deletion, no operations |
Destroyed | ❌ | ❌ | Permanently deleted |
Suspended | ❌ | ❌ | Frozen due to security concern |
IKeyManagementProvider Interface
public interface IKeyManagementProvider
{
// Key retrieval
Task<KeyMetadata?> GetKeyAsync(string keyId, CancellationToken ct);
Task<KeyMetadata?> GetKeyVersionAsync(string keyId, int version, CancellationToken ct);
Task<KeyMetadata?> GetActiveKeyAsync(string? purpose, CancellationToken ct);
Task<IReadOnlyList<KeyMetadata>> ListKeysAsync(KeyStatus? status, string? purpose, CancellationToken ct);
// Key lifecycle
Task<KeyRotationResult> RotateKeyAsync(string keyId, EncryptionAlgorithm algorithm,
string? purpose, DateTimeOffset? expiresAt, CancellationToken ct);
Task<bool> SuspendKeyAsync(string keyId, string reason, CancellationToken ct);
Task<bool> DeleteKeyAsync(string keyId, int retentionDays, CancellationToken ct);
}
Key Management Providers
In-Memory (Development Only)
using Excalibur.Compliance;
// Development/testing only — keys are stored in memory
builder.Services.AddDevEncryption();
// Or: builder.Services.AddEncryption(e => e.UseInMemoryKeyManagement("dev"));
InMemoryKeyManagementProvider stores keys in memory. Keys are lost on restart. Never use in production.
Azure Key Vault
using Excalibur.Compliance.Azure;
builder.Services.AddAzureKeyVaultKeyManagement(azure =>
{
azure.VaultUri(new Uri("https://my-vault.vault.azure.net/"))
// Key naming
.KeyNamePrefix("dispatch-encryption-")
// Require Premium (HSM-backed) keys in production
.RequirePremiumTier();
});
// Authentication uses DefaultAzureCredential (Managed Identity, environment,
// or developer credentials) unless a custom TokenCredential is supplied.
AWS KMS
using Excalibur.Compliance.Aws;
builder.Services.AddAwsKmsKeyManagement(options =>
{
options.Region = "us-east-1";
options.KeyId = "arn:aws:kms:us-east-1:123456789:key/12345678-...";
// Optional: Custom endpoint for LocalStack testing
options.ServiceUrl = configuration["AWS:ServiceUrl"];
// Key alias for rotation
options.KeyAlias = "alias/dispatch-encryption";
});
HashiCorp Vault
using Excalibur.Compliance.Vault;
// Core connection settings via the fluent builder
builder.Services.AddVaultKeyManagement(vault =>
vault.VaultUri(new Uri("https://vault.example.com:8200"))
.TransitMountPath("transit")
.KeyNamePrefix("dispatch-")); // Keys named: dispatch-{keyId}
// Authentication via Configure<VaultOptions> (grouped Auth sub-options)
builder.Services.Configure<VaultOptions>(options =>
{
// Token auth
options.Auth.AuthMethod = VaultAuthMethod.Token;
options.Auth.Token = configuration["Vault:Token"];
// Or AppRole auth
// options.Auth.AuthMethod = VaultAuthMethod.AppRole;
// options.Auth.AppRoleId = configuration["Vault:RoleId"];
// options.Auth.AppRoleSecretId = configuration["Vault:SecretId"];
});
Message-Level Encryption
MessageEncryptionMiddleware
The middleware provides transparent encryption/decryption in the dispatch pipeline:
using Excalibur.Security;
builder.Services.AddDispatch()
.AddMessageEncryption(options =>
{
options.Enabled = true;
options.EncryptByDefault = false; // Only encrypt marked messages
options.DefaultAlgorithm = EncryptionAlgorithm.Aes256Gcm;
options.ExcludedMessageTypes = new HashSet<string> { "HealthCheckQuery" };
});
ISensitiveMessage Marker
Mark messages containing sensitive data for automatic encryption:
using Excalibur.Security;
// Messages implementing ISensitiveMessage are automatically encrypted
public class CreatePatientAction : IDispatchAction, ISensitiveMessage
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string SSN { get; set; } // Sensitive!
}
// Regular messages are not encrypted by default
public class GetOrderAction : IDispatchAction<Order>
{
public Guid OrderId { get; set; }
}
Pipeline Behavior
Field-Level Encryption
[PersonalData] Attribute
Mark fields for automatic encryption at rest:
using Excalibur.Compliance;
public class Patient
{
public Guid Id { get; set; }
[PersonalData] // Encrypted at rest
public string FirstName { get; set; }
[PersonalData]
public string LastName { get; set; }
[PersonalData]
[Sensitive] // Additional classification
public string SSN { get; set; }
public DateTime DateOfBirth { get; set; } // NOT encrypted
}
[EncryptedField] Attribute
For more control over field encryption:
using Excalibur.Compliance;
public class PaymentInfo
{
[EncryptedField(
Purpose = "payment-data",
Algorithm = EncryptionAlgorithm.Aes256Gcm)]
public string CardNumber { get; set; }
[EncryptedField(Purpose = "payment-data")]
public string CVV { get; set; }
}
Encrypting Store Decorators
Transparent encryption for persistence layers using the decorator pattern:
Event Store Encryption
// AddEventSourcingCryptoShredding wires the encrypting decorators across the event
// store, inbox and outbox. Behaviour is configured on EncryptionOptions.
builder.Services.AddEncryption(encryption => encryption
.UseKeyManagement<AesGcmEncryptionProvider>("primary")
.ConfigureOptions(options =>
{
options.Mode = EncryptionMode.EncryptAndDecrypt;
options.DefaultPurpose = "event-data";
}));
builder.Services.AddEventSourcingCryptoShredding();
Encryption Modes
| Mode | Write Behavior | Read Behavior | Use Case |
|---|---|---|---|
EncryptAndDecrypt | Encrypt all | Decrypt all | Normal operation |
EncryptNewDecryptAll | Encrypt new | Decrypt old & new | After migration |
DecryptOnlyWritePlaintext | Write plaintext | Decrypt if encrypted | During migration |
DecryptOnlyReadOnly | Reject writes | Decrypt only | Maintenance mode |
Disabled | Pass through | Pass through | Testing/debugging |
Available Decorators
| Decorator | Package | Purpose |
|---|---|---|
EncryptingEventStoreDecorator | Excalibur.EventSourcing | Event store encryption |
EncryptingProjectionStoreDecorator | Excalibur.EventSourcing | Projection encryption |
EncryptingInboxStoreDecorator | Excalibur.Compliance | Inbox message encryption |
EncryptingOutboxStoreDecorator | Excalibur.Compliance | Outbox message encryption |
Mixed-Mode Migration
The decorators support seamless migration from plaintext to encrypted data:
// Phase 1: Start encrypting new data, decrypt both old and new
builder.Services.Configure<EncryptionOptions>(o =>
o.Mode = EncryptionMode.EncryptNewDecryptAll);
// Phase 2: Run background re-encryption job
// (See Key Rotation section below)
// Phase 3: All data encrypted, normal operation
builder.Services.Configure<EncryptionOptions>(o =>
o.Mode = EncryptionMode.EncryptAndDecrypt);
});
Key Rotation
IReEncryptionService
Re-encrypt data when keys rotate:
using Excalibur.Compliance;
public class KeyRotationJob : BackgroundService
{
private readonly IReEncryptionService _reEncryption;
private readonly IKeyManagementProvider _keyManagement;
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Rotate key
var result = await _keyManagement.RotateKeyAsync(
keyId: "my-key",
algorithm: EncryptionAlgorithm.Aes256Gcm,
purpose: "event-data",
expiresAt: DateTimeOffset.UtcNow.AddDays(90),
ct);
// Re-encrypt existing data with new provider
var options = new ReEncryptionOptions
{
SourceProviderId = "old-provider",
TargetProviderId = "new-provider",
BatchSize = 1000,
ContinueOnError = true
};
var processed = 0;
await foreach (var item in _reEncryption.ReEncryptBatchAsync(
GetEntitiesAsync(), options, ct))
{
if (item.Success)
processed += item.FieldsReEncrypted;
}
_logger.LogInformation("Re-encrypted {Count} fields", processed);
}
}
Lazy Re-Encryption
Configure lazy migration to automatically re-encrypt plaintext data on read/write:
using Excalibur.Compliance;
builder.Services.Configure<EncryptionOptions>(options =>
{
options.LazyMigrationEnabled = true;
options.LazyMigrationMode = LazyMigrationMode.OnRead;
});
FIPS 140-2 Compliance
Validation
// Check if running in FIPS mode
var provider = serviceProvider.GetRequiredService<IEncryptionProvider>();
var isFipsCompliant = await provider.ValidateFipsComplianceAsync(ct);
if (!isFipsCompliant)
{
_logger.LogWarning("System is not running in FIPS 140-2 mode");
}
Configuration
builder.Services.AddEncryption(options =>
{
options.RequireFipsComplianceByDefault = true;
});
// Per-operation enforcement
var context = new EncryptionContext
{
RequireFipsCompliance = true // Throws if not FIPS compliant
};
Platform Requirements
| Platform | FIPS Mode |
|---|---|
| Windows | Registry value FipsAlgorithmPolicy under HKLM\SYSTEM\CurrentControlSet\Control\Lsa |
| Linux | /proc/sys/crypto/fips_enabled = 1 |
| Azure | Azure FIPS-enabled VMs |
| AWS | AWS GovCloud, FIPS endpoints |
How detection works
On Windows, FIPS status is read from the FipsAlgorithmPolicy registry value named above. On Linux it is read from /proc/sys/crypto/fips_enabled.
Detection previously read CryptoConfig.AllowOnlyFipsAlgorithms. .NET Core and later pin that property to false and never populate it from the host policy, so every Windows host reported not FIPS compliant, including one genuinely running under FIPS policy. That result feeds the SOC 2 encryption control validator, so evidence generated on a Windows host you believe is FIPS-enabled recorded a false negative. Re-generate it.
Unconfirmed is not the same as disabled
FipsDetectionResult carries a boolean IsFipsEnabled and a ValidationDetails string, and there are three outcomes rather than two:
| Outcome | IsFipsEnabled | What it means |
|---|---|---|
| Policy on | true | The host policy enables FIPS. |
| Policy off | false | The host policy is present and disabled — a deployment decision. |
| Policy unreadable | false | The value is absent or inaccessible. ValidationDetails states that compliance is unconfirmed. |
The third outcome reports false because the check cannot assert compliance it did not establish — but it is a different problem from a policy deliberately turned off. Read ValidationDetails, not only the boolean. An unconfirmed result usually means the process cannot read the registry key and is an operational fault to resolve; reporting it as a settled not compliant would assert something the check never determined.
AesGcmEncryptionProvider resolves this through the detector rather than reading the runtime property itself, so the provider and the compliance report cannot disagree. It accepts an optional IFipsDetector; hosts that supply none are unaffected.
Crypto-Shredding (GDPR)
For GDPR right-to-erasure compliance, delete the encryption key to render all data unrecoverable:
// Crypto-shredding for GDPR compliance
public class GdprErasureHandler
{
private readonly IKeyManagementProvider _keyManagement;
public async Task ExecuteErasureAsync(string userId, CancellationToken ct)
{
// User's data was encrypted with a user-specific key
var userKeyId = $"user-{userId}-pii";
// Delete the key - all encrypted data becomes unrecoverable
await _keyManagement.DeleteKeyAsync(
keyId: userKeyId,
retentionDays: 7, // Grace period for recovery
ct);
}
}
Compliance Mapping
| Feature | FedRAMP | GDPR | SOC 2 | HIPAA |
|---|---|---|---|---|
| AES-256-GCM | SC-28 | Art 32 | C2 | §164.312(a)(2)(iv) |
| Key rotation | SC-12 | Art 32 | C2 | §164.312(a)(2)(iv) |
| Tenant isolation | AC-4 | Art 25 | CC5 | §164.312(a)(1) |
| FIPS compliance | SC-13 | - | - | §164.312(a)(2)(iv) |
| Crypto-shredding | - | Art 17 | C3 | Disposal |
| Audit logging | AU-3 | Art 30 | CC4 | §164.312(b) |
Best Practices
Do
- Use tenant-specific encryption contexts for multi-tenant applications
- Rotate keys regularly (90 days recommended)
- Enable FIPS mode for government/healthcare workloads
- Use cloud KMS (Azure Key Vault, AWS KMS) in production
- Test key rotation and re-encryption procedures
Don't
- Store encryption keys in code or configuration files
- Use
InMemoryKeyManagementProviderin production - Skip AAD (Associated Authenticated Data) for tenant-bound data
- Ignore
DecryptOnlykeys - they contain historical data - Delete keys without a retention period
Related Documentation
- Compliance Quick Start - Getting started with compliance features
- Audit Logging - Security audit trails
- GDPR Compliance - Data protection requirements
- FedRAMP Compliance - Federal security requirements
See Also
- Encryption Providers — Detailed configuration for AES-GCM, Azure Key Vault, AWS KMS, and HashiCorp Vault providers
- Authorization & Audit (A3) — Activity-based authorization and access control for encrypted resources
- Data Masking — Field-level data masking and PII redaction for compliance