Introduction to Real-Time Data Masking
Real-time data masking is the process of obscuring sensitive data on-the-fly as it flows through applications, APIs, or data streams. Unlike static masking applied to databases, real-time masking protects data in motion while preserving its format and utility for authorized use cases.
Prerequisites
- Java 11 or later
- Understanding of data streaming and transformation
- Basic knowledge of regular expressions and data patterns
Step 1: Project Setup and Dependencies
Maven Configuration (pom.xml)
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring-boot.version>3.1.0</spring-boot.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Spring Boot for web and streaming -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Apache Commons for utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.7</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
<!-- Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.6</version>
</dependency>
</dependencies>
Step 2: Core Masking Strategies and Patterns
Masking Strategy Interface
package com.example.datamasking.masking;
/**
* Interface for all data masking strategies
*/
public interface MaskingStrategy {
String mask(String data);
String mask(String data, String context);
boolean supports(String dataType);
}
/**
* Different masking techniques
*/
public enum MaskingTechnique {
FULL, // Replace entire value
PARTIAL, // Mask only part of the value
FORMAT_PRESERVING, // Maintain original format
TOKENIZATION, // Replace with token
ENCRYPTION, // Encrypt the value
HASHING, // Hash the value
PSEUDONYMIZATION // Replace with realistic fake data
}
Common Masking Implementations
package com.example.datamasking.masking;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.regex.Pattern;
@Component
public class CommonMaskingStrategies {
// Regex patterns for common sensitive data types
private static final Pattern EMAIL_PATTERN =
Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b");
private static final Pattern PHONE_PATTERN =
Pattern.compile("\\b(\\+?\\d{1,3}[-.]?)?\\(?\\d{3}\\)?[-.]?\\d{3}[-.]?\\d{4}\\b");
private static final Pattern CREDIT_CARD_PATTERN =
Pattern.compile("\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b");
private static final Pattern SSN_PATTERN =
Pattern.compile("\\b\\d{3}-\\d{2}-\\d{4}\\b");
private static final String MASK_CHAR = "*";
private static final String MASK_EMAIL_DOMAIN = "example.com";
/**
* Full masking - replace entire content with fixed mask
*/
public static class FullMaskingStrategy implements MaskingStrategy {
private final String maskValue;
public FullMaskingStrategy(String maskValue) {
this.maskValue = maskValue;
}
public FullMaskingStrategy() {
this("***MASKED***");
}
@Override
public String mask(String data) {
return maskValue;
}
@Override
public String mask(String data, String context) {
return maskValue;
}
@Override
public boolean supports(String dataType) {
return true; // Supports all data types
}
}
/**
* Partial masking - show only first and last characters
*/
public static class PartialMaskingStrategy implements MaskingStrategy {
private final int visiblePrefix;
private final int visibleSuffix;
public PartialMaskingStrategy(int visiblePrefix, int visibleSuffix) {
this.visiblePrefix = visiblePrefix;
this.visibleSuffix = visibleSuffix;
}
public PartialMaskingStrategy() {
this(2, 2); // Show first 2 and last 2 characters by default
}
@Override
public String mask(String data) {
if (StringUtils.isBlank(data)) return data;
if (data.length() <= visiblePrefix + visibleSuffix) {
return StringUtils.repeat(MASK_CHAR, data.length());
}
String prefix = data.substring(0, visiblePrefix);
String suffix = data.substring(data.length() - visibleSuffix);
String middle = StringUtils.repeat(MASK_CHAR,
data.length() - visiblePrefix - visibleSuffix);
return prefix + middle + suffix;
}
@Override
public String mask(String data, String context) {
return mask(data);
}
@Override
public boolean supports(String dataType) {
return true;
}
}
/**
* Email-specific masking
*/
public static class EmailMaskingStrategy implements MaskingStrategy {
@Override
public String mask(String email) {
if (StringUtils.isBlank(email) || !EMAIL_PATTERN.matcher(email).matches()) {
return email;
}
int atIndex = email.indexOf('@');
if (atIndex <= 1) {
return "***@" + MASK_EMAIL_DOMAIN;
}
String username = email.substring(0, atIndex);
String domain = email.substring(atIndex + 1);
// Show first character, mask the rest of username
String maskedUsername = username.charAt(0) +
StringUtils.repeat(MASK_CHAR, Math.max(0, username.length() - 1));
return maskedUsername + "@" + domain;
}
@Override
public String mask(String email, String context) {
return mask(email);
}
@Override
public boolean supports(String dataType) {
return "email".equalsIgnoreCase(dataType);
}
}
/**
* Phone number masking
*/
public static class PhoneMaskingStrategy implements MaskingStrategy {
@Override
public String mask(String phone) {
if (StringUtils.isBlank(phone)) return phone;
// Remove non-digit characters
String digits = phone.replaceAll("\\D", "");
if (digits.length() < 7) return StringUtils.repeat(MASK_CHAR, phone.length());
// Format: +1 (XXX) XXX-XXXX -> +1 (XXX) ***-XXXX
if (digits.length() == 11 || digits.length() == 10) {
String areaCode = digits.length() == 11 ? digits.substring(1, 4) : digits.substring(0, 3);
String lastFour = digits.substring(digits.length() - 4);
return String.format("(%s) ***-%s", areaCode, lastFour);
}
// Fallback: show last 4 digits
return "***-" + digits.substring(digits.length() - 4);
}
@Override
public String mask(String phone, String context) {
return mask(phone);
}
@Override
public boolean supports(String dataType) {
return "phone".equalsIgnoreCase(dataType) || "phonenumber".equalsIgnoreCase(dataType);
}
}
/**
* Credit card masking
*/
public static class CreditCardMaskingStrategy implements MaskingStrategy {
@Override
public String mask(String creditCard) {
if (StringUtils.isBlank(creditCard)) return creditCard;
String digits = creditCard.replaceAll("\\D", "");
if (digits.length() < 12) {
return StringUtils.repeat(MASK_CHAR, creditCard.length());
}
// Show last 4 digits, mask the rest
String lastFour = digits.substring(digits.length() - 4);
String masked = StringUtils.repeat(MASK_CHAR, digits.length() - 4) + lastFour;
// Preserve original formatting if any
if (creditCard.contains("-")) {
return formatWithDashes(masked);
} else if (creditCard.contains(" ")) {
return formatWithSpaces(masked);
}
return masked;
}
private String formatWithDashes(String maskedDigits) {
StringBuilder formatted = new StringBuilder();
for (int i = 0; i < maskedDigits.length(); i++) {
if (i > 0 && i % 4 == 0) {
formatted.append("-");
}
formatted.append(maskedDigits.charAt(i));
}
return formatted.toString();
}
private String formatWithSpaces(String maskedDigits) {
StringBuilder formatted = new StringBuilder();
for (int i = 0; i < maskedDigits.length(); i++) {
if (i > 0 && i % 4 == 0) {
formatted.append(" ");
}
formatted.append(maskedDigits.charAt(i));
}
return formatted.toString();
}
@Override
public String mask(String creditCard, String context) {
return mask(creditCard);
}
@Override
public boolean supports(String dataType) {
return "creditcard".equalsIgnoreCase(dataType) ||
"credit_card".equalsIgnoreCase(dataType) ||
"card".equalsIgnoreCase(dataType);
}
}
/**
* SSN masking
*/
public static class SSDMaskingStrategy implements MaskingStrategy {
@Override
public String mask(String ssn) {
if (StringUtils.isBlank(ssn)) return ssn;
String digits = ssn.replaceAll("\\D", "");
if (digits.length() != 9) {
return StringUtils.repeat(MASK_CHAR, ssn.length());
}
return "***-**-" + digits.substring(5);
}
@Override
public String mask(String ssn, String context) {
return mask(ssn);
}
@Override
public boolean supports(String dataType) {
return "ssn".equalsIgnoreCase(dataType) ||
"socialsecurity".equalsIgnoreCase(dataType);
}
}
}
Step 3: Real-Time Masking Engine
Masking Engine Core
package com.example.datamasking.engine;
import com.example.datamasking.masking.MaskingStrategy;
import com.example.datamasking.masking.CommonMaskingStrategies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
@Component
public class RealTimeMaskingEngine {
private static final Logger logger = LoggerFactory.getLogger(RealTimeMaskingEngine.class);
private final Map<String, MaskingStrategy> strategyRegistry;
private final Map<Pattern, MaskingStrategy> patternBasedStrategies;
private final MaskingStrategy defaultStrategy;
// Field-level masking configurations
private final Map<String, String> fieldMaskingConfig;
public RealTimeMaskingEngine() {
this.strategyRegistry = new ConcurrentHashMap<>();
this.patternBasedStrategies = new ConcurrentHashMap<>();
this.fieldMaskingConfig = new ConcurrentHashMap<>();
this.defaultStrategy = new CommonMaskingStrategies.PartialMaskingStrategy();
initializeDefaultStrategies();
initializeFieldConfigurations();
}
@PostConstruct
public void init() {
logger.info("Real-time masking engine initialized with {} strategies",
strategyRegistry.size());
}
private void initializeDefaultStrategies() {
// Register default strategies
registerStrategy("email", new CommonMaskingStrategies.EmailMaskingStrategy());
registerStrategy("phone", new CommonMaskingStrategies.PhoneMaskingStrategy());
registerStrategy("creditcard", new CommonMaskingStrategies.CreditCardMaskingStrategy());
registerStrategy("ssn", new CommonMaskingStrategies.SSDMaskingStrategy());
registerStrategy("default", new CommonMaskingStrategies.PartialMaskingStrategy());
registerStrategy("full", new CommonMaskingStrategies.FullMaskingStrategy());
// Pattern-based strategies
patternBasedStrategies.put(
Pattern.compile(".*[Ee]mail.*"),
new CommonMaskingStrategies.EmailMaskingStrategy()
);
patternBasedStrategies.put(
Pattern.compile(".*[Pp]hone.*"),
new CommonMaskingStrategies.PhoneMaskingStrategy()
);
patternBasedStrategies.put(
Pattern.compile(".*[Cc]ard.*"),
new CommonMaskingStrategies.CreditCardMaskingStrategy()
);
patternBasedStrategies.put(
Pattern.compile(".*[Ss][Ss][Nn].*"),
new CommonMaskingStrategies.SSDMaskingStrategy()
);
}
private void initializeFieldConfigurations() {
// Pre-configured field mappings
fieldMaskingConfig.put("email", "email");
fieldMaskingConfig.put("phoneNumber", "phone");
fieldMaskingConfig.put("mobile", "phone");
fieldMaskingConfig.put("creditCard", "creditcard");
fieldMaskingConfig.put("ssn", "ssn");
fieldMaskingConfig.put("socialSecurityNumber", "ssn");
fieldMaskingConfig.put("password", "full");
}
public void registerStrategy(String dataType, MaskingStrategy strategy) {
strategyRegistry.put(dataType.toLowerCase(), strategy);
logger.debug("Registered masking strategy for type: {}", dataType);
}
public void registerFieldMapping(String fieldName, String strategyType) {
fieldMaskingConfig.put(fieldName, strategyType);
logger.debug("Registered field mapping: {} -> {}", fieldName, strategyType);
}
/**
* Mask a value based on field name
*/
public String maskField(String fieldName, String value) {
if (value == null) return null;
String strategyType = fieldMaskingConfig.get(fieldName);
if (strategyType == null) {
// Try pattern matching
strategyType = findStrategyByPattern(fieldName);
}
MaskingStrategy strategy = strategyRegistry.get(strategyType);
if (strategy == null) {
strategy = defaultStrategy;
}
String maskedValue = strategy.mask(value, fieldName);
logger.debug("Masked field '{}': '{}' -> '{}'", fieldName, value, maskedValue);
return maskedValue;
}
/**
* Mask a value with explicit strategy
*/
public String maskWithStrategy(String value, String strategyType) {
if (value == null) return null;
MaskingStrategy strategy = strategyRegistry.get(strategyType.toLowerCase());
if (strategy == null) {
logger.warn("Strategy not found: {}, using default", strategyType);
strategy = defaultStrategy;
}
return strategy.mask(value);
}
/**
* Auto-detect data type and mask accordingly
*/
public String autoMask(String value) {
if (value == null) return null;
// Try to detect data type and apply appropriate masking
if (CommonMaskingStrategies.EMAIL_PATTERN.matcher(value).matches()) {
return strategyRegistry.get("email").mask(value);
} else if (CommonMaskingStrategies.CREDIT_CARD_PATTERN.matcher(value).matches()) {
return strategyRegistry.get("creditcard").mask(value);
} else if (CommonMaskingStrategies.SSN_PATTERN.matcher(value).matches()) {
return strategyRegistry.get("ssn").mask(value);
} else if (CommonMaskingStrategies.PHONE_PATTERN.matcher(value).matches()) {
return strategyRegistry.get("phone").mask(value);
}
return defaultStrategy.mask(value);
}
private String findStrategyByPattern(String fieldName) {
for (Map.Entry<Pattern, MaskingStrategy> entry : patternBasedStrategies.entrySet()) {
if (entry.getKey().matcher(fieldName).matches()) {
return getStrategyType(entry.getValue());
}
}
return "default";
}
private String getStrategyType(MaskingStrategy strategy) {
for (Map.Entry<String, MaskingStrategy> entry : strategyRegistry.entrySet()) {
if (entry.getValue().getClass().equals(strategy.getClass())) {
return entry.getKey();
}
}
return "default";
}
public Set<String> getSupportedDataTypes() {
return Collections.unmodifiableSet(strategyRegistry.keySet());
}
public Map<String, String> getFieldConfigurations() {
return Collections.unmodifiableMap(fieldMaskingConfig);
}
}
Step 4: JSON Data Masking
JSON Masking Processor
package com.example.datamasking.processor;
import com.example.datamasking.engine.RealTimeMaskingEngine;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Iterator;
import java.util.Map;
@Component
public class JsonMaskingProcessor {
private static final Logger logger = LoggerFactory.getLogger(JsonMaskingProcessor.class);
private final RealTimeMaskingEngine maskingEngine;
private final ObjectMapper objectMapper;
public JsonMaskingProcessor(RealTimeMaskingEngine maskingEngine, ObjectMapper objectMapper) {
this.maskingEngine = maskingEngine;
this.objectMapper = objectMapper;
}
/**
* Mask sensitive fields in JSON object
*/
public String maskJson(String jsonString) {
try {
JsonNode rootNode = objectMapper.readTree(jsonString);
JsonNode maskedNode = maskJsonNode(rootNode, "");
return objectMapper.writeValueAsString(maskedNode);
} catch (Exception e) {
logger.error("Failed to mask JSON", e);
return jsonString; // Return original on error
}
}
/**
* Mask specific fields in JSON object
*/
public String maskJsonFields(String jsonString, Map<String, String> fieldMasks) {
try {
JsonNode rootNode = objectMapper.readTree(jsonString);
JsonNode maskedNode = maskSpecificFields(rootNode, fieldMasks);
return objectMapper.writeValueAsString(maskedNode);
} catch (Exception e) {
logger.error("Failed to mask specific JSON fields", e);
return jsonString;
}
}
private JsonNode maskJsonNode(JsonNode node, String path) {
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node;
ObjectNode maskedNode = objectMapper.createObjectNode();
Iterator<Map.Entry<String, JsonNode>> fields = objectNode.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
String fieldName = field.getKey();
JsonNode fieldValue = field.getValue();
String currentPath = path.isEmpty() ? fieldName : path + "." + fieldName;
JsonNode maskedValue = maskJsonNode(fieldValue, currentPath);
maskedNode.set(fieldName, maskedValue);
}
return maskedNode;
} else if (node.isArray()) {
ArrayNode arrayNode = (ArrayNode) node;
ArrayNode maskedArray = objectMapper.createArrayNode();
for (int i = 0; i < arrayNode.size(); i++) {
JsonNode element = arrayNode.get(i);
String elementPath = path + "[" + i + "]";
maskedArray.add(maskJsonNode(element, elementPath));
}
return maskedArray;
} else if (node.isValueNode()) {
// Apply masking to leaf values based on field path
String stringValue = node.asText();
String maskedValue = maskingEngine.maskField(path, stringValue);
return new TextNode(maskedValue);
}
return node;
}
private JsonNode maskSpecificFields(JsonNode node, Map<String, String> fieldMasks) {
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node;
ObjectNode maskedNode = objectMapper.createObjectNode();
Iterator<Map.Entry<String, JsonNode>> fields = objectNode.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
String fieldName = field.getKey();
JsonNode fieldValue = field.getValue();
if (fieldMasks.containsKey(fieldName) && fieldValue.isValueNode()) {
String strategy = fieldMasks.get(fieldName);
String maskedValue = maskingEngine.maskWithStrategy(fieldValue.asText(), strategy);
maskedNode.set(fieldName, new TextNode(maskedValue));
} else if (fieldValue.isContainerNode()) {
maskedNode.set(fieldName, maskSpecificFields(fieldValue, fieldMasks));
} else {
maskedNode.set(fieldName, fieldValue);
}
}
return maskedNode;
} else if (node.isArray()) {
ArrayNode arrayNode = (ArrayNode) node;
ArrayNode maskedArray = objectMapper.createArrayNode();
for (JsonNode element : arrayNode) {
maskedArray.add(maskSpecificFields(element, fieldMasks));
}
return maskedArray;
}
return node;
}
/**
* Performance-optimized streaming JSON masking for large payloads
*/
public String maskJsonStreaming(String jsonString) {
// Simplified streaming implementation
// In production, use Jackson Streaming API for large JSON
return maskJson(jsonString);
}
}
Step 5: Streaming Data Masking
Reactive Stream Masking
package com.example.datamasking.streaming;
import com.example.datamasking.engine.RealTimeMaskingEngine;
import com.example.datamasking.processor.JsonMaskingProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Map;
import java.util.function.Function;
@Component
public class ReactiveMaskingService {
private static final Logger logger = LoggerFactory.getLogger(ReactiveMaskingService.class);
private final RealTimeMaskingEngine maskingEngine;
private final JsonMaskingProcessor jsonProcessor;
public ReactiveMaskingService(RealTimeMaskingEngine maskingEngine,
JsonMaskingProcessor jsonProcessor) {
this.maskingEngine = maskingEngine;
this.jsonProcessor = jsonProcessor;
}
/**
* Mask a stream of strings in real-time
*/
public Flux<String> maskStringStream(Flux<String> inputStream, String fieldName) {
return inputStream
.map(value -> maskingEngine.maskField(fieldName, value))
.doOnNext(masked -> logger.debug("Masked stream value: {}", masked));
}
/**
* Mask a stream of JSON objects in real-time
*/
public Flux<String> maskJsonStream(Flux<String> jsonStream) {
return jsonStream
.map(jsonProcessor::maskJson)
.doOnNext(masked -> logger.debug("Masked JSON stream element"));
}
/**
* Mask with backpressure handling
*/
public Flux<String> maskWithBackpressure(Flux<String> inputStream,
String fieldName,
int bufferSize) {
return inputStream
.buffer(bufferSize)
.flatMap(batch -> Flux.fromIterable(batch)
.map(value -> maskingEngine.maskField(fieldName, value))
.delayElements(Duration.ofMillis(10)) // Prevent overwhelming
);
}
/**
* Conditional masking based on predicate
*/
public Flux<String> conditionalMask(Flux<String> inputStream,
Function<String, Boolean> condition,
String fieldName) {
return inputStream
.map(value -> {
if (condition.apply(value)) {
return maskingEngine.maskField(fieldName, value);
}
return value;
});
}
/**
* Parallel masking for high-throughput scenarios
*/
public Flux<String> parallelMask(Flux<String> inputStream, String fieldName, int parallelism) {
return inputStream
.parallel(parallelism)
.runOn(reactor.core.scheduler.Schedulers.parallel())
.map(value -> maskingEngine.maskField(fieldName, value))
.sequential();
}
}
Step 6: REST API for Real-Time Masking
Masking REST Controller
package com.example.datamasking.controller;
import com.example.datamasking.engine.RealTimeMaskingEngine;
import com.example.datamasking.processor.JsonMaskingProcessor;
import com.example.datamasking.streaming.ReactiveMaskingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
@RestController
@RequestMapping("/api/masking")
public class DataMaskingController {
private static final Logger logger = LoggerFactory.getLogger(DataMaskingController.class);
private final RealTimeMaskingEngine maskingEngine;
private final JsonMaskingProcessor jsonProcessor;
private final ReactiveMaskingService reactiveService;
public DataMaskingController(RealTimeMaskingEngine maskingEngine,
JsonMaskingProcessor jsonProcessor,
ReactiveMaskingService reactiveService) {
this.maskingEngine = maskingEngine;
this.jsonProcessor = jsonProcessor;
this.reactiveService = reactiveService;
}
@PostMapping("/field")
public ResponseEntity<MaskingResponse> maskField(
@RequestBody FieldMaskingRequest request) {
try {
String maskedValue = maskingEngine.maskField(
request.getFieldName(),
request.getValue()
);
return ResponseEntity.ok(new MaskingResponse(
request.getValue(),
maskedValue,
"success"
));
} catch (Exception e) {
logger.error("Field masking failed", e);
return ResponseEntity.badRequest().body(new MaskingResponse(
request.getValue(),
null,
"error: " + e.getMessage()
));
}
}
@PostMapping("/json")
public ResponseEntity<JsonMaskingResponse> maskJson(
@RequestBody JsonMaskingRequest request) {
try {
String maskedJson = jsonProcessor.maskJson(request.getJson());
return ResponseEntity.ok(new JsonMaskingResponse(
maskedJson,
"success"
));
} catch (Exception e) {
logger.error("JSON masking failed", e);
return ResponseEntity.badRequest().body(new JsonMaskingResponse(
null,
"error: " + e.getMessage()
));
}
}
@PostMapping(value = "/stream", consumes = MediaType.APPLICATION_NDJSON_VALUE)
public Flux<MaskingResponse> maskStream(
@RequestBody Flux<FieldMaskingRequest> requestStream) {
return requestStream
.map(request -> {
String maskedValue = maskingEngine.maskField(
request.getFieldName(),
request.getValue()
);
return new MaskingResponse(request.getValue(), maskedValue, "success");
})
.doOnError(error -> logger.error("Stream masking failed", error));
}
@PostMapping(value = "/json/stream",
consumes = MediaType.APPLICATION_NDJSON_VALUE,
produces = MediaType.APPLICATION_NDJSON_VALUE)
public Flux<JsonMaskingResponse> maskJsonStream(
@RequestBody Flux<JsonMaskingRequest> jsonStream) {
return jsonStream
.map(request -> {
String maskedJson = jsonProcessor.maskJson(request.getJson());
return new JsonMaskingResponse(maskedJson, "success");
})
.doOnError(error -> logger.error("JSON stream masking failed", error));
}
@GetMapping("/strategies")
public ResponseEntity<Map<String, Object>> getStrategies() {
return ResponseEntity.ok(Map.of(
"supportedTypes", maskingEngine.getSupportedDataTypes(),
"fieldConfigs", maskingEngine.getFieldConfigurations()
));
}
@PostMapping("/configure")
public ResponseEntity<String> configureField(
@RequestParam String fieldName,
@RequestParam String strategyType) {
try {
maskingEngine.registerFieldMapping(fieldName, strategyType);
return ResponseEntity.ok("Configuration updated successfully");
} catch (Exception e) {
return ResponseEntity.badRequest().body("Configuration failed: " + e.getMessage());
}
}
// Request/Response DTOs
public static class FieldMaskingRequest {
private String fieldName;
private String value;
// Getters and setters
public String getFieldName() { return fieldName; }
public void setFieldName(String fieldName) { this.fieldName = fieldName; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
public static class JsonMaskingRequest {
private String json;
public String getJson() { return json; }
public void setJson(String json) { this.json = json; }
}
public static class MaskingResponse {
private final String original;
private final String masked;
private final String status;
public MaskingResponse(String original, String masked, String status) {
this.original = original;
this.masked = masked;
this.status = status;
}
// Getters
public String getOriginal() { return original; }
public String getMasked() { return masked; }
public String getStatus() { return status; }
}
public static class JsonMaskingResponse {
private final String maskedJson;
private final String status;
public JsonMaskingResponse(String maskedJson, String status) {
this.maskedJson = maskedJson;
this.status = status;
}
// Getters
public String getMaskedJson() { return maskedJson; }
public String getStatus() { return status; }
}
}
Step 7: Performance Monitoring and Metrics
Masking Metrics Collector
package com.example.datamasking.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class MaskingMetrics {
private final MeterRegistry meterRegistry;
private final Counter totalMaskingOperations;
private final Counter failedMaskingOperations;
private final Timer maskingTimer;
private final ConcurrentHashMap<String, AtomicLong> strategyUsage;
public MaskingMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.strategyUsage = new ConcurrentHashMap<>();
this.totalMaskingOperations = Counter.builder("masking.operations.total")
.description("Total number of masking operations")
.register(meterRegistry);
this.failedMaskingOperations = Counter.builder("masking.operations.failed")
.description("Number of failed masking operations")
.register(meterRegistry);
this.maskingTimer = Timer.builder("masking.operation.duration")
.description("Time taken for masking operations")
.register(meterRegistry);
}
public void recordMaskingOperation(String strategy, long durationMs, boolean success) {
totalMaskingOperations.increment();
if (!success) {
failedMaskingOperations.increment();
}
maskingTimer.record(durationMs, TimeUnit.MILLISECONDS);
strategyUsage.computeIfAbsent(strategy, k ->
meterRegistry.gauge("masking.strategy.usage",
new AtomicLong(0))).incrementAndGet();
}
public void recordFieldMasking(String fieldName) {
Counter.builder("masking.fields")
.tag("field", fieldName)
.register(meterRegistry)
.increment();
}
}
Step 8: Configuration and Application
Application Configuration
package com.example.datamasking.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MaskingConfig {
@Bean
public String[] defaultSensitiveFields() {
return new String[] {
"email", "phone", "ssn", "creditCard",
"password", "address", "birthDate", "ipAddress"
};
}
}
Spring Boot Application
package com.example.datamasking;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DataMaskingApplication {
public static void main(String[] args) {
SpringApplication.run(DataMaskingApplication.class, args);
}
}
Application Properties
# application.yml server: port: 8080 spring: application: name: data-masking-service logging: level: com.example.datamasking: DEBUG management: endpoints: web: exposure: include: health,metrics,info endpoint: health: show-details: always # Masking configuration masking: default-strategy: partial enable-auto-detection: true stream-buffer-size: 1000 max-json-size: 10MB
Step 9: Testing
Unit Tests
package com.example.datamasking.test;
import com.example.datamasking.engine.RealTimeMaskingEngine;
import com.example.datamasking.masking.CommonMaskingStrategies;
import com.example.datamasking.processor.JsonMaskingProcessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class DataMaskingTest {
@Autowired
private RealTimeMaskingEngine maskingEngine;
@Autowired
private JsonMaskingProcessor jsonProcessor;
@Test
void testEmailMasking() {
String email = "[email protected]";
String masked = maskingEngine.maskField("email", email);
assertNotNull(masked);
assertTrue(masked.contains("@"));
assertFalse(masked.equals(email));
System.out.println("Email masked: " + email + " -> " + masked);
}
@Test
void testPhoneMasking() {
String phone = "+1 (555) 123-4567";
String masked = maskingEngine.maskField("phone", phone);
assertNotNull(masked);
assertTrue(masked.contains("***"));
System.out.println("Phone masked: " + phone + " -> " + masked);
}
@Test
void testJsonMasking() {
String json = """
{
"name": "John Doe",
"email": "[email protected]",
"phone": "+1-555-123-4567",
"creditCard": "4111-1111-1111-1111",
"address": {
"street": "123 Main St",
"city": "Anytown"
}
}
""";
String maskedJson = jsonProcessor.maskJson(json);
assertNotNull(maskedJson);
assertFalse(maskedJson.contains("[email protected]"));
assertFalse(maskedJson.contains("4111-1111-1111-1111"));
System.out.println("Original JSON: " + json);
System.out.println("Masked JSON: " + maskedJson);
}
@Test
void testPerformance() {
int iterations = 1000;
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
maskingEngine.maskField("email", "test" + i + "@example.com");
}
long duration = System.currentTimeMillis() - startTime;
double avgTime = (double) duration / iterations;
System.out.println("Processed " + iterations + " emails in " + duration + "ms");
System.out.println("Average time per operation: " + avgTime + "ms");
assertTrue(avgTime < 1.0, "Masking should be fast enough for real-time use");
}
}
Performance Considerations
- Caching: Cache compiled regex patterns and strategy instances
- Object Pooling: Reuse Jackson ObjectMapper instances
- Stream Processing: Use reactive streams for high-throughput scenarios
- Parallel Processing: Utilize multiple cores for CPU-intensive masking
- Memory Management: Use efficient data structures and avoid object creation in hot paths
Security Best Practices
- No Data Leakage: Ensure original data is properly garbage collected
- Access Controls: Implement proper authentication and authorization
- Audit Logging: Log all masking operations for compliance
- Input Validation: Validate all inputs to prevent injection attacks
- Secure Configuration: Protect masking configuration from unauthorized changes
This real-time data masking implementation provides a comprehensive solution for protecting sensitive data as it flows through your Java applications, with support for various data types, streaming scenarios, and performance optimization.