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 type | Key store entry | Example |
|---|---|---|
| Encryption key | {prefix}{subjectId} | cust-42 |
| Blind index HMAC key | bi:{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 tableThis 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:
[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:
// 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));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:
- Generate a new HMAC key in the key store.
- Load all affected rows in batches.
- For each row: decrypt the field, apply transforms, compute new HMAC, write companion column.
- Commit the new key as active.
// 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);
}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 Store | Supported | Notes |
|---|---|---|
InMemory | Yes | Testing only — lost on restart |
SQLite | Yes | Durable, zero-config file-based |
PostgreSQL | Yes | Durable, supports prefix listing |
HashiCorp Vault | Yes | KV v2 secrets engine |
Azure Key Vault | Yes | Stored as Key Vault secrets |
AWS Parameter Store | Yes | Stored 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
- Blind Indexes Overview — How HMAC blind indexes work
- Security Considerations — Threat model and key protection guidance
- Key Stores Overview — Choosing and configuring a key store
- Key Rotation — Rotating encryption keys
