java
57 lines · 8 steps
Masking sensitive data with regex in Java
A utility class redacts card numbers, emails, and SSNs from text while preserving just enough to stay useful.
Explained by
highlit
1public final class SensitiveDataMasker {
2
3 private static final Pattern CARD_NUMBER = Pattern.compile("\\b(?:\\d[ -]*?){13,19}\\b");
4 private static final Pattern EMAIL = Pattern.compile("[A-Za-z0-9._%+-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})");
5 private static final Pattern SSN = Pattern.compile("\\b\\d{3}-\\d{2}-(\\d{4})\\b");
6
7 private SensitiveDataMasker() {
8 }
9
10 public static String mask(String input) {
11 if (input == null || input.isEmpty()) {
12 return input;
13 }
14 String result = maskCardNumbers(input);
15 result = maskEmails(result);
16 result = maskSsns(result);
17 return result;
18 }
19
20 private static String maskCardNumbers(String input) {
21 Matcher matcher = CARD_NUMBER.matcher(input);
22 StringBuilder sb = new StringBuilder();
23 while (matcher.find()) {
24 String digits = matcher.group().replaceAll("[ -]", "");
25 if (isLuhnValid(digits)) {
26 String last4 = digits.substring(digits.length() - 4);
27 matcher.appendReplacement(sb, Matcher.quoteReplacement("****-****-****-" + last4));
28 } else {
29 matcher.appendReplacement(sb, Matcher.quoteReplacement(matcher.group()));
30 }
31 }
32 matcher.appendTail(sb);
33 return sb.toString();
34 }
35
36 private static String maskEmails(String input) {
37 return EMAIL.matcher(input).replaceAll("***@$1");
38 }
39
40 private static String maskSsns(String input) {
41 return SSN.matcher(input).replaceAll("***-**-$1");
42 }
43
44 private static boolean isLuhnValid(String digits) {
45 int sum = 0;
46 boolean doubleDigit = false;
47 for (int i = digits.length() - 1; i >= 0; i--) {
48 int d = digits.charAt(i) - '0';
49 if (doubleDigit && (d *= 2) > 9) {
50 d -= 9;
51 }
52 sum += d;
53 doubleDigit = !doubleDigit;
54 }
55 return sum % 10 == 0;
56 }
57}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Compiling patterns once as static finals avoids recompiling regex on every call.
- 2Validating a card with Luhn before masking prevents false positives on random digit runs.
- 3Capturing groups in replacement strings lets you redact part of a match while keeping the rest.
Related explainers
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/masking-sensitive-data-with-regex-in-java-explained-java-50b0/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.