Data Masking
This page describes technical features that can support your compliance work. It is not legal advice, and it does not establish that any system is compliant with any law, regulation or standard. You remain responsible for your own compliance assessment, independent testing and validation, and review by qualified legal and compliance professionals. See the Compliance Disclaimer.
Data masking protects sensitive information (PII/PHI) by replacing identifiable patterns with masked values in logs, error messages, and API responses.
Before You Start
- .NET 10.0
- Install the required packages:
dotnet add package Excalibur.Compliance
- Familiarity with encryption providers and GDPR erasure
Overview
Quick Start
Configuration
services.AddDataMasking();
// Or with specific compliance mode
services.AddHipaaDataMasking(); // PHI patterns
services.AddPciDssDataMasking(); // Credit card only
services.AddStrictDataMasking(); // All patterns
Basic Usage
public class LoggingService
{
private readonly IDataMasker _masker;
public LoggingService(IDataMasker masker)
{
_masker = masker;
}
public void LogMessage(string message)
{
// Mask all sensitive patterns before logging
var safeMessage = _masker.MaskAll(message);
_logger.LogInformation(safeMessage);
}
}
Masking Patterns
Standard Patterns
| Pattern | Example | Masked | Rule Property |
|---|---|---|---|
| [email protected] | j***@e***.com | MaskEmail | |
| Phone | 555-123-4567 | ***-***-4567 | MaskPhone |
| SSN | 123-45-6789 | ***-**-6789 | MaskSsn |
| Credit Card | 4111-1111-1111-1111 | ****-****-****-1111 | MaskCardNumber |
| IP Address | 192.168.1.100 | ***.***.***.100 | MaskIpAddress |
| Date of Birth | 12/25/1990 | **/**/**** | MaskDateOfBirth |
Pattern Details
Credit card masking preserves the last 4 digits for verification while hiding the PAN:
var input = "Card: 4111-1111-1111-1111";
var masked = _masker.Mask(input, MaskingRules.PciDss);
// Result: "Card: ****-****-****-1111"
SSN masking preserves the last 4 digits per IRS guidelines:
var input = "SSN: 123-45-6789";
var masked = _masker.Mask(input, MaskingRules.Default);
// Result: "SSN: ***-**-6789"
Masking Rules
Default Rules
var rules = MaskingRules.Default;
// Enables: Email, Phone, SSN, Credit Card
// Disables: IP Address, Date of Birth
Compliance-Specific Rules
// PCI-DSS: Credit card numbers only
var pciRules = MaskingRules.PciDss;
// HIPAA: All PHI patterns
var hipaaRules = MaskingRules.Hipaa;
// Enables: Email, Phone, SSN, IP Address, Date of Birth
// Strict: All patterns enabled
var strictRules = MaskingRules.Strict;
Custom Rules
var customRules = new MaskingRules
{
MaskEmail = true,
MaskPhone = true,
MaskSsn = true,
MaskCardNumber = false, // Disable card masking
MaskIpAddress = true,
MaskDateOfBirth = true,
MaskCharacter = '#' // Use # instead of *
};
services.AddDataMasking(customRules);
Object Masking
Automatic Property Masking
Mark properties with attributes for automatic masking. Note that the attributes select properties by
name; MaskObject additionally applies the standard pattern set to every string value in the object, so
an unannotated property containing a recognised pattern is masked as well.
MaskObject is not available under trimming or Native AOTMaskObject reflects over the type and round-trips through JSON, so it is annotated
[RequiresUnreferencedCode] and [RequiresDynamicCode]. In a trimmed or AOT-published application, mask
strings with Mask or MaskAll instead.
public class CustomerDto
{
public string Name { get; set; }
[PersonalData(MaskInLogs = true)]
public string Email { get; set; }
[PersonalData(MaskInLogs = true)]
public string PhoneNumber { get; set; }
[Sensitive(MaskInLogs = true)]
public string SocialSecurityNumber { get; set; }
}
Mask Objects
var customer = new CustomerDto
{
Name = "John Doe",
PhoneNumber = "555-123-4567",
SocialSecurityNumber = "123-45-6789"
};
var masked = _masker.MaskObject(customer);
// Email: j***@e***.com
// PhoneNumber: ***-***-4567
// SocialSecurityNumber: ***-**-6789
Integration Patterns
Logging Middleware
public class MaskingLoggingMiddleware : IDispatchMiddleware
{
// Stage has no default on the interface — a middleware that omits it does not compile.
public DispatchMiddlewareStage? Stage => DispatchMiddlewareStage.PreProcessing;
private readonly IDataMasker _masker;
public async ValueTask<IMessageResult> InvokeAsync(
IDispatchMessage message,
IMessageContext context,
DispatchRequestDelegate next,
CancellationToken ct)
{
// Mask message before logging
var safeMessage = _masker.MaskAll(message.ToString() ?? string.Empty);
_logger.LogInformation("Processing: {@Message}", safeMessage);
return await next(message, context, ct);
}
}
Exception Handling
public class SafeExceptionHandler
{
private readonly IDataMasker _masker;
public string GetSafeErrorMessage(Exception ex)
{
// Mask any PII in exception messages
return _masker.MaskAll(ex.Message);
}
}
API Responses
public class MaskingActionFilter : IAsyncActionFilter
{
private readonly IDataMasker _masker;
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
var result = await next();
if (result.Result is ObjectResult objectResult)
{
if (objectResult.Value is CustomerDto dto)
{
// MaskObject round-trips through JSON, so it needs a CONCRETE class.
// Passing a declared `object` returns a JsonElement and loses the typed payload;
// passing an interface or abstract type throws NotSupportedException.
objectResult.Value = _masker.MaskObject(dto);
}
}
}
}
Performance
The masker uses compiled regex patterns with timeout guards for optimal performance:
| Metric | Value |
|---|---|
| Overhead | Design target: under 2% per operation. Not a measured figure — no masking benchmark ships; measure against your own payloads. |
| Regex engine | NonBacktracking — linear time, ReDoS-resistant by construction |
| Regex timeout | 2 seconds (a backstop, not the primary defence) |
| Pattern caching | The six standard patterns are static and compiled once |
| Thread safety | Fully thread-safe |
// The standard patterns are compiled once, in a static initializer.
// Safe for concurrent access from multiple threads.
var masker = new RegexDataMasker(MaskingRules.Default);
MaskObject additionally builds one property-name pattern per masked property on every call, and
those are not compiled. Prefer Mask or MaskAll on hot paths.
Security Considerations
ReDoS Protection
The standard patterns run on .NET's non-backtracking regex engine, which evaluates in time linear in the input and so has no catastrophic-backtracking case to trigger. A timeout is set as well, as a backstop rather than as the primary defence:
// Internal implementation uses:
new Regex(
pattern,
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.NonBacktracking,
TimeSpan.FromSeconds(2));
Partial Visibility
Some patterns preserve partial data for verification:
| Pattern | Preserved | Purpose |
|---|---|---|
| Credit Card | Last 4 digits | Receipt verification |
| SSN | Last 4 digits | IRS compliance |
| Phone | Last 4 digits | Caller verification |
| IP | Last octet | Network troubleshooting |
Attribute-Based Discovery
Use attributes to mark sensitive properties at the source:
// PersonalData - GDPR Article 4(1) personal data
[PersonalData(Category = PersonalDataCategory.Identity)]
public string Email { get; set; }
// Sensitive - General sensitive data
[Sensitive(Classification = DataClassification.Confidential)]
public string ApiKey { get; set; }
Testing
Verify Masking
[Fact]
public void Should_Mask_CreditCard_Preserving_Last4()
{
var masker = new RegexDataMasker(MaskingRules.PciDss);
var input = "Card: 4111-1111-1111-1111";
var result = masker.Mask(input, MaskingRules.PciDss);
result.ShouldBe("Card: ****-****-****-1111");
}
[Fact]
public void Should_Mask_Multiple_Patterns()
{
var masker = new RegexDataMasker(MaskingRules.Strict);
var result = masker.MaskAll(input);
result.ShouldContain("j***@e***.com");
result.ShouldContain("***-***-4567");
}
Best Practices
| Practice | Recommendation |
|---|---|
| Default masking | Enable in all environments |
| Compliance mode | Match your regulatory requirements |
| Log review | Audit logs periodically for unmasked data |
| Attribute usage | Mark all PII/PHI at definition time |
| Custom patterns | Add organization-specific patterns as needed |
Next Steps
- GDPR Erasure - Cryptographic data deletion
- Audit Logging - Compliance audit trails
See Also
- GDPR Erasure - Cryptographic data deletion for right to be forgotten
- Compliance Overview - Compliance framework capabilities
- Encryption Architecture - Encryption at rest and in transit