Blind Indexes
Tayra encrypts [PersonalData] fields with AES-256-GCM, which produces a different ciphertext every time the same plaintext is encrypted. This is the correct security behaviour — but it means you cannot query the database for records by an encrypted value. A WHERE email = @email clause on a ciphertext column will never match.
Blind indexes solve this problem by generating a deterministic, one-way fingerprint — an HMAC — of the plaintext value and storing it in a separate companion column. Queries target the HMAC column instead of the encrypted column.
How It Works
Write path
──────────
plaintext ──→ transforms ──→ HMAC-SHA256(key, normalized) ──→ companion column
plaintext ──→ AES-256-GCM encrypt ──→ encrypted column
Query path
──────────
search term ──→ same transforms ──→ HMAC-SHA256(key, normalized) ──→ WHERE companion_col = ?
──→ load row ──→ decrypt ──→ plaintextThe HMAC key is separate from the encryption key. Destroying the encryption key shreds the plaintext data; the HMAC column retains an uninvertible fingerprint.
Quick Start
1. Annotate the model
Apply [BlindIndex] alongside [PersonalData] and add a companion property to hold the HMAC:
public class BlindIndexedCustomer
{
[DataSubjectId]
public Guid Id { get; set; }
[PersonalData]
[BlindIndex(
IndexPropertyName = nameof(EmailHash),
Transforms = ["lowercase", "trim"])]
public string Email { get; set; } = "";
/// <summary>
/// Companion property — stores the HMAC hash of Email for queries.
/// </summary>
public string? EmailHash { get; set; }
}Fluent Configuration
If you prefer to configure blind indexes without attributes, use the fluent API:
var biFluentServices = new ServiceCollection();
biFluentServices.AddTayra(opts =>
{
opts.LicenseKey = licenseKey;
opts.Entity<FluentCustomer>(e =>
{
e.DataSubjectId(c => c.CustomerId);
e.PersonalData(c => c.Email);
e.BlindIndex(c => c.Email)
.WithLowercase().WithTrim()
.StoredIn(c => c.EmailIndex);
});
});Both attribute and fluent configurations produce identical behavior. See Fluent API for more details.
2. Save a record
When EncryptAsync is called, Tayra automatically computes and writes the HMAC into the companion property before persisting:
// Create a customer and encrypt — Tayra computes the HMAC automatically
var indexed = new BlindIndexedCustomer
{
Id = Guid.NewGuid(),
Email = "jane@example.com",
};
await biTayra.EncryptAsync(indexed);
// indexed.Email is now AES-256-GCM ciphertext
// indexed.EmailHash is the HMAC-SHA256 hash of "jane@example.com" (lowercased, trimmed)
Console.WriteLine($" EmailHash: {indexed.EmailHash}");3. Query by the blind index
Apply the same transforms to the search term, compute the HMAC, and query the companion column:
// To query, compute the same HMAC for the search term
string searchHash = await biTayra.ComputeBlindIndexAsync(
"jane@example.com", "EmailHash", typeof(BlindIndexedCustomer));
// Use the hash in a WHERE clause:
// db.Query<Customer>().Where(c => c.EmailHash == searchHash)
Console.WriteLine($" Search hash matches: {searchHash == indexed.EmailHash}");Security Model
- HMAC-SHA256 is one-way. You cannot reverse a blind index back to plaintext.
- HMAC keys are separate from encryption keys. The key used to compute
EmailHashis not the same key that encryptsEmail. They are stored under different prefixes in the key store. - Transforms normalize input before hashing. Applying
lowercaseandtrimmeans"jane@example.com","Jane@example.com", and" jane@example.com "all produce the same HMAC. This prevents trivial enumeration bypasses. - Frequency analysis is possible. If a field has low cardinality (e.g., a status field with three possible values), an attacker who can read the database can learn the distribution. See Security Considerations for mitigation strategies.
Blind indexes reveal that two rows share the same value
If two customers have the same email address, their EmailHash values will be identical. This is intentional — it is what makes querying possible — but it is a security trade-off. Do not use blind indexes on highly sensitive fields with very low cardinality.
NuGet Package
Blind index support is included in Tayra.Core. No additional package is required.
In This Section
- Querying — EF Core and Marten query examples
- Transforms — Built-in normalisation transforms and custom transforms
- Security — Threat model, cardinality risks, and guidance
- Key Management — HMAC key storage, scoping, and rotation
See Also
[PersonalData]— Encryption attribute- EF Core Integration — Transparent encryption with EF Core
- Marten Integration — Document store encryption
