Custom Key Store
If none of the built-in key store providers fit your infrastructure, you can implement the IKeyStore interface to build your own. This is useful for integrating with databases or secrets managers that Tayra does not support out of the box (e.g., Redis, Cosmos DB, GCP Secret Manager).
When to Build Custom
Consider a custom key store when:
- You use a storage backend not covered by the built-in providers
- You have an existing secrets management system you want to reuse
- You need specialized behavior (e.g., multi-region replication, custom encryption)
- You want to wrap an existing key store with caching, logging, or metrics
IKeyStore Interface
Your custom key store must implement all required methods of the IKeyStore interface:
public interface IKeyStore
{
// Required — core operations
Task StoreAsync(string keyId, byte[] key, CancellationToken ct = default);
Task<byte[]?> GetAsync(string keyId, CancellationToken ct = default);
Task DeleteAsync(string keyId, CancellationToken ct = default);
Task<bool> ExistsAsync(string keyId, CancellationToken ct = default);
Task DeleteByPrefixAsync(string prefix, CancellationToken ct = default);
// Optional — default implementations throw NotSupportedException
Task<IReadOnlyList<string>> ListKeyIdsAsync(
string prefix, CancellationToken ct = default);
Task<IReadOnlyList<KeyInfo>> GetKeysCreatedBeforeAsync(
DateTimeOffset cutoff, int limit = 100, CancellationToken ct = default);
}Method Contracts
| Method | Contract |
|---|---|
StoreAsync | Must be idempotent. If the key already exists, do nothing (do not overwrite). |
GetAsync | Return null if the key does not exist or has been deleted. Never throw for missing keys. |
DeleteAsync | Delete the key permanently. Must be safe to call on non-existent keys (no-op). |
ExistsAsync | Return true if the key exists and has not been deleted. |
DeleteByPrefixAsync | Delete all keys whose IDs start with the given prefix. Used for bulk crypto-shredding. |
ListKeyIdsAsync | Optional. Return all key IDs matching the prefix. Needed for key rotation. |
GetKeysCreatedBeforeAsync | Optional. Return keys older than the cutoff. Needed for data retention policies. |
Example Implementation
/// <summary>
/// Example custom key store implementation.
/// Replace with your own persistence logic (Redis, Cosmos DB, etc.).
/// </summary>
public class MyCustomKeyStore : IKeyStore
{
private readonly Dictionary<string, byte[]> _store = new();
private readonly object _lock = new();
public Task StoreAsync(string keyId, byte[] key, CancellationToken ct = default)
{
lock (_lock)
{
_store.TryAdd(keyId, key);
}
return Task.CompletedTask;
}
public Task<byte[]?> GetAsync(string keyId, CancellationToken ct = default)
{
lock (_lock)
{
return Task.FromResult(_store.GetValueOrDefault(keyId));
}
}
public Task DeleteAsync(string keyId, CancellationToken ct = default)
{
lock (_lock)
{
_store.Remove(keyId);
}
return Task.CompletedTask;
}
public Task<bool> ExistsAsync(string keyId, CancellationToken ct = default)
{
lock (_lock)
{
return Task.FromResult(_store.ContainsKey(keyId));
}
}
public Task DeleteByPrefixAsync(string prefix, CancellationToken ct = default)
{
lock (_lock)
{
var keysToRemove = _store.Keys
.Where(k => k.StartsWith(prefix, StringComparison.Ordinal))
.ToList();
foreach (var key in keysToRemove)
{
_store.Remove(key);
}
}
return Task.CompletedTask;
}
}Registration
Register your custom key store with the DI container:
var services = new ServiceCollection();
services.AddTayra(opts => opts.LicenseKey = licenseKey);
// Register your custom IKeyStore implementation
services.AddSingleton<IKeyStore, MyCustomKeyStore>();Singleton Lifetime
Register your key store as a singleton (AddSingleton). Tayra resolves IKeyStore once at startup and uses it for the lifetime of the application. If your key store holds external connections, implement IDisposable and the DI container will dispose it on shutdown.
Implementation Guidelines
Thread Safety
Your IKeyStore implementation must be thread-safe. Tayra may call key store methods concurrently from multiple threads. Use thread-safe collections, synchronization primitives, or inherently thread-safe clients (e.g., HttpClient, most database clients).
Idempotent Store
StoreAsync must be idempotent. If called twice with the same keyId, the second call should be a no-op. This is because Tayra may retry operations or call StoreAsync defensively. Overwriting an existing key with a different value would break decryption of previously encrypted data.
Best Practices
- Handle transient errors -- Implement retry logic with exponential backoff for network-dependent backends.
- Respect cancellation tokens -- Pass the
CancellationTokenthrough to all async calls. - Log appropriately -- Inject
ILogger<T>and log warnings for retries, errors for permanent failures. - Secure key material -- Ensure keys are encrypted at rest in your chosen backend. Never log key bytes.
- Clean up on delete -- When
DeleteAsyncis called, the key must be permanently removed. This is the crypto-shredding guarantee. - Support prefix operations --
DeleteByPrefixAsyncis critical for GDPR right-to-erasure. Ensure your backend supports efficient prefix-based queries or scans.
Wrapping an Existing Key Store
You can also create a decorator that wraps an existing IKeyStore to add cross-cutting concerns:
public class CachingKeyStore : IKeyStore
{
private readonly IKeyStore _inner;
private readonly IMemoryCache _cache;
public CachingKeyStore(IKeyStore inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}
public async Task<byte[]?> GetAsync(string keyId, CancellationToken ct = default)
{
if (_cache.TryGetValue(keyId, out byte[]? cached))
return cached;
var key = await _inner.GetAsync(keyId, ct);
if (key is not null)
_cache.Set(keyId, key, TimeSpan.FromMinutes(5));
return key;
}
// Delegate all other methods to _inner...
}Cache Invalidation
If you add a caching layer, make sure DeleteAsync evicts the key from the cache. Otherwise, crypto-shredding will appear to succeed but decryption will continue to work until the cache entry expires.
See Also
- Key Stores Overview -- Comparison of all built-in providers
- In-Memory Key Store -- Simple reference implementation
- PostgreSQL Key Store -- Production-ready example to study
