Skip to main content

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 8.0+ (or .NET 9/10 for latest features)
  • Install the required packages:
    dotnet add package Excalibur.Dispatch.Security
    dotnet add package Excalibur.Dispatch.Compliance.Abstractions
  • Familiarity with middleware concepts and encryption providers

Overview

The encryption architecture is built on three core components:

ComponentPurposePackage
IEncryptionProviderField-level encryption/decryptionExcalibur.Dispatch.Compliance.Abstractions
IKeyManagementProviderKey lifecycle and rotationExcalibur.Dispatch.Compliance.Abstractions
MessageEncryptionMiddlewarePipeline message encryptionExcalibur.Dispatch.Security
flowchart TB
subgraph Application["Application Layer"]
MSG[Message]
EVT[Domain Event]
ENT[Entity]
end

subgraph Middleware["Dispatch Pipeline"]
MEM[MessageEncryptionMiddleware]
end

subgraph Providers["Encryption Providers"]
AES[AesGcmEncryptionProvider]
REG[EncryptionProviderRegistry]
end

subgraph KeyMgmt["Key Management"]
KMP[IKeyManagementProvider]
AKV[Azure Key Vault]
AWS[AWS KMS]
HCV[HashiCorp Vault]
MEM2[In-Memory]
end

subgraph Storage["Storage Layer"]
ES[EncryptingEventStoreDecorator]
PS[EncryptingProjectionStoreDecorator]
IS[EncryptingInboxStoreDecorator]
OS[EncryptingOutboxStoreDecorator]
end

MSG --> MEM
MEM --> AES
EVT --> ES
ES --> AES
AES --> REG
REG --> KMP
KMP --> AKV
KMP --> AWS
KMP --> HCV
KMP --> MEM2

AES-256-GCM Encryption

The default encryption provider uses AES-256-GCM, providing authenticated encryption with associated data (AEAD).

Technical Specifications

ParameterValueDescription
AlgorithmAES-256-GCMNIST-approved, FIPS 140-2 compatible
Key Size256 bits (32 bytes)Maximum AES key strength
Nonce Size96 bits (12 bytes)GCM standard, randomly generated
Auth Tag128 bits (16 bytes)Maximum authentication tag size

Basic Usage

using Excalibur.Dispatch.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
};
Multi-Tenant Security

When TenantId is provided, it's included in the AAD (Associated Authenticated Data). This 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:

stateDiagram-v2
[*] --> Active: Create/Rotate
Active --> DecryptOnly: Rotate
DecryptOnly --> PendingDestruction: Schedule Deletion
PendingDestruction --> Destroyed: Retention Period Expires
Active --> Suspended: Security Concern
DecryptOnly --> Suspended: Security Concern
Suspended --> Active: Reactivate
Suspended --> Destroyed: Destroy
StatusEncryptDecryptDescription
ActiveCurrent key for new encryption
DecryptOnlyRotated out, can still decrypt old data
PendingDestructionAwaiting deletion, no operations
DestroyedPermanently deleted
SuspendedFrozen 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.Dispatch.Compliance;

// Development/testing only — keys are stored in memory
builder.Services.AddDevEncryption();
// Or: builder.Services.AddEncryption(e => e.UseInMemoryKeyManagement("dev"));
Development Only

InMemoryKeyManagementProvider stores keys in memory. Keys are lost on restart. Never use in production.

Azure Key Vault

using Excalibur.Dispatch.Compliance.Azure;

builder.Services.AddAzureKeyVaultKeyManagement(options =>
{
options.VaultUri = new Uri("https://my-vault.vault.azure.net/");

// Option 1: Managed Identity (recommended)
options.UseManagedIdentity = true;

// Option 2: Service Principal
options.TenantId = configuration["Azure:TenantId"];
options.ClientId = configuration["Azure:ClientId"];
options.ClientSecret = configuration["Azure:ClientSecret"];

// Key settings
options.KeyName = "dispatch-encryption-key";
options.KeyRotationDays = 90;
});

AWS KMS

using Excalibur.Dispatch.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.Dispatch.Compliance.Vault;

builder.Services.AddVaultKeyManagement(options =>
{
options.Address = "https://vault.example.com:8200";

// Authentication
options.Token = configuration["Vault:Token"];
// Or AppRole
options.RoleId = configuration["Vault:RoleId"];
options.SecretId = configuration["Vault:SecretId"];

// Transit engine path
options.TransitMountPath = "transit";
options.KeyName = "dispatch-encryption";
});

Message-Level Encryption

MessageEncryptionMiddleware

The middleware provides transparent encryption/decryption in the dispatch pipeline:

using Excalibur.Dispatch.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.Dispatch.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

sequenceDiagram
participant App as Application
participant MW as EncryptionMiddleware
participant Handler as Handler
participant Transport as Transport

Note over App,Transport: Outgoing Message
App->>MW: Send Command
MW->>MW: Check ISensitiveMessage
MW->>Handler: Process
Handler-->>MW: Result
MW->>MW: Encrypt payload
MW->>Transport: Encrypted message

Note over App,Transport: Incoming Message
Transport->>MW: Encrypted message
MW->>MW: Decrypt payload
MW->>Handler: Process
Handler-->>MW: Result
MW-->>App: Response

Field-Level Encryption

[PersonalData] Attribute

Mark fields for automatic encryption at rest:

using Excalibur.Dispatch.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.Dispatch.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

using Excalibur.EventSourcing.Encryption.Decorators;

// Register the encrypting decorator
builder.Services.AddEventSourcing(options =>
{
options.UseEncryption(encryption =>
{
encryption.Mode = EncryptionMode.EncryptAndDecrypt;
encryption.DefaultPurpose = "event-data";
});
});

Encryption Modes

ModeWrite BehaviorRead BehaviorUse Case
EncryptAndDecryptEncrypt allDecrypt allNormal operation
EncryptNewDecryptAllEncrypt newDecrypt old & newAfter migration
DecryptOnlyWritePlaintextWrite plaintextDecrypt if encryptedDuring migration
DecryptOnlyReadOnlyReject writesDecrypt onlyMaintenance mode
DisabledPass throughPass throughTesting/debugging

Available Decorators

DecoratorPackagePurpose
EncryptingEventStoreDecoratorExcalibur.EventSourcingEvent store encryption
EncryptingProjectionStoreDecoratorExcalibur.EventSourcingProjection encryption
EncryptingInboxStoreDecoratorExcalibur.Dispatch.ComplianceInbox message encryption
EncryptingOutboxStoreDecoratorExcalibur.Dispatch.ComplianceOutbox 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.AddEventSourcing(options =>
{
options.UseEncryption(e => e.Mode = EncryptionMode.EncryptNewDecryptAll);
});

// Phase 2: Run background re-encryption job
// (See Key Rotation section below)

// Phase 3: All data encrypted, normal operation
builder.Services.AddEventSourcing(options =>
{
options.UseEncryption(e => e.Mode = EncryptionMode.EncryptAndDecrypt);
});

Key Rotation

IReEncryptionService

Re-encrypt data when keys rotate:

using Excalibur.Dispatch.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.Dispatch.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

PlatformFIPS Mode
WindowsRegistry setting: HKLM\SYSTEM\CurrentControlSet\Control\Lsa\FIPSAlgorithmPolicy
Linux/proc/sys/crypto/fips_enabled = 1
AzureAzure FIPS-enabled VMs
AWSAWS GovCloud, FIPS endpoints

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

FeatureFedRAMPGDPRSOC 2HIPAA
AES-256-GCMSC-28Art 32C2§164.312(a)(2)(iv)
Key rotationSC-12Art 32C2§164.312(a)(2)(iv)
Tenant isolationAC-4Art 25CC5§164.312(a)(1)
FIPS complianceSC-13--§164.312(a)(2)(iv)
Crypto-shredding-Art 17C3Disposal
Audit loggingAU-3Art 30CC4§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 InMemoryKeyManagementProvider in production
  • Skip AAD (Associated Authenticated Data) for tenant-bound data
  • Ignore DecryptOnly keys - they contain historical data
  • Delete keys without a retention period

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