Skip to content

Key Stores ​

Tayra uses a pluggable key store architecture to persist encryption keys. Every key store implements the IKeyStore interface, giving you freedom to choose the backend that best fits your infrastructure.

Available Providers ​

ProviderPackageUse CasePersistenceEncryption at Rest
In-MemoryTayra.Core (built-in)Unit tests, throwaway demosNoNo
SQLiteTayra.KeyStore.SqliteLocal development (zero-config)YesNo (raw key bytes)
PostgreSQLTayra.KeyStore.PostgreSqlLocal dev or self-managed productionYesNo (requires hardening)
HashiCorp VaultTayra.KeyStore.VaultProductionYesYes (seal)
Azure Key VaultTayra.KeyStore.AzureKeyVaultProductionYesYes (HSM-backed)
AWS Parameter StoreTayra.KeyStore.AwsParameterStoreProductionYesYes (KMS)
AWS Secrets ManagerTayra.KeyStore.AwsSecretsManagerProduction (per-tenant)YesYes (KMS)
AWS Aurora (IAM auth)Tayra.KeyStore.AwsAuroraProduction (AWS-native PG)YesNo (requires hardening)

Choosing a Key Store ​

EnvironmentRecommended ProviderWhy
Unit/integration testsIn-MemoryZero configuration, no external dependencies
Local developmentSQLiteZero-config file-based persistence, no server needed
Local development (with PG)PostgreSQLPersistent keys, useful if you already run PostgreSQL locally
Self-managed productionPostgreSQL (with hardening)For teams that don't use cloud secrets managers
Production on AWS AuroraAWS Aurora (IAM auth)PostgreSQL compatibility with ephemeral IAM DB auth tokens
ProductionVault, Azure Key Vault, or AWS KMSHSM-backed encryption, access auditing, key wrapping

SQLite - Development Only (in either role)

The SQLite key store stores raw key bytes without envelope encryption. It is for local development only - including when used as the master keystore via .WithSqliteMasterKey(...). Production deployments must use a hardened secrets manager regardless of role.

AWS Secrets Manager - Per-Secret Pricing

Secrets Manager bills $0.40 per secret per month. With Tayra's default per-data-subject key model, a customer base of 100k users costs ~$40k/month. Use AWS Parameter Store for per-subject keys; use Secrets Manager only for per-tenant key models or when policy mandates it.

PostgreSQL - Requires Hardening for Production

The PostgreSQL key store stores raw key bytes in a database table. It can be used in production only with proper hardening (TDE, TLS, pgAudit, least-privilege access). See the Production Security Guide for the full checklist. Without hardening, use it for development only.

The IKeyStore Interface ​

All key stores implement IKeyStore, which defines these operations:

cs
/// <summary>
/// Abstraction for storing and retrieving encryption keys.
/// Implementations provide persistence (PostgreSQL, Vault, in-memory, etc.).
/// </summary>
public interface IKeyStore
{
    /// <summary>
    /// Stores a key. Implementations must be idempotent and first-writer-wins:
    /// if the key ID already exists, the call is a no-op and the existing key bytes
    /// are kept. Callers that need the authoritative key material must read it back
    /// with <see cref="GetAsync"/> after storing rather than assuming the bytes they
    /// passed were persisted.
    /// </summary>
    Task StoreAsync(string keyId, byte[] key, CancellationToken ct = default);

    /// <summary>
    /// Retrieves a key by ID. Returns null if the key does not exist (e.g., has been shredded).
    /// </summary>
    Task<byte[]?> GetAsync(string keyId, CancellationToken ct = default);

    /// <summary>
    /// Retrieves multiple keys in a single call. Returns a map of found key ID to key bytes.
    /// Key IDs that do not exist (absent or shredded) are simply NOT present in the returned map,
    /// so callers can detect misses by looking up each requested ID.
    /// </summary>
    /// <remarks>
    /// The default implementation loops <see cref="GetAsync"/> so every store works out of the box.
    /// Fast backends (PostgreSQL, SQLite, in-memory) override this with a single batched query to
    /// collapse millions of per-subject round trips into a handful of bulk reads; stores with no
    /// native multi-get (Vault, cloud secret stores) fall back to the loop.
    /// </remarks>
    async Task<IReadOnlyDictionary<string, byte[]>> GetManyAsync(
        IReadOnlyCollection<string> keyIds, CancellationToken ct = default)
    {
        ArgumentNullException.ThrowIfNull(keyIds);

        var result = new Dictionary<string, byte[]>(keyIds.Count);
        foreach (var keyId in keyIds)
        {
            var key = await GetAsync(keyId, ct).ConfigureAwait(false);
            if (key is not null)
            {
                result[keyId] = key;
            }
        }

        return result;
    }

    /// <summary>
    /// Deletes a key by ID. This is the crypto-shredding operation.
    /// </summary>
    Task DeleteAsync(string keyId, CancellationToken ct = default);

    /// <summary>
    /// Checks whether a key exists.
    /// </summary>
    Task<bool> ExistsAsync(string keyId, CancellationToken ct = default);

    /// <summary>
    /// Deletes all keys matching the given prefix. Used for bulk crypto-shredding
    /// (e.g., deleting all keys for a data subject across groups).
    /// </summary>
    Task DeleteByPrefixAsync(string prefix, CancellationToken ct = default);

    /// <summary>
    /// Lists all key IDs matching the given prefix.
    /// Used for key rotation to discover versioned keys.
    /// </summary>
    Task<IReadOnlyList<string>> ListKeyIdsAsync(string prefix, CancellationToken ct = default)
        => throw new NotSupportedException("This key store does not support listing key IDs.");

    /// <summary>
    /// Returns keys that were created before the specified cutoff time.
    /// Used by the key retention background service to find expired keys.
    /// </summary>
    Task<IReadOnlyList<Retention.KeyInfo>> GetKeysCreatedBeforeAsync(
        DateTimeOffset cutoff, int limit = 100, CancellationToken ct = default)
        => throw new NotSupportedException("This key store does not support querying keys by creation time.");
}
anchor
  • StoreAsync - Stores a key. Implementations should be idempotent (no-op if the key already exists).
  • GetAsync - Retrieves a key by ID. Returns null if the key has been deleted (crypto-shredded).
  • GetManyAsync - Retrieves several keys in one call; missing keys are absent from the result. The default implementation loops GetAsync; fast backends override it with a batched query.
  • DeleteAsync - Deletes a key. This is the crypto-shredding operation.
  • ExistsAsync - Checks whether a key exists without retrieving its value.
  • DeleteByPrefixAsync - Bulk crypto-shredding by key prefix (e.g., delete all keys for a data subject).
  • ListKeyIdsAsync - Lists key IDs matching a prefix. Used for key rotation discovery.
  • GetKeysCreatedBeforeAsync - Finds keys older than a cutoff time. Used for data retention policies.

INFO

ListKeyIdsAsync and GetKeysCreatedBeforeAsync have default implementations that throw NotSupportedException. Not all providers support these operations.

Fail-Closed on Key-Store Errors ​

If the key store cannot be reached while resolving a key - a connectivity issue, bad credentials, or a missing schema/table - encryption and decryption fail closed: Tayra raises a TayraKeyStoreUnavailableException (with the underlying store error as its InnerException) rather than persisting unprotected data or letting a raw, non-attributable store error surface downstream. Because key resolution runs before any field is encrypted, the exception is raised with your object unmodified, so no half-encrypted or corrupt data reaches your database.

cs
try
{
    await session.SaveChangesAsync();
}
catch (TayraKeyStoreUnavailableException ex)
{
    // ex.KeyId, ex.Operation ("load" / "store" / "list"), and ex.InnerException
    // point at the real cause (e.g. the key-store schema does not exist).
}
anchor

Registration Pattern ​

Every key store follows the same chained registration pattern. Call services.AddTayra() to register core services and chain the provider-specific Use* method:

cs
var services = new ServiceCollection();
services.AddTayra(opts => opts.LicenseKey = licenseKey);
anchor

You can also build a custom key store if none of the built-in providers fit your needs.

See Also ​