Logging Sensitive Data Masking in Java

Introduction to Log Masking

Log masking is a critical security practice that prevents sensitive information from being exposed in application logs. This comprehensive guide covers techniques, patterns, and best practices for implementing effective log masking in Java applications.

Core Masking Strategies

1. Pattern-Based Masking

2. Annotation-Driven Masking

3. Custom Masking Providers

4. Log Framework Integrations

Basic Pattern-Based Masking

Simple Regex Masking Utility

import java.util.regex.*;
import java.util.*;
public class BasicLogMasker {
private static final Map<Pattern, String> MASK_PATTERNS = new LinkedHashMap<>();
static {
// Credit card patterns
MASK_PATTERNS.put(Pattern.compile("\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})\\b"), 
"****-****-****-####");
MASK_PATTERNS.put(Pattern.compile("\\b3[47][0-9]{13}\\b"), 
"****-******-####");
// SSN patterns
MASK_PATTERNS.put(Pattern.compile("\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b"), 
"***-**-####");
// Email patterns
MASK_PATTERNS.put(Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"), 
"***@***.***");
// Phone numbers
MASK_PATTERNS.put(Pattern.compile("\\b\\(?[0-9]{3}\\)?[-. ]?[0-9]{3}[-. ]?[0-9]{4}\\b"), 
"(***) ***-####");
// API keys (common patterns)
MASK_PATTERNS.put(Pattern.compile("\\bsk_[a-zA-Z0-9]{20,}\\b"), 
"sk_************");
MASK_PATTERNS.put(Pattern.compile("\\bAKIA[0-9A-Z]{16}\\b"), 
"AKIA************");
}
public static String maskSensitiveData(String input) {
if (input == null || input.trim().isEmpty()) {
return input;
}
String masked = input;
for (Map.Entry<Pattern, String> entry : MASK_PATTERNS.entrySet()) {
Pattern pattern = entry.getKey();
String replacement = entry.getValue();
Matcher matcher = pattern.matcher(masked);
masked = matcher.replaceAll(replacement);
}
return masked;
}
// Specific masking methods for common data types
public static String maskCreditCard(String creditCard) {
if (creditCard == null || creditCard.length() < 12) {
return creditCard;
}
return "****-****-****-" + creditCard.substring(creditCard.length() - 4);
}
public static String maskSSN(String ssn) {
if (ssn == null || ssn.length() < 9) {
return ssn;
}
return "***-**-" + ssn.substring(ssn.length() - 4);
}
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) {
return email;
}
int atIndex = email.indexOf("@");
String localPart = email.substring(0, atIndex);
String domain = email.substring(atIndex + 1);
String maskedLocal = localPart.length() <= 2 ? 
"*".repeat(localPart.length()) : 
localPart.charAt(0) + "*".repeat(localPart.length() - 2) + localPart.charAt(localPart.length() - 1);
int dotIndex = domain.lastIndexOf(".");
String domainName = domain.substring(0, dotIndex);
String tld = domain.substring(dotIndex + 1);
String maskedDomain = domainName.length() <= 2 ?
"*".repeat(domainName.length()) :
domainName.charAt(0) + "*".repeat(domainName.length() - 2) + domainName.charAt(domainName.length() - 1);
return maskedLocal + "@" + maskedDomain + "." + tld;
}
public static void main(String[] args) {
// Test the masker
String testData = "User with SSN 123-45-6789, email [email protected], " +
"and credit card 4111-1111-1111-1111 called phone 555-123-4567.";
System.out.println("Original: " + testData);
System.out.println("Masked:   " + maskSensitiveData(testData));
// Test individual methods
System.out.println("\nIndividual masking:");
System.out.println("Credit Card: " + maskCreditCard("4111111111111111"));
System.out.println("SSN: " + maskSSN("123456789"));
System.out.println("Email: " + maskEmail("[email protected]"));
}
}

Advanced Annotation-Based Masking

Custom Annotations for Sensitive Data

import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
// Marker annotation for sensitive fields
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SensitiveData {
MaskType type() default MaskType.GENERIC;
int visiblePrefix() default 0;
int visibleSuffix() default 0;
}
// Masking strategies
enum MaskType {
CREDIT_CARD,
SSN,
EMAIL,
PHONE,
API_KEY,
PASSWORD,
GENERIC
}
// Sample entity class with sensitive data
class User {
private String username;
@SensitiveData(type = MaskType.EMAIL)
private String email;
@SensitiveData(type = MaskType.SSN, visibleSuffix = 4)
private String socialSecurityNumber;
@SensitiveData(type = MaskType.CREDIT_CARD, visibleSuffix = 4)
private String creditCardNumber;
@SensitiveData(type = MaskType.PASSWORD)
private String password;
@SensitiveData(type = MaskType.PHONE, visiblePrefix = 3, visibleSuffix = 2)
private String phoneNumber;
// Constructors, getters, and setters
public User(String username, String email, String ssn, String creditCard, 
String password, String phone) {
this.username = username;
this.email = email;
this.socialSecurityNumber = ssn;
this.creditCardNumber = creditCard;
this.password = password;
this.phoneNumber = phone;
}
// Getters and setters
public String getUsername() { return username; }
public String getEmail() { return email; }
public String getSocialSecurityNumber() { return socialSecurityNumber; }
public String getCreditCardNumber() { return creditCardNumber; }
public String getPassword() { return password; }
public String getPhoneNumber() { return phoneNumber; }
}
// Reflection-based masking utility
public class AnnotationBasedMasker {
public static String maskObject(Object obj) {
if (obj == null) {
return "null";
}
StringBuilder result = new StringBuilder();
result.append(obj.getClass().getSimpleName()).append("{");
Field[] fields = obj.getClass().getDeclaredFields();
boolean firstField = true;
for (Field field : fields) {
if (!firstField) {
result.append(", ");
}
firstField = false;
field.setAccessible(true);
result.append(field.getName()).append("=");
try {
Object value = field.get(obj);
if (field.isAnnotationPresent(SensitiveData.class)) {
SensitiveData annotation = field.getAnnotation(SensitiveData.class);
String maskedValue = maskValue(value != null ? value.toString() : null, annotation);
result.append(maskedValue);
} else {
result.append(value);
}
} catch (IllegalAccessException e) {
result.append("[ACCESS_ERROR]");
}
}
result.append("}");
return result.toString();
}
private static String maskValue(String value, SensitiveData annotation) {
if (value == null) {
return "null";
}
switch (annotation.type()) {
case CREDIT_CARD:
return maskCreditCard(value, annotation.visibleSuffix());
case SSN:
return maskSSN(value, annotation.visibleSuffix());
case EMAIL:
return maskEmail(value);
case PHONE:
return maskPhone(value, annotation.visiblePrefix(), annotation.visibleSuffix());
case PASSWORD:
return "********";
case API_KEY:
return maskApiKey(value);
case GENERIC:
default:
return maskGeneric(value, annotation.visiblePrefix(), annotation.visibleSuffix());
}
}
private static String maskCreditCard(String cardNumber, int visibleSuffix) {
if (cardNumber.length() < visibleSuffix) {
return "*".repeat(cardNumber.length());
}
return "*".repeat(cardNumber.length() - visibleSuffix) + 
cardNumber.substring(cardNumber.length() - visibleSuffix);
}
private static String maskSSN(String ssn, int visibleSuffix) {
if (ssn.length() < visibleSuffix) {
return "*".repeat(ssn.length());
}
return "*".repeat(ssn.length() - visibleSuffix) + 
ssn.substring(ssn.length() - visibleSuffix);
}
private static String maskEmail(String email) {
if (!email.contains("@")) {
return "*".repeat(email.length());
}
String[] parts = email.split("@");
String localPart = parts[0];
String domain = parts[1];
String maskedLocal = localPart.length() <= 2 ? 
"*".repeat(localPart.length()) : 
localPart.charAt(0) + "*".repeat(Math.max(0, localPart.length() - 2)) + 
(localPart.length() > 1 ? localPart.charAt(localPart.length() - 1) : "");
return maskedLocal + "@" + maskDomain(domain);
}
private static String maskDomain(String domain) {
if (!domain.contains(".")) {
return "*".repeat(domain.length());
}
int lastDot = domain.lastIndexOf(".");
String domainName = domain.substring(0, lastDot);
String tld = domain.substring(lastDot + 1);
String maskedDomain = domainName.length() <= 2 ?
"*".repeat(domainName.length()) :
domainName.charAt(0) + "*".repeat(Math.max(0, domainName.length() - 2)) + 
(domainName.length() > 1 ? domainName.charAt(domainName.length() - 1) : "");
return maskedDomain + "." + tld;
}
private static String maskPhone(String phone, int visiblePrefix, int visibleSuffix) {
// Remove non-digit characters for masking
String digits = phone.replaceAll("\\D", "");
if (digits.length() < visiblePrefix + visibleSuffix) {
return "*".repeat(phone.length());
}
String prefix = digits.substring(0, visiblePrefix);
String suffix = digits.substring(digits.length() - visibleSuffix);
String middle = "*".repeat(digits.length() - visiblePrefix - visibleSuffix);
return prefix + middle + suffix;
}
private static String maskApiKey(String apiKey) {
if (apiKey.length() <= 8) {
return "*".repeat(apiKey.length());
}
return apiKey.substring(0, 4) + "*".repeat(apiKey.length() - 8) + apiKey.substring(apiKey.length() - 4);
}
private static String maskGeneric(String value, int visiblePrefix, int visibleSuffix) {
if (value.length() < visiblePrefix + visibleSuffix) {
return "*".repeat(value.length());
}
String prefix = value.substring(0, Math.min(visiblePrefix, value.length()));
String suffix = visibleSuffix > 0 ? 
value.substring(value.length() - Math.min(visibleSuffix, value.length())) : "";
String middle = "*".repeat(Math.max(0, value.length() - visiblePrefix - visibleSuffix));
return prefix + middle + suffix;
}
public static void main(String[] args) {
User user = new User(
"johndoe",
"[email protected]",
"123-45-6789",
"4111-1111-1111-1111",
"secret123",
"+1-555-123-4567"
);
System.out.println("Original user: " + user);
System.out.println("Masked user: " + maskObject(user));
}
}

Logback Configuration with Masking

Custom Logback Layout with Masking

<!-- logback.xml -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="com.example.MaskingPatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</layout>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="com.example.MaskingPatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</layout>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class MaskingPatternLayout extends PatternLayout {
private static final Pattern[] MASK_PATTERNS = {
Pattern.compile("\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})\\b"), // Credit cards
Pattern.compile("\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b"), // SSN
Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"), // Email
Pattern.compile("\\bpassword[=:]['\"]?([^,'\"\\s]+)['\"]?", Pattern.CASE_INSENSITIVE),
Pattern.compile("\\bapi[_-]?key[=:]['\"]?([^,'\"\\s]+)['\"]?", Pattern.CASE_INSENSITIVE),
Pattern.compile("\\bsecret[=:]['\"]?([^,'\"\\s]+)['\"]?", Pattern.CASE_INSENSITIVE),
Pattern.compile("\\btoken[=:]['\"]?([^,'\"\\s]+)['\"]?", Pattern.CASE_INSENSITIVE)
};
@Override
public String doLayout(ILoggingEvent event) {
String message = super.doLayout(event);
return maskSensitiveData(message);
}
private String maskSensitiveData(String message) {
if (message == null || message.isEmpty()) {
return message;
}
String masked = message;
for (Pattern pattern : MASK_PATTERNS) {
Matcher matcher = pattern.matcher(masked);
StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
String matched = matcher.group();
String replacement;
if (pattern.pattern().toLowerCase().contains("credit") || 
pattern.pattern().contains("4[0-9]")) {
replacement = maskCreditCard(matched);
} else if (pattern.pattern().contains("[0-9]{3}-[0-9]{2}-[0-9]{4}")) {
replacement = "***-**-" + matched.substring(matched.length() - 4);
} else if (pattern.pattern().contains("@")) {
replacement = maskEmail(matched);
} else {
// For key=value patterns like password=secret
if (matcher.groupCount() > 0 && matcher.group(1) != null) {
String value = matcher.group(1);
replacement = matcher.group().replace(value, "********");
} else {
replacement = "********";
}
}
matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(buffer);
masked = buffer.toString();
}
return masked;
}
private String maskCreditCard(String cardNumber) {
String digits = cardNumber.replaceAll("\\D", "");
if (digits.length() >= 4) {
return "****-****-****-" + digits.substring(digits.length() - 4);
}
return "****";
}
private String maskEmail(String email) {
int atIndex = email.indexOf("@");
if (atIndex > 0) {
String localPart = email.substring(0, atIndex);
String domain = email.substring(atIndex);
if (localPart.length() <= 2) {
return "*".repeat(localPart.length()) + domain;
} else {
return localPart.charAt(0) + 
"*".repeat(localPart.length() - 2) + 
localPart.charAt(localPart.length() - 1) + 
domain;
}
}
return email;
}
}

Log4j2 Custom Plugin for Masking

Log4j2 Masking Plugin

import org.apache.logging.log4j.core.*;
import org.apache.logging.log4j.core.config.plugins.*;
import org.apache.logging.log4j.core.layout.*;
import org.apache.logging.log4j.core.pattern.*;
import java.util.regex.*;
@Plugin(name = "MaskingPatternLayout", category = Node.CATEGORY, 
elementType = Layout.ELEMENT_TYPE, printObject = true)
public class MaskingPatternLayout extends PatternLayout {
private final List<Pattern> maskPatterns;
protected MaskingPatternLayout(String pattern, Configuration config, 
RegexReplacement replace, Charset charset, 
boolean alwaysWriteExceptions, 
boolean noConsoleNoAnsi) {
super(config, replace, pattern, charset, alwaysWriteExceptions, noConsoleNoAnsi);
this.maskPatterns = initializeMaskPatterns();
}
@PluginFactory
public static MaskingPatternLayout createLayout(
@PluginAttribute(value = "pattern", defaultString = PatternLayout.DEFAULT_CONVERSION_PATTERN) String pattern,
@PluginConfiguration Configuration config,
@PluginElement("Replace") RegexReplacement replace,
@PluginAttribute(value = "charset", defaultString = "UTF-8") Charset charset,
@PluginAttribute(value = "alwaysWriteExceptions", defaultBoolean = true) boolean alwaysWriteExceptions,
@PluginAttribute(value = "noConsoleNoAnsi", defaultBoolean = false) boolean noConsoleNoAnsi) {
return new MaskingPatternLayout(pattern, config, replace, charset, 
alwaysWriteExceptions, noConsoleNoAnsi);
}
private List<Pattern> initializeMaskPatterns() {
List<Pattern> patterns = new ArrayList<>();
// Credit cards
patterns.add(Pattern.compile("\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})\\b"));
// SSN
patterns.add(Pattern.compile("\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b"));
// Email
patterns.add(Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"));
// Sensitive key-value pairs
patterns.add(Pattern.compile("(?i)(password|pwd|pass)=['\"]?([^,'\"\\s]+)['\"]?"));
patterns.add(Pattern.compile("(?i)(api[_-]?key|apikey)=['\"]?([^,'\"\\s]+)['\"]?"));
patterns.add(Pattern.compile("(?i)(secret|token)=['\"]?([^,'\"\\s]+)['\"]?"));
patterns.add(Pattern.compile("(?i)(authorization|auth):\\s*(bearer\\s+[^\\s]+)", Pattern.CASE_INSENSITIVE));
return patterns;
}
@Override
public String toSerializable(LogEvent event) {
String original = super.toSerializable(event);
return maskSensitiveData(original);
}
private String maskSensitiveData(String message) {
if (message == null || message.isEmpty()) {
return message;
}
String masked = message;
for (Pattern pattern : maskPatterns) {
Matcher matcher = pattern.matcher(masked);
StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
String replacement = getReplacement(matcher);
matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(buffer);
masked = buffer.toString();
}
return masked;
}
private String getReplacement(Matcher matcher) {
String matched = matcher.group();
// Handle key-value pairs
if (matcher.pattern().pattern().toLowerCase().contains("password") ||
matcher.pattern().pattern().toLowerCase().contains("api") ||
matcher.pattern().pattern().toLowerCase().contains("secret") ||
matcher.pattern().pattern().toLowerCase().contains("token")) {
if (matcher.groupCount() >= 2 && matcher.group(2) != null) {
String key = matcher.group(1);
String value = matcher.group(2);
return key + "=********";
}
}
// Handle authorization headers
if (matcher.pattern().pattern().toLowerCase().contains("authorization")) {
if (matched.toLowerCase().startsWith("bearer")) {
return "Bearer ********";
}
return "********";
}
// Handle specific data types
if (matcher.pattern().pattern().contains("4[0-9]")) {
return maskCreditCard(matched);
} else if (matcher.pattern().pattern().contains("[0-9]{3}-[0-9]{2}-[0-9]{4}")) {
return "***-**-" + (matched.length() >= 4 ? matched.substring(matched.length() - 4) : "****");
} else if (matcher.pattern().pattern().contains("@")) {
return maskEmail(matched);
}
return "********";
}
private String maskCreditCard(String cardNumber) {
String digits = cardNumber.replaceAll("\\D", "");
if (digits.length() >= 4) {
return "****-****-****-" + digits.substring(digits.length() - 4);
}
return "****";
}
private String maskEmail(String email) {
int atIndex = email.indexOf("@");
if (atIndex > 0) {
String localPart = email.substring(0, atIndex);
String domain = email.substring(atIndex);
if (localPart.length() <= 2) {
return "*".repeat(localPart.length()) + domain;
} else {
return localPart.charAt(0) + 
"*".repeat(Math.max(0, localPart.length() - 2)) + 
(localPart.length() > 1 ? localPart.charAt(localPart.length() - 1) : "") + 
domain;
}
}
return email;
}
}
<!-- log4j2.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages="com.example.logging">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<MaskingPatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<File name="File" fileName="logs/app.log">
<MaskingPatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</File>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>

Spring Boot Integration

Spring Boot Configuration with Log Masking

import org.springframework.context.annotation.*;
import org.springframework.boot.context.properties.*;
import java.util.*;
@Configuration
@ConfigurationProperties(prefix = "logging.masking")
public class LogMaskingConfig {
private List<String> patterns = new ArrayList<>();
private boolean enabled = true;
private MaskingStrategy strategy = MaskingStrategy.PARTIAL;
public enum MaskingStrategy {
FULL,      // Replace entire value with ****
PARTIAL,   // Show partial information (first/last chars)
HASH       // Replace with hash of the value
}
// Getters and setters
public List<String> getPatterns() { return patterns; }
public void setPatterns(List<String> patterns) { this.patterns = patterns; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public MaskingStrategy getStrategy() { return strategy; }
public void setStrategy(MaskingStrategy strategy) { this.strategy = strategy; }
@Bean
public LogMaskingService logMaskingService() {
return new LogMaskingService(this);
}
}
@Service
public class LogMaskingService {
private final LogMaskingConfig config;
private final List<Pattern> compiledPatterns;
public LogMaskingService(LogMaskingConfig config) {
this.config = config;
this.compiledPatterns = compilePatterns();
}
private List<Pattern> compilePatterns() {
List<Pattern> patterns = new ArrayList<>();
// Add default patterns
patterns.add(Pattern.compile("\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})\\b"));
patterns.add(Pattern.compile("\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b"));
patterns.add(Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"));
// Add custom patterns from configuration
for (String patternStr : config.getPatterns()) {
try {
patterns.add(Pattern.compile(patternStr));
} catch (Exception e) {
// Log warning about invalid pattern
System.err.println("Invalid regex pattern: " + patternStr);
}
}
return patterns;
}
public String maskMessage(String message) {
if (!config.isEnabled() || message == null) {
return message;
}
String masked = message;
for (Pattern pattern : compiledPatterns) {
Matcher matcher = pattern.matcher(masked);
StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
String replacement = getMaskedReplacement(matcher.group());
matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(buffer);
masked = buffer.toString();
}
return masked;
}
private String getMaskedReplacement(String value) {
switch (config.getStrategy()) {
case FULL:
return "*".repeat(Math.min(value.length(), 8));
case HASH:
return "hash_" + Integer.toHexString(value.hashCode());
case PARTIAL:
default:
return maskPartial(value);
}
}
private String maskPartial(String value) {
if (value.length() <= 2) {
return "*".repeat(value.length());
}
// For emails
if (value.contains("@")) {
return maskEmail(value);
}
// For credit cards, SSN, etc.
if (value.matches(".*\\d.*")) {
if (value.replaceAll("\\D", "").length() >= 4) {
return "*".repeat(value.length() - 4) + 
value.substring(value.length() - 4);
}
}
// Generic masking
int visibleChars = Math.max(1, value.length() / 4);
return value.substring(0, visibleChars) + 
"*".repeat(value.length() - visibleChars * 2) + 
value.substring(value.length() - visibleChars);
}
private String maskEmail(String email) {
int atIndex = email.indexOf("@");
if (atIndex > 0) {
String localPart = email.substring(0, atIndex);
String domain = email.substring(atIndex);
String maskedLocal = localPart.length() <= 2 ? 
"*".repeat(localPart.length()) : 
localPart.charAt(0) + "*".repeat(localPart.length() - 2) + 
localPart.charAt(localPart.length() - 1);
return maskedLocal + domain;
}
return email;
}
}
// Aspect for automatic method argument masking
@Aspect
@Component
public class LoggingAspect {
private final LogMaskingService maskingService;
private final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
public LoggingAspect(LogMaskingService maskingService) {
this.maskingService = maskingService;
}
@Around("execution(* com.example.service.*.*(..))")
public Object logMethodCall(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
// Mask arguments before logging
Object[] maskedArgs = maskArguments(joinPoint.getArgs());
String maskedArguments = Arrays.toString(maskedArgs);
logger.info("Entering {}.{} with arguments: {}", className, methodName, maskedArguments);
try {
Object result = joinPoint.proceed();
// Mask result before logging if needed
String maskedResult = maskResult(result);
logger.info("Exiting {}.{} with result: {}", className, methodName, maskedResult);
return result;
} catch (Exception e) {
logger.error("Exception in {}.{}: {}", className, methodName, e.getMessage());
throw e;
}
}
private Object[] maskArguments(Object[] args) {
if (args == null) {
return new Object[0];
}
Object[] masked = new Object[args.length];
for (int i = 0; i < args.length; i++) {
masked[i] = maskObject(args[i]);
}
return masked;
}
private Object maskObject(Object obj) {
if (obj == null) {
return null;
}
// If it's a string, apply masking
if (obj instanceof String) {
return maskingService.maskMessage((String) obj);
}
// If it's a custom object with sensitive data, use reflection
if (isSensitiveObject(obj)) {
return maskSensitiveObject(obj);
}
return obj;
}
private String maskResult(Object result) {
if (result == null) {
return "null";
}
String resultStr = result.toString();
return maskingService.maskMessage(resultStr);
}
private boolean isSensitiveObject(Object obj) {
// Check if object has fields with @SensitiveData annotation
return Arrays.stream(obj.getClass().getDeclaredFields())
.anyMatch(field -> field.isAnnotationPresent(SensitiveData.class));
}
private Object maskSensitiveObject(Object obj) {
// Use reflection to create a masked version of the object
// This would use the AnnotationBasedMasker from earlier
return AnnotationBasedMasker.maskObject(obj);
}
}

Application Properties Configuration

# application.yml
logging:
masking:
enabled: true
strategy: PARTIAL
patterns:
- "\\b[0-9]{16}\\b"  # Additional 16-digit numbers
- "\\b[A-Z]{2}[0-9]{6,}\\b"  # License plates, etc.
level:
com.example: INFO
org.springframework: WARN
# Logback configuration (if using logback)
logging:
config: classpath:logback-masking.xml

Testing the Masking Implementation

Unit Tests for Masking

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
public class LogMaskingTest {
private BasicLogMasker masker;
private AnnotationBasedMasker annotationMasker;
@BeforeEach
void setUp() {
masker = new BasicLogMasker();
annotationMasker = new AnnotationBasedMasker();
}
@Test
void testCreditCardMasking() {
String input = "Card: 4111-1111-1111-1111";
String masked = masker.maskSensitiveData(input);
assertFalse(masked.contains("4111-1111-1111-1111"));
assertTrue(masked.contains("****-****-****-1111"));
}
@Test
void testSSNMasking() {
String input = "SSN: 123-45-6789";
String masked = masker.maskSensitiveData(input);
assertFalse(masked.contains("123-45-6789"));
assertTrue(masked.contains("***-**-6789"));
}
@Test
void testEmailMasking() {
String input = "Email: [email protected]";
String masked = masker.maskSensitiveData(input);
assertFalse(masked.contains("[email protected]"));
assertTrue(masked.contains("j****[email protected]"));
}
@Test
void testAnnotationBasedMasking() {
User user = new User(
"johndoe",
"[email protected]",
"123-45-6789",
"4111-1111-1111-1111",
"secret123",
"+1-555-123-4567"
);
String masked = annotationMasker.maskObject(user);
assertFalse(masked.contains("[email protected]"));
assertFalse(masked.contains("123-45-6789"));
assertFalse(masked.contains("4111-1111-1111-1111"));
assertFalse(masked.contains("secret123"));
assertTrue(masked.contains("j****e@e****e.com"));
assertTrue(masked.contains("***-**-6789"));
assertTrue(masked.contains("****-****-****-1111"));
assertTrue(masked.contains("********")); // password
}
@Test
void testLogMaskingService() {
LogMaskingConfig config = new LogMaskingConfig();
config.setEnabled(true);
config.setStrategy(LogMaskingConfig.MaskingStrategy.PARTIAL);
LogMaskingService service = new LogMaskingService(config);
String message = "User login: [email protected], card=4111111111111111";
String masked = service.maskMessage(message);
assertFalse(masked.contains("[email protected]"));
assertFalse(masked.contains("4111111111111111"));
}
@Test
void testPerformance() {
String largeMessage = "Large log message with multiple sensitive data: " +
"[email protected], ssn=123-45-6789, " +
"card=4111-1111-1111-1111, phone=555-123-4567. " +
"Repeated multiple times for performance testing.";
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
builder.append(largeMessage);
}
String largeInput = builder.toString();
long startTime = System.currentTimeMillis();
String masked = masker.maskSensitiveData(largeInput);
long endTime = System.currentTimeMillis();
System.out.println("Masking performance: " + (endTime - startTime) + "ms");
// Ensure no sensitive data leaked
assertFalse(masked.contains("[email protected]"));
assertFalse(masked.contains("123-45-6789"));
assertFalse(masked.contains("4111-1111-1111-1111"));
}
}

Best Practices and Recommendations

1. Comprehensive Pattern Coverage

public class ComprehensiveMaskingPatterns {
public static final List<Pattern> SENSITIVE_DATA_PATTERNS = Arrays.asList(
// Financial information
Pattern.compile("\\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\\d{3})\\d{11})\\b"),
// Social security numbers
Pattern.compile("\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b"),
// Email addresses
Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"),
// Phone numbers (international formats)
Pattern.compile("\\b(?:\\+?1[-. ]?)?\\(?[0-9]{3}\\)?[-. ]?[0-9]{3}[-. ]?[0-9]{4}\\b"),
// API keys and tokens
Pattern.compile("\\b(?:sk|pk)_[a-zA-Z0-9]{20,}\\b"), // Stripe-like
Pattern.compile("\\bAKIA[0-9A-Z]{16}\\b"), // AWS
Pattern.compile("\\bgh[pousr]_[A-Za-z0-9]{36}\\b"), // GitHub
Pattern.compile("\\b[a-zA-Z0-9]{40}\\b"), // Generic 40-char tokens
// Authorization headers
Pattern.compile("(?i)authorization:\\s*(bearer|basic)\\s+[^\\s]+"),
// Password in key-value pairs
Pattern.compile("(?i)(password|pwd|pass|passphrase)[=:]['\"]?([^,'\"\\s]+)['\"]?"),
// Secret keys
Pattern.compile("(?i)(secret|secretkey|privatekey|apisecret)[=:]['\"]?([^,'\"\\s]+)['\"]?"),
// Database connection strings
Pattern.compile("(jdbc|mongodb|redis|postgresql)://[^:]+:[^@]+@[^\\s]+"),
// JWT tokens
Pattern.compile("\\beyJ[\\w-]+\\.[\\w-]+\\.[\\w-]+\\b"),
// IP addresses (optional - based on requirement)
Pattern.compile("\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b"),
// MAC addresses
Pattern.compile("\\b(?:[0-9A-Fa-f]{2}[:-]){5}(?:[0-9A-Fa-f]{2})\\b"),
// Vehicle identification numbers
Pattern.compile("\\b[A-HJ-NPR-Z0-9]{17}\\b")
);
}

2. Layered Defense Strategy

public class LayeredLogMasking {
// Layer 1: Pre-logging validation
public void logSafely(String message, Object... args) {
if (containsSensitiveData(message)) {
throw new SecurityException("Attempt to log sensitive data: " + 
maskForValidation(message));
}
// Apply masking before logging
String maskedMessage = maskMessage(message);
Object[] maskedArgs = maskArguments(args);
logger.info(maskedMessage, maskedArgs);
}
// Layer 2: Runtime monitoring
@Component
public class LogMonitoringAspect {
private final Set<String> sensitiveLogs = Collections.synchronizedSet(new HashSet<>());
@AfterReturning("execution(* org.slf4j.Logger.*(..))")
public void monitorLogs(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if (args.length > 0 && args[0] instanceof String) {
String message = (String) args[0];
if (containsUnmaskedSensitiveData(message)) {
sensitiveLogs.add(message);
alertSecurityTeam(message);
}
}
}
private boolean containsUnmaskedSensitiveData(String message) {
// Check if message contains unmasked sensitive patterns
return ComprehensiveMaskingPatterns.SENSITIVE_DATA_PATTERNS.stream()
.anyMatch(pattern -> pattern.matcher(message).find());
}
private void alertSecurityTeam(String message) {
// Send alert to security team
System.err.println("SECURITY ALERT: Unmasked sensitive data in logs: " + 
maskForValidation(message));
}
}
// Layer 3: Post-logging scanning
@Component
public class LogFileScanner {
@Scheduled(fixedRate = 300000) // Scan every 5 minutes
public void scanLogFiles() {
scanLogFileForSensitiveData("logs/application.log");
scanLogFileForSensitiveData("logs/application.json");
}
private void scanLogFileForSensitiveData(String filePath) {
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
if (containsUnmaskedSensitiveData(line)) {
handleSensitiveDataFound(filePath, lineNumber, line);
}
}
} catch (IOException e) {
System.err.println("Error scanning log file: " + e.getMessage());
}
}
private void handleSensitiveDataFound(String filePath, int lineNumber, String line) {
// Log the incident
System.err.printf("Sensitive data found in %s at line %d: %s%n", 
filePath, lineNumber, maskForValidation(line));
// Optionally: truncate or sanitize the log file
// This should be done carefully in production
}
}
}

Conclusion

Implementing comprehensive log masking in Java applications is crucial for security and compliance. Key takeaways:

  1. Use Multiple Layers: Combine pattern-based, annotation-based, and framework-level masking
  2. Customize for Your Needs: Adapt masking patterns based on your specific sensitive data types
  3. Test Thoroughly: Ensure masking works correctly and doesn't impact performance
  4. Monitor Continuously: Implement monitoring to detect unmasked sensitive data
  5. Follow Compliance Requirements: Ensure your masking strategy meets regulatory requirements (GDPR, HIPAA, PCI-DSS)

By implementing these strategies, you can significantly reduce the risk of sensitive data exposure through application logs while maintaining useful logging for debugging and monitoring purposes.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper