Skip to content

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

csharp
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

MethodRequiredPurpose
StoreAsyncYesStores a key. Must be idempotent -- storing the same key ID twice should be a no-op, not an error.
GetAsyncYesRetrieves a key by ID. Returns null if the key does not exist (has been deleted/shredded).
DeleteAsyncYesDeletes a key. This is the crypto-shredding primitive. Must be idempotent.
ExistsAsyncYesReturns true if the key exists, false otherwise.
DeleteByPrefixAsyncYesDeletes all keys matching a prefix. Used for bulk crypto-shredding (e.g., all groups for a subject).
ListKeyIdsAsyncOptionalLists key IDs matching a prefix. Used for key rotation to discover versioned keys. Throws NotSupportedException by default.
GetKeysCreatedBeforeAsyncOptionalReturns 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:

OperationBehavior
StoreAsync with existing key IDNo-op. Does not overwrite the existing key. Does not throw.
DeleteAsync with non-existent key IDNo-op. Does not throw.
DeleteByPrefixAsync with no matching keysNo-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

PackageBackendThread-SafeListKeyIdsAsyncGetKeysCreatedBeforeAsync
Tayra.Core (built-in)ConcurrentDictionaryYesYesNo
Tayra.KeyStore.PostgreSqlPostgreSQL tableYesYesYes
Tayra.KeyStore.VaultHashiCorp Vault KV v2YesYesNo
Tayra.KeyStore.AzureKeyVaultAzure Key Vault secretsYesYesNo
Tayra.KeyStore.AwsParameterStoreAWS SSM Parameter StoreYesYesNo

Registration

By default, AddTayra() uses the built-in InMemoryKeyStore. For production, each key store package provides an extension method that chains from AddTayra():

csharp
// 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:

csharp
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