Add --scrub-log flag to remove possibly sensitive information from the log

This commit is contained in:
AsamK 2022-09-04 09:48:53 +02:00
parent 2e8e81a926
commit 1d77153a2b
7 changed files with 308 additions and 17 deletions

View file

@ -71,6 +71,9 @@ public class App {
parser.addArgument("--log-file")
.type(File.class)
.help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file.");
parser.addArgument("--scrub-log")
.action(Arguments.storeTrue())
.help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs.");
parser.addArgument("-c", "--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");

View file

@ -27,6 +27,7 @@ import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.logging.LogConfigurator;
import org.asamk.signal.manager.ManagerLogger;
import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@ -47,7 +48,8 @@ public class Main {
final var nsLog = parseArgs(args);
final var verboseLevel = nsLog == null ? 0 : nsLog.getInt("verbose");
final var logFile = nsLog == null ? null : nsLog.<File>get("log-file");
configureLogging(verboseLevel, logFile);
final var scrubLog = nsLog != null && nsLog.getBoolean("scrub-log");
configureLogging(verboseLevel, logFile, scrubLog);
var parser = App.buildArgumentParser();
@ -82,6 +84,7 @@ public class Main {
.defaultHelp(false);
parser.addArgument("-v", "--verbose").action(Arguments.count());
parser.addArgument("--log-file").type(File.class);
parser.addArgument("--scrub-log").action(Arguments.storeTrue());
try {
return parser.parseKnownArgs(args, null);
@ -90,9 +93,10 @@ public class Main {
}
}
private static void configureLogging(final int verboseLevel, final File logFile) {
private static void configureLogging(final int verboseLevel, final File logFile, final boolean scrubLog) {
LogConfigurator.setVerboseLevel(verboseLevel);
LogConfigurator.setLogFile(logFile);
LogConfigurator.setScrubSensitiveInformation(scrubLog);
if (verboseLevel > 0) {
java.util.logging.Logger.getLogger("")

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.logging;
import java.io.File;
@ -20,6 +20,7 @@ public class LogConfigurator extends ContextAwareBase implements Configurator {
private static int verboseLevel = 0;
private static File logFile = null;
private static boolean scrubSensitiveInformation = false;
public static void setVerboseLevel(int verboseLevel) {
LogConfigurator.verboseLevel = verboseLevel;
@ -29,6 +30,10 @@ public class LogConfigurator extends ContextAwareBase implements Configurator {
LogConfigurator.logFile = logFile;
}
public static void setScrubSensitiveInformation(final boolean scrubSensitiveInformation) {
LogConfigurator.scrubSensitiveInformation = scrubSensitiveInformation;
}
public ExecutionStatus configure(LoggerContext lc) {
final var rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
@ -98,18 +103,26 @@ public class LogConfigurator extends ContextAwareBase implements Configurator {
}
private PatternLayout createSimpleLoggingLayout(final LoggerContext lc) {
return new PatternLayout() {{
setPattern("%-5level %logger{0} - %msg%n");
setContext(lc);
start();
}};
final var patternLayout = getPatternLayout();
patternLayout.setPattern("%-5level %logger{0} - %msg%n");
patternLayout.setContext(lc);
patternLayout.start();
return patternLayout;
}
private PatternLayout createDetailedLoggingLayout(final LoggerContext lc) {
return new PatternLayout() {{
setPattern("%d{yyyy-MM-dd'T'HH:mm:ss.SSSXX} [%thread] %-5level %logger{36} - %msg%n");
setContext(lc);
start();
}};
final var patternLayout = getPatternLayout();
patternLayout.setPattern("%d{yyyy-MM-dd'T'HH:mm:ss.SSSXX} [%thread] %-5level %logger{36} - %msg%n");
patternLayout.setContext(lc);
patternLayout.start();
return patternLayout;
}
private PatternLayout getPatternLayout() {
if (scrubSensitiveInformation) {
return new ScrubberPatternLayout();
} else {
return new PatternLayout();
}
}
}

View file

@ -0,0 +1,256 @@
/*
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.asamk.signal.logging;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Scrub data for possibly sensitive information.
*/
public final class Scrubber {
private Scrubber() {
}
/**
* The middle group will be censored.
* Supposedly, the shortest international phone numbers in use contain seven digits.
* Handles URL encoded +, %2B
*/
private static final Pattern E164_PATTERN = Pattern.compile("(\\+|%2B)(\\d{5,13})(\\d{2})");
private static final String E164_CENSOR = "*************";
/**
* The second group will be censored.
*/
private static final Pattern CRUDE_EMAIL_PATTERN = Pattern.compile("\\b([^\\s/])([^\\s/]*@[^\\s]+)");
private static final String EMAIL_CENSOR = "...@...";
/**
* The middle group will be censored.
*/
private static final Pattern UUID_PATTERN = Pattern.compile(
"(JOB::)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{10})([0-9a-f]{2})",
Pattern.CASE_INSENSITIVE);
private static final String UUID_CENSOR = "********-****-****-****-**********";
/**
* The entire string is censored.
*/
private static final Pattern IPV4_PATTERN = Pattern.compile("\\b"
+ "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\."
+ "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\."
+ "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\."
+ "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\b");
private static final String IPV4_CENSOR = "...ipv4...";
/**
* The domain name except for TLD will be censored.
*/
private static final Pattern DOMAIN_PATTERN = Pattern.compile("([a-z0-9]+\\.)+([a-z0-9\\-]*[a-z\\-][a-z0-9\\-]*)",
Pattern.CASE_INSENSITIVE);
private static final String DOMAIN_CENSOR = "***.";
private static final Set<String> TOP_100_TLDS = new HashSet<>(Arrays.asList("com",
"net",
"org",
"jp",
"de",
"uk",
"fr",
"br",
"it",
"ru",
"es",
"me",
"gov",
"pl",
"ca",
"au",
"cn",
"co",
"in",
"nl",
"edu",
"info",
"eu",
"ch",
"id",
"at",
"kr",
"cz",
"mx",
"be",
"tv",
"se",
"tr",
"tw",
"al",
"ua",
"ir",
"vn",
"cl",
"sk",
"ly",
"cc",
"to",
"no",
"fi",
"us",
"pt",
"dk",
"ar",
"hu",
"tk",
"gr",
"il",
"news",
"ro",
"my",
"biz",
"ie",
"za",
"nz",
"sg",
"ee",
"th",
"io",
"xyz",
"pe",
"bg",
"hk",
"lt",
"link",
"ph",
"club",
"si",
"site",
"mobi",
"by",
"cat",
"wiki",
"la",
"ga",
"xxx",
"cf",
"hr",
"ng",
"jobs",
"online",
"kz",
"ug",
"gq",
"ae",
"is",
"lv",
"pro",
"fm",
"tips",
"ms",
"sa",
"app"));
public static CharSequence scrub(CharSequence in) {
in = scrubE164(in);
in = scrubEmail(in);
in = scrubUuids(in);
in = scrubDomains(in);
in = scrubIpv4(in);
return in;
}
private static CharSequence scrubE164(CharSequence in) {
return scrub(in,
E164_PATTERN,
(matcher, output) -> output.append(matcher.group(1))
.append(E164_CENSOR, 0, matcher.group(2).length())
.append(matcher.group(3)));
}
private static CharSequence scrubEmail(CharSequence in) {
return scrub(in,
CRUDE_EMAIL_PATTERN,
(matcher, output) -> output.append(matcher.group(1)).append(EMAIL_CENSOR));
}
private static CharSequence scrubUuids(CharSequence in) {
return scrub(in, UUID_PATTERN, (matcher, output) -> {
if (matcher.group(1) != null && !matcher.group(1).isEmpty()) {
output.append(matcher.group(1)).append(matcher.group(2)).append(matcher.group(3));
} else {
output.append(UUID_CENSOR).append(matcher.group(3));
}
});
}
private static CharSequence scrubDomains(CharSequence in) {
return scrub(in, DOMAIN_PATTERN, (matcher, output) -> {
String match = matcher.group(0);
if (matcher.groupCount() == 2
&& TOP_100_TLDS.contains(matcher.group(2).toLowerCase(Locale.US))
&& !match.endsWith("whispersystems.org")
&& !match.endsWith("signal.org")) {
output.append(DOMAIN_CENSOR).append(matcher.group(2));
} else {
output.append(match);
}
});
}
private static CharSequence scrubIpv4(CharSequence in) {
return scrub(in, IPV4_PATTERN, (matcher, output) -> output.append(IPV4_CENSOR));
}
private static CharSequence scrub(
CharSequence in, Pattern pattern, ProcessMatch processMatch
) {
final StringBuilder output = new StringBuilder(in.length());
final Matcher matcher = pattern.matcher(in);
int lastEndingPos = 0;
while (matcher.find()) {
output.append(in, lastEndingPos, matcher.start());
processMatch.scrubMatch(matcher, output);
lastEndingPos = matcher.end();
}
if (lastEndingPos == 0) {
// there were no matches, save copying all the data
return in;
} else {
output.append(in, lastEndingPos, in.length());
return output;
}
}
private interface ProcessMatch {
void scrubMatch(Matcher matcher, StringBuilder output);
}
}

View file

@ -0,0 +1,12 @@
package org.asamk.signal.logging;
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
public class ScrubberPatternLayout extends PatternLayout {
@Override
public String doLayout(ILoggingEvent event) {
return Scrubber.scrub(super.doLayout(event)).toString();
}
}

View file

@ -1 +1 @@
org.asamk.signal.LogConfigurator
org.asamk.signal.logging.LogConfigurator