DEV Community

Cover image for EF Core 9 mask data
Karen Payne
Karen Payne

Posted on

EF Core 9 mask data

Introduction

Learn how to use EF Core interceptors and value converters to mask data read from a SQL Server database table.

Note that there are more secure methods than what is presented here, but for the more secure methods, not all developers will have the time and/or the knowledge to implement them. If a developer wants to make the effort, see dynamic data masking.

shows masked SSN and credit cards

What are Value converters

Value converters allow property values to be converted when reading from or writing to the database. This conversion can be from one value to another of the same type (for example, encrypting strings) or from a value of one type to a value of another type (for example, converting enum values to and from strings in the database.)

Uses

  • EnumToStringConverter: Enum to and from int
  • ConverterMappingHints: A type to and from a type

Value converter samples

What are interceptors

Entity Framework Core (EF Core) interceptors enable interception, modification, and/or suppression of EF Core operations.

Typical uses include intervention to SaveChanges, auditing, diagnostics, to name a few.

Masking Social Security and credit card numbers

Example 1

Use get in each property to mask data.

public partial class Person
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public DateOnly BirthDate { get; set; }

    public string SocialSecurity { get; set; }

    public string CreditCard { get; set; }

    [NotMapped]
    public string MaskedCreditCard
    {
        get
        {
            if (string.IsNullOrWhiteSpace(CreditCard))
                return "XXXX-XXXX-XXXX-XXXX";

            // Optional: Strip spaces, dashes, etc.
            var digitsOnly = new string(CreditCard.Where(char.IsDigit).ToArray());

            if (digitsOnly.Length < 4)
                return "XXXX-XXXX-XXXX-XXXX";

            return $"XXXX-XXXX-XXXX-{digitsOnly[^4..]}";
        }
    }
    [NotMapped]
    public string MaskedSocialSecurity
    {
        get
        {
            if (string.IsNullOrEmpty(SocialSecurity) || SocialSecurity.Length < 4)
                return "XXX-XX-XXXX";

            return $"XXX-XX-{SocialSecurity[^4..]}";
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The problem is the developer must remember to use the mask properties.

Example 2 value converters

Use a value converter.

In the constructor, for saving data to the database, no masking while for reads the method MaskCreditCard mask, in this case a credit card number. Once understand a developer can write one for SSN and other string properties.

public class CreditCardMaskConverter : ValueConverter<string, string>
{
    public CreditCardMaskConverter()
        : base(
            v => v, // Write to database as-is
            v => MaskCreditCard(v)) // Read from database masked
    {
    }

    private static string MaskCreditCard(string creditCard)
    {
        if (string.IsNullOrWhiteSpace(creditCard))
            return "XXXX-XXXX-XXXX-XXXX";

        var digitsOnly = new string(creditCard.Where(char.IsDigit).ToArray());

        if (digitsOnly.Length < 4)
            return "XXXX-XXXX-XXXX-XXXX";

        return $"XXXX-XXXX-XXXX-{digitsOnly[^4..]}";
    }
}
Enter fullscreen mode Exit fullscreen mode

Register the value converter in the DbContext.

See the line, HasConversion.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>(entity =>
    {

        entity.Property(e => e.FirstName).IsRequired();
        entity.Property(e => e.LastName).IsRequired();
        entity.Property(e => e.SocialSecurity).IsRequired();
        entity.Property(e => e.CreditCard).IsRequired();
        entity.Property(e => e.CreditCard).HasConversion(new CreditCardMaskConverter());

    });

    OnModelCreatingPartial(modelBuilder);

}
Enter fullscreen mode Exit fullscreen mode

Example 3 Interceptors

Each class for masking

  • Implements IMaterializationInterceptor
  • Writes masking logic in the method InitializedInstance
public class CreditCardMaskingInterceptor : IMaterializationInterceptor
{
    public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
    {
        if (entity is not Person person)
            return entity;

        if (!string.IsNullOrEmpty(person.CreditCard) && person.CreditCard.Length >= 4)
        {
            // Mask all but the last 4 digits
            person.CreditCard = $"XXXX-XXXX-XXXX-{person.CreditCard[^4..]}";
        }
        else
        {
            person.CreditCard = "XXXX-XXXX-XXXX-XXXX";
        }

        return entity;
    }
}


public class SocialSecurityMaskingInterceptor : IMaterializationInterceptor
{

    public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
    {
        if (entity is not Person person) return entity;
        if (!string.IsNullOrEmpty(person.SocialSecurity) && person.SocialSecurity.Length >= 4)
        {
            person.SocialSecurity = $"XXX-XX-{person.SocialSecurity[^4..]}";
        }
        else
        {
            person.SocialSecurity = "XXX-XX-XXXX";
        }

        return entity;
    }
}
Enter fullscreen mode Exit fullscreen mode

Registering interceptors

In a DbContext.

registering above interceptors in the sample DbContext

Next level

Encrypt Before Saving / Decrypt After Loading, for ASP.NET Core consider IDataProtector

Sample class


public class EncryptionService
{
    private readonly IDataProtector _protector;

    public EncryptionService(IDataProtectionProvider provider)
    {
        _protector = provider.CreateProtector("CreditCardProtection");
    }

    public string Encrypt(string input)
    {
        if (string.IsNullOrWhiteSpace(input))
            return string.Empty;

        return _protector.Protect(input);
    }

    public string Decrypt(string input)
    {
        if (string.IsNullOrWhiteSpace(input))
            return string.Empty;

        return _protector.Unprotect(input);
    }
}
Enter fullscreen mode Exit fullscreen mode

Person class modifications

public partial class Person
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public DateOnly BirthDate { get; set; }

    // Encrypted in DB
    public string CreditCardEncrypted { get; set; }

    [NotMapped]
    public string CreditCardPlain { get; set; }

    [NotMapped]
    public string MaskedCreditCard
    {
        get
        {
            if (string.IsNullOrWhiteSpace(CreditCardPlain))
                return "XXXX-XXXX-XXXX-XXXX";

            var digitsOnly = new string(CreditCardPlain.Where(char.IsDigit).ToArray());

            if (digitsOnly.Length < 4)
                return "XXXX-XXXX-XXXX-XXXX";

            return $"XXXX-XXXX-XXXX-{digitsOnly[^4..]}";
        }
    }

    public void EncryptCreditCard(EncryptionService encryptionService)
    {
        if (!string.IsNullOrEmpty(CreditCardPlain))
        {
            CreditCardEncrypted = encryptionService.Encrypt(CreditCardPlain);
        }
    }

    public void DecryptCreditCard(EncryptionService encryptionService)
    {
        if (!string.IsNullOrEmpty(CreditCardEncrypted))
        {
            CreditCardPlain = encryptionService.Decrypt(CreditCardEncrypted);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Decrypt person

var person = await _context.Persons.FindAsync(id);
person.DecryptCreditCard(_encryptionService);
Enter fullscreen mode Exit fullscreen mode

Encrypt person

person.EncryptCreditCard(_encryptionService);
_context.Update(person);
await _context.SaveChangesAsync();
Enter fullscreen mode Exit fullscreen mode

Source code

Value converters and interceptors are included along with class mask properties. Make a decision which works best for you or go with permission base making.

source code ASP.NET Core source code

Summary

From here, decide which masking technique fits your security needs. The bottom line is never to compromise customer data.

Top comments (0)