Skip to content

Collection Encryption

Tayra supports encrypting collections of personal data. When a property marked with [PersonalData] is a List<string>, string[], or other supported collection type, Tayra encrypts each element individually. This is useful for entities that store multiple PII values in a single field, such as phone numbers, email aliases, medical allergies, or previous addresses.

Defining Collection PII

Mark collection properties with the [PersonalData] attribute, the same way you mark scalar string properties. Tayra detects the collection type automatically and encrypts each element:

cs
public class PatientRecord
{
    [DataSubjectId(Prefix = "patient-")]
    public Guid PatientId { get; set; }

    [PersonalData]
    public string FullName { get; set; } = "";

    [PersonalData]
    public List<string> Allergies { get; set; } = new();

    [PersonalData]
    public string[] PreviousSurnames { get; set; } = Array.Empty<string>();

    public string Department { get; set; } = "";
}
anchor

In the example above:

  • Allergies is a List<string> - each allergy is encrypted individually
  • PreviousSurnames is a string[] - each surname is encrypted individually
  • FullName is a regular string - encrypted as usual
  • Department has no attribute - left untouched

Per-Element Encryption

Each element in the collection is encrypted independently with the same key (derived from the [DataSubjectId]). This means:

  • Individual elements can be added or removed without re-encrypting the entire collection
  • Each element has its own nonce (initialization vector), so identical plaintext values produce different ciphertext
  • The collection size is preserved - a list with 3 elements before encryption still has 3 elements after
csharp
// Before encryption
patient.Allergies = new List<string> { "Penicillin", "Peanuts", "Latex" };

// After encryption - 3 Base64-encoded ciphertext strings
patient.Allergies = new List<string>
{
    "AQzR4x5k...",  // Encrypted "Penicillin"
    "AQnT7y2m...",  // Encrypted "Peanuts"
    "AQ8pL9dw...",  // Encrypted "Latex"
};

Supported Collection Types

Any property whose type implements IEnumerable<string> is detected as a string collection. At runtime, the collection instance must be mutable:

TypeEncryptionDecryptionNotes
List<string>In-place mutationIn-place mutationElements are replaced with encrypted/decrypted values
string[]In-place mutationIn-place mutationArray elements are replaced directly
IList<string>In-place mutationIn-place mutationWorks via the IList<string> indexer
HashSet<string>Snapshot, clear, re-addSnapshot, clear, re-addNo indexer; elements are snapshotted, the set is cleared, and processed values are re-added
Other mutable ICollection<string>Snapshot, clear, re-addSnapshot, clear, re-addSame approach as HashSet<string>

A property may also be declared as a read-only interface (e.g., IReadOnlyList<string> or IEnumerable<string>) as long as the runtime instance is mutable - writability is checked when the collection is first encrypted or decrypted.

A collection can be declared as a property or as a public instance field. Because elements are mutated in place, the member itself does not need a setter (a readonly field holding a mutable collection is fine); only the runtime collection instance must be mutable.

Crypto-Shredding with Collections

When the encryption key is deleted (crypto-shredding), collection elements follow the same rules as scalar fields:

  • If partial redaction is configured, each element's embedded redacted value is returned
  • Otherwise, each element is replaced with an empty string ("")
csharp
// After crypto-shredding (key deleted)
patient.Allergies = new List<string> { "", "", "" };

Partial Redaction

Collection elements support the same partial redaction modes as scalar fields. Configure redaction on the [PersonalData] attribute:

csharp
public class ContactInfo
{
    [DataSubjectId]
    public Guid SubjectId { get; set; }

    [PersonalData(Masking = MaskingStrategies.MaskAfter, MaskingParameter = 3)]
    public List<string> EmailAddresses { get; set; } = new();
}

After crypto-shredding, each email would retain its first 3 characters:

"jan*****" // was "jane@example.com"
"bob*****" // was "bob@work.org"

Limitations

  • Only string element types are supported for collection encryption. Collections of non-string types (e.g., List<int>, List<DateTime>) must use [SerializedPersonalData] on the property with a companion byte[] field instead.
  • Null elements in a collection are skipped during encryption and decryption. They remain null in the collection.
  • Read-only collection instances (e.g., ImmutableList<string>, a ReadOnlyCollection<string> wrapper) cannot be updated and throw an InvalidOperationException at encrypt/decrypt time. (Earlier releases silently skipped them, leaving plaintext behind.) Use a mutable collection such as List<string>, string[], or HashSet<string>.
  • Deep collections (collections of objects with [DeepPersonalData]) are supported - Tayra will recursively encrypt PII fields on each element. The element objects must have their own [DataSubjectId] or inherit the parent's key context.

Performance

Collection encryption processes elements sequentially. For very large collections (thousands of elements), the encryption time scales linearly. If performance is a concern, consider batching large collections or encrypting them as a single serialized blob with [SerializedPersonalData].

See Also