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.