Key Store
The IKeyStore interface is Tayra's public extension point for key persistence. This is the one interface you implement when building a custom key store. Tayra never manages key storage directly — it delegates entirely to the key store implementation, which can be backed by PostgreSQL, HashiCorp Vault, Azure Key Vault, AWS Parameter Store, or a simple in-memory dictionary.
TIP
IKeyStore is the only Tayra interface you need to implement directly. For all other operations (encryption, shredding, rotation), use ITayra.
IKeyStore Interface
public interface IKeyStore
{
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);
Task<IReadOnlyList<string>> ListKeyIdsAsync(string prefix, CancellationToken ct = default);
Task<IReadOnlyList<KeyInfo>> GetKeysCreatedBeforeAsync(
DateTimeOffset cutoff, int limit = 100, CancellationToken ct = default);
}Method Reference
| Method | Required | Purpose |
|---|---|---|
StoreAsync | Yes | Stores a key. Must be idempotent -- storing the same key ID twice should be a no-op, not an error. |
GetAsync | Yes | Retrieves a key by ID. Returns null if the key does not exist (has been deleted/shredded). |
DeleteAsync | Yes | Deletes a key. This is the crypto-shredding primitive. Must be idempotent. |
ExistsAsync | Yes | Returns true if the key exists, false otherwise. |
DeleteByPrefixAsync | Yes | Deletes all keys matching a prefix. Used for bulk crypto-shredding (e.g., all groups for a subject). |
ListKeyIdsAsync | Optional | Lists key IDs matching a prefix. Used for key rotation to discover versioned keys. Throws NotSupportedException by default. |
GetKeysCreatedBeforeAsync | Optional | Returns keys created before a cutoff time. Used by the key retention background service. Throws NotSupportedException by default. |
Idempotency Contract
Key store implementations must follow these idempotency rules:
| Operation | Behavior |
|---|---|
StoreAsync with existing key ID | No-op. Does not overwrite the existing key. Does not throw. |
DeleteAsync with non-existent key ID | No-op. Does not throw. |
DeleteByPrefixAsync with no matching keys | No-op. Does not throw. |
This is critical because the DefaultCryptoEngine calls StoreAsync as part of GetOrCreateKeyAsync. A race condition between two threads creating the same key must not corrupt the key -- the first writer wins, and subsequent calls are no-ops.
Built-in Implementations
| Package | Backend | Thread-Safe | ListKeyIdsAsync | GetKeysCreatedBeforeAsync |
|---|---|---|---|---|
Tayra.Core (built-in) | ConcurrentDictionary | Yes | Yes | No |
Tayra.KeyStore.PostgreSql | PostgreSQL table | Yes | Yes | Yes |
Tayra.KeyStore.Vault | HashiCorp Vault KV v2 | Yes | Yes | No |
Tayra.KeyStore.AzureKeyVault | Azure Key Vault secrets | Yes | Yes | No |
Tayra.KeyStore.AwsParameterStore | AWS SSM Parameter Store | Yes | Yes | No |
Registration
By default, AddTayra() uses the built-in InMemoryKeyStore. For production, each key store package provides an extension method that chains from AddTayra():
// Default: uses InMemoryKeyStore (suitable for development and testing)
services.AddTayra(opts => opts.LicenseKey = licenseKey);
// Production key stores — choose one:
services.AddTayra(opts => opts.LicenseKey = licenseKey)
.UsePostgreSqlKeyStore(connectionString);
services.AddTayra(opts => opts.LicenseKey = licenseKey)
.UseVaultKeyStore(opts => { /* VaultSharp config */ });
services.AddTayra(opts => opts.LicenseKey = licenseKey)
.UseAzureKeyVaultKeyStore(opts => { /* Azure config */ });
services.AddTayra(opts => opts.LicenseKey = licenseKey)
.UseAwsKeyStore(opts => { /* AWS config */ });One Key Store Per Application
Register exactly one IKeyStore implementation. If multiple are registered, the last one wins (standard DI behavior). Tayra does not support multi-store routing out of the box.
Custom Implementation
To create a custom key store, implement IKeyStore and register it:
public class RedisKeyStore : IKeyStore
{
public Task StoreAsync(string keyId, byte[] key, CancellationToken ct = default)
{
// Store in Redis
}
public Task<byte[]?> GetAsync(string keyId, CancellationToken ct = default)
{
// Retrieve from Redis
}
// ... other methods
}
// Register custom key store before AddTayra()
services.AddSingleton<IKeyStore, RedisKeyStore>();
services.AddTayra(opts => opts.LicenseKey = licenseKey);Your implementation must be:
- Thread-safe -- Tayra registers key stores as singletons and calls them concurrently.
- Idempotent -- Follow the idempotency contract described above.
- Secure -- Encryption keys are sensitive material. Store them in encrypted-at-rest storage where possible.
Required vs. Optional Methods
The last two methods in IKeyStore have default implementations that throw NotSupportedException. They are only needed for specific features:
ListKeyIdsAsync-- Required for key rotation (ITayra.RotateKeyAsync). If your key store does not support this, key rotation will fail at runtime.GetKeysCreatedBeforeAsync-- Required for the key retention background service. If your key store does not support this, automatic key expiration is not available.
All five built-in key stores implement ListKeyIdsAsync. Only the PostgreSQL key store implements GetKeysCreatedBeforeAsync (because it stores a created_at timestamp per key).
See Also
- Crypto Engine -- How the key store is used by the crypto engine
- Encryption -- What the keys protect
- Field Encrypter -- The high-level encryption API
- Installation -- Package list and key store options
