Skip to content

Field Encryption

Tayra's field encryption engine scans your entity types for [PersonalData], [DeepPersonalData], and [SerializedPersonalData] annotations, then encrypts and decrypts those fields in-place using AES-256-GCM with per-subject keys.

ITayra — The Public API

All encryption operations go through ITayra:

csharp
// DI
var tayra = provider.GetRequiredService<ITayra>();

// Non-DI
using var tayra = TayraHost.Create(opts => opts.LicenseKey = key);

await tayra.EncryptAsync(customer);   // Encrypts all PII fields in-place
await tayra.DecryptAsync(customer);   // Decrypts back to plaintext
MethodPurpose
EncryptAsync<T>Encrypts all [PersonalData] fields on the object in-place. Keys are auto-created if needed. Blind indexes are computed automatically.
DecryptAsync<T>Decrypts all [PersonalData] fields in-place. If the key is missing (shredded), replacement values are used.
HasPersonalData<T>Returns true if the type has any PII annotations. Useful for conditional encryption.

Metadata-Driven Reflection

Internal implementation — provided for clarity

The field encryption engine does not hard-code field names or types. Instead, it relies on a metadata cache to discover annotated properties:

  1. Type scanning — On first use, the metadata cache scans the type's public instance properties using reflection.
  2. Field classification — Each annotated property is classified as Text, Deep, Serialized, TextCollection, or DeepCollection.
  3. Subject resolution[DataSubjectId] properties are recorded with their group and prefix.
  4. Caching — The metadata is cached in a ConcurrentDictionary and reused for all subsequent calls.

After the first call, there is zero reflection overhead.

EncryptAsync Flow

Internal implementation — provided for clarity

When EncryptAsync<T> is called:

EncryptAsync(customer)

    ├─ Get metadata for typeof(Customer)
    │   └─ Fields: Name (Text), Email (Text)
    │   └─ Subjects: Id (default group)

    ├─ Resolve keys for encryption
    │   └─ Get or create AES key for the data subject
    │       └─ Returns 32-byte AES key (created or existing)

    ├─ For each field:
    │   ├─ Text: AesGcm.Encrypt → Base64 → set property
    │   ├─ Deep: Recurse into nested object
    │   ├─ Serialized: Serialize → AesGcm.Encrypt → set companion byte[]
    │   ├─ TextCollection: Encrypt each element
    │   └─ DeepCollection: Recurse into each element

    ├─ Emit telemetry (activity, metrics, audit event)
    └─ Return

Key points:

  • Keys are created automatically on first use.
  • The operation is in-place — properties are modified directly on the object.
  • If a [DataSubjectId] value is null, fields in that group are skipped.
  • If a masking strategy is set (via the Masking property), the masked value is embedded in the ciphertext string.

DecryptAsync Flow

Internal implementation — provided for clarity

When DecryptAsync<T> is called:

DecryptAsync(customer)

    ├─ Get metadata for typeof(Customer)

    ├─ Resolve keys for decryption
    │   └─ Retrieve key for the data subject
    │       ├─ Key exists → byte[] returned
    │       └─ Key shredded → null returned

    ├─ For each field:
    │   ├─ Key exists:
    │   │   └─ Base64 decode → AesGcm.Decrypt → set property
    │   │
    │   └─ Key is null (shredded):
    │       ├─ Check for embedded redacted value (TAYRA_M: prefix)
    │       │   └─ Found → use redacted value
    │       └─ Not found → use MaskValue from attribute (or default)

    ├─ Emit telemetry
    └─ Return

Key points:

  • Never creates keys during decryption.
  • A missing key triggers the replacement value logic.
  • Decryption errors (wrong key, corrupted data) are caught and logged, leaving the property unchanged.

Masking Embedding

When a [PersonalData] field has a masking strategy configured (via the Masking property):

During encryption:

  1. The plaintext value is redacted according to the mode (e.g., "Jane Doe" becomes "Ja******" with MaskAfter(2)).
  2. The redacted value is prepended to the ciphertext: TAYRA_M:Ja******\n{base64_ciphertext}.
  3. The combined string is stored in the property.

During decryption with key:

  1. The TAYRA_M: prefix is detected and stripped.
  2. The remaining Base64 ciphertext is decrypted normally.
  3. The original plaintext is restored.

During decryption without key (shredded):

  1. The TAYRA_M: prefix is detected.
  2. The redacted value (Ja******) is extracted and returned.
  3. The MaskValue from the attribute is ignored.

Thread Safety

ITayra is registered as a singleton and is thread-safe. Concurrent calls on different objects are safe. Concurrent calls on the same object instance are not safe — use external synchronization if needed.

See Also