Skip to content

Blind Index Key Management

Blind indexes use HMAC-SHA256 to produce deterministic fingerprints. The HMAC operation requires a secret key. This page covers how that key is stored, scoped, and managed over time.

Key Storage

Blind index HMAC keys are stored in the same IKeyStore as encryption keys. They are distinguished by a bi: prefix in the key name.

Key typeKey store entryExample
Encryption key{prefix}{subjectId}cust-42
Blind index HMAC keybi:{fieldHash}bi:customer_email

The field hash component is derived from the entity type name, property name, and configured transforms. It is stable across application restarts.

Blind index keys are not per-subject

Encryption keys are scoped to a data subject. Blind index HMAC keys are scoped to a field definition — one key per [BlindIndex]-annotated property. This is what makes querying possible: all rows with the same email address must produce the same HMAC.

Key Scopes

Field-Level Scope (Default)

By default, one HMAC key is shared across all subjects for a given field. Every CustomerEntity.Email blind index is computed with the same key:

bi:customer_email  →  used for ALL rows in the Customers table

This is the correct behaviour for searchable fields. You want "jane@example.com" to produce the same HMAC regardless of which customer record it belongs to.

Custom Key Name

You can override the auto-derived key name with an explicit value:

csharp
[BlindIndex(
    IndexPropertyName = nameof(EmailHash),
    Transforms = ["lowercase", "trim"],
    Scope = "global_email_index")]
public string Email { get; set; } = "";

This is useful when multiple entity types share the same conceptual index (e.g., a shared email lookup across Customer, Supplier, and Employee tables).

Key Generation

HMAC keys are generated automatically the first time ComputeBlindIndexAsync is called for a field that has no existing key in the key store. The generated key is a 32-byte cryptographically random value.

You can pre-generate keys during application startup to avoid a write-on-first-use race condition in high-concurrency environments:

cs
// Pre-warm HMAC keys during application startup.
// ComputeBlindIndexAsync triggers key generation if the HMAC key doesn't exist yet.
// This avoids a write-on-first-use race condition in high-concurrency environments.
await biTayra.ComputeBlindIndexAsync("warmup", "EmailHash", typeof(BlindIndexedCustomer));
anchor

Key Rotation

Rotating an HMAC key invalidates all existing blind index values. After rotation, no existing row can be found by a blind index query until its companion column has been recomputed.

Rotation is a multi-step operation:

  1. Generate a new HMAC key in the key store.
  2. Load all affected rows in batches.
  3. For each row: decrypt the field, apply transforms, compute new HMAC, write companion column.
  4. Commit the new key as active.
cs
// Rotating an HMAC key invalidates all existing blind index values.
// After rotation, recompute companion columns for all affected rows.

// Step 1: Delete the existing HMAC key
await biTayra.ShredByPrefixAsync("bi:");

// Step 2: Recompute blind indexes for all affected rows
// In practice, load rows from your database in batches.
var rows = new[] { indexed }; // Replace with batch query
foreach (var row in rows)
{
    // Decrypt to restore plaintext values
    await biTayra.DecryptAsync(row);

    // Re-encrypt — HMAC recomputed with the new key (auto-generated on first use)
    await biTayra.EncryptAsync(row);
}
anchor

Queries will return no results during rotation

Between step 1 (new key active) and step 2 (all rows recomputed), blind index queries will fail to find rows whose companion columns still contain old HMAC values. Schedule rotation during a maintenance window or implement a dual-read strategy (query with new key, fall back to old key) for zero-downtime rotation.

When to Rotate

HMAC key rotation is less urgent than encryption key rotation because the HMAC key is not directly used to encrypt personal data. Consider rotating when:

  • You have reason to believe the HMAC key has been exposed.
  • You are rotating all secrets as part of a periodic security review.
  • A key store migration requires regenerating all keys.
  • You are changing the transforms on a field (which effectively requires a full recompute anyway).

Key Deletion

Deleting an HMAC key removes the ability to:

  • Query by the blind index (new searches return no results).
  • Recompute companion columns for existing rows (the key is gone).

Unlike encryption key deletion (which is the mechanism for crypto-shredding), HMAC key deletion has no GDPR purpose. Do not delete HMAC keys unless you intend to permanently disable querying by that field.

Key Store Compatibility

All Tayra key store backends support blind index HMAC keys:

Key StoreSupportedNotes
InMemoryYesTesting only — lost on restart
SQLiteYesDurable, zero-config file-based
PostgreSQLYesDurable, supports prefix listing
HashiCorp VaultYesKV v2 secrets engine
Azure Key VaultYesStored as Key Vault secrets
AWS Parameter StoreYesStored as SecureString parameters

HMAC keys are small (32 bytes each) and rarely change. They do not contribute meaningfully to key store storage costs.

See Also