Skip to content

Commit 7c94097

Browse files
Flossyclaude
andcommitted
fix: resolve CredentialMasker regex group reference bugs
- Fixed IllegalArgumentException caused by unescaped $ in replacement strings - Changed replaceAll() to use string concatenation instead of literal $MASKED_VALUE - Fixed API_KEY_PATTERN to match 'API key' with space ([_\s-]? instead of [_-]?) - All 14 CredentialMasker tests now pass Fixes #383 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 28daab8 commit 7c94097

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright (C) 2024-2026 FlossWare
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
package org.flossware.platform.api;
19+
20+
import java.util.regex.Pattern;
21+
22+
/**
23+
* Utility for masking sensitive credentials in logs and error messages.
24+
*
25+
* <p>Prevents accidental exposure of passwords, tokens, API keys, and other sensitive data in
26+
* logging output.
27+
*
28+
* <p>Patterns masked:
29+
*
30+
* <ul>
31+
* <li>password=xxx
32+
* <li>token=xxx
33+
* <li>apikey=xxx
34+
* <li>api_key=xxx
35+
* <li>Authorization: Bearer xxx
36+
* <li>Basic base64-encoded-credentials
37+
* </ul>
38+
*
39+
* <p>Thread Safety: This class is thread-safe.
40+
*
41+
* @since 1.1
42+
*/
43+
public final class CredentialMasker {
44+
45+
private static final String MASKED_VALUE = "***MASKED***";
46+
47+
// Regex patterns for common credential patterns
48+
private static final Pattern PASSWORD_PATTERN =
49+
Pattern.compile("password[\"']?\\s*[:=]\\s*[\"']?([^\"'\\s,}]+)", Pattern.CASE_INSENSITIVE);
50+
private static final Pattern TOKEN_PATTERN =
51+
Pattern.compile("token[\"']?\\s*[:=]\\s*[\"']?([^\"'\\s,}]+)", Pattern.CASE_INSENSITIVE);
52+
private static final Pattern API_KEY_PATTERN =
53+
Pattern.compile(
54+
"api[_\\s-]?key[\"']?\\s*[:=]\\s*[\"']?([^\"'\\s,}]+)", Pattern.CASE_INSENSITIVE);
55+
private static final Pattern BEARER_PATTERN =
56+
Pattern.compile("Bearer\\s+([^\\s]+)", Pattern.CASE_INSENSITIVE);
57+
private static final Pattern BASIC_AUTH_PATTERN = Pattern.compile("Basic\\s+([^\\s]+)");
58+
private static final Pattern ENV_VAR_PATTERN = Pattern.compile("\\$\\{([A-Z0-9_]+)\\}");
59+
60+
private CredentialMasker() {
61+
// Utility class
62+
}
63+
64+
/**
65+
* Masks sensitive credentials in the given message.
66+
*
67+
* @param message the message that may contain credentials
68+
* @return the message with credentials masked
69+
*/
70+
public static String mask(String message) {
71+
if (message == null || message.isEmpty()) {
72+
return message;
73+
}
74+
75+
String masked = message;
76+
77+
// Mask password values
78+
masked = PASSWORD_PATTERN.matcher(masked).replaceAll("password=" + MASKED_VALUE);
79+
80+
// Mask token values
81+
masked = TOKEN_PATTERN.matcher(masked).replaceAll("token=" + MASKED_VALUE);
82+
83+
// Mask API key values
84+
masked = API_KEY_PATTERN.matcher(masked).replaceAll("api_key=" + MASKED_VALUE);
85+
86+
// Mask Bearer tokens
87+
masked = BEARER_PATTERN.matcher(masked).replaceAll("Bearer " + MASKED_VALUE);
88+
89+
// Mask Basic auth
90+
masked = BASIC_AUTH_PATTERN.matcher(masked).replaceAll("Basic " + MASKED_VALUE);
91+
92+
// Mask environment variable references (but not the variable name)
93+
masked = ENV_VAR_PATTERN.matcher(masked).replaceAll("\\$\\{$1\\}");
94+
95+
return masked;
96+
}
97+
98+
/**
99+
* Masks a specific credential value.
100+
*
101+
* @param credentialValue the credential value to mask
102+
* @return a masked version of the credential
103+
*/
104+
public static String maskValue(String credentialValue) {
105+
if (credentialValue == null || credentialValue.isEmpty()) {
106+
return credentialValue;
107+
}
108+
// Show first character and last character only for debugging
109+
if (credentialValue.length() <= 2) {
110+
return MASKED_VALUE;
111+
}
112+
return credentialValue.charAt(0) + "..." + credentialValue.charAt(credentialValue.length() - 1);
113+
}
114+
}

0 commit comments

Comments
 (0)