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

@ -41,6 +41,9 @@ Raise log level and include lib signal logs.
Write log output to the given file. Write log output to the given file.
If `--verbose` is also given, the detailed logs will only be written to the log file. If `--verbose` is also given, the detailed logs will only be written to the log file.
*--scrub-log*::
Scrub possibly sensitive information from the log, like phone numbers and UUIDs.
*--config* CONFIG:: *--config* CONFIG::
Set the path, where to store the config. Set the path, where to store the config.
Make sure you have full read/write access to the given directory. Make sure you have full read/write access to the given directory.
@ -230,7 +233,8 @@ Read the message from standard input.
*-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]:: *-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]::
Add one or more files as attachment. Add one or more files as attachment.
Can be either a file path or a data URI. Data URI encoded attachments must follow the RFC 2397. Can be either a file path or a data URI.
Data URI encoded attachments must follow the RFC 2397.
Additionally a file name can be added: Additionally a file name can be added:
e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>` e.g.: `data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
@ -257,8 +261,7 @@ Specify the mentions of the original message (same format as `--mention`).
*--preview-url*:: *--preview-url*::
Specify the url for the link preview. Specify the url for the link preview.
The same url must also appear in the message body, otherwise the preview won't be The same url must also appear in the message body, otherwise the preview won't be displayed by the apps.
displayed by the apps.
*--preview-title*:: *--preview-title*::
Specify the title for the link preview (mandatory). Specify the title for the link preview (mandatory).

View file

@ -71,6 +71,9 @@ public class App {
parser.addArgument("--log-file") parser.addArgument("--log-file")
.type(File.class) .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."); .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") parser.addArgument("-c", "--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli)."); .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.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.logging.LogConfigurator;
import org.asamk.signal.manager.ManagerLogger; import org.asamk.signal.manager.ManagerLogger;
import org.asamk.signal.util.SecurityProvider; import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
@ -47,7 +48,8 @@ public class Main {
final var nsLog = parseArgs(args); final var nsLog = parseArgs(args);
final var verboseLevel = nsLog == null ? 0 : nsLog.getInt("verbose"); final var verboseLevel = nsLog == null ? 0 : nsLog.getInt("verbose");
final var logFile = nsLog == null ? null : nsLog.<File>get("log-file"); 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(); var parser = App.buildArgumentParser();
@ -82,6 +84,7 @@ public class Main {
.defaultHelp(false); .defaultHelp(false);
parser.addArgument("-v", "--verbose").action(Arguments.count()); parser.addArgument("-v", "--verbose").action(Arguments.count());
parser.addArgument("--log-file").type(File.class); parser.addArgument("--log-file").type(File.class);
parser.addArgument("--scrub-log").action(Arguments.storeTrue());
try { try {
return parser.parseKnownArgs(args, null); 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.setVerboseLevel(verboseLevel);
LogConfigurator.setLogFile(logFile); LogConfigurator.setLogFile(logFile);
LogConfigurator.setScrubSensitiveInformation(scrubLog);
if (verboseLevel > 0) { if (verboseLevel > 0) {
java.util.logging.Logger.getLogger("") java.util.logging.Logger.getLogger("")

View file

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