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.
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
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..]}";
}
}
}
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..]}";
}
}
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);
}
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;
}
}
Registering interceptors
In a 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);
}
}
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);
}
}
}
Decrypt person
var person = await _context.Persons.FindAsync(id);
person.DecryptCreditCard(_encryptionService);
Encrypt person
person.EncryptCreditCard(_encryptionService);
_context.Update(person);
await _context.SaveChangesAsync();
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)