Skip to content

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 ──→ plaintext

The 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:

cs
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; }
}
anchor

Fluent Configuration

If you prefer to configure blind indexes without attributes, use the fluent API:

cs
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);
    });
});
anchor

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:

cs
// 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}");
anchor

3. Query by the blind index

Apply the same transforms to the search term, compute the HMAC, and query the companion column:

cs
// 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}");
anchor

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 EmailHash is not the same key that encrypts Email. They are stored under different prefixes in the key store.
  • Transforms normalize input before hashing. Applying lowercase and trim means "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