From 1d77153a2bc5c6c7a8d7bc7b72fe8fc7db1208c8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 4 Sep 2022 09:48:53 +0200 Subject: [PATCH] Add --scrub-log flag to remove possibly sensitive information from the log --- man/signal-cli.1.adoc | 9 +- src/main/java/org/asamk/signal/App.java | 3 + src/main/java/org/asamk/signal/Main.java | 8 +- .../signal/{ => logging}/LogConfigurator.java | 35 ++- .../org/asamk/signal/logging/Scrubber.java | 256 ++++++++++++++++++ .../signal/logging/ScrubberPatternLayout.java | 12 + .../ch.qos.logback.classic.spi.Configurator | 2 +- 7 files changed, 308 insertions(+), 17 deletions(-) rename src/main/java/org/asamk/signal/{ => logging}/LogConfigurator.java (80%) create mode 100644 src/main/java/org/asamk/signal/logging/Scrubber.java create mode 100644 src/main/java/org/asamk/signal/logging/ScrubberPatternLayout.java diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 025ee7ffc3..f1e9e79ad5 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -41,6 +41,9 @@ Raise log level and include lib signal logs. Write log output to the given 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:: Set the path, where to store the config. 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 ...]]:: 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: e.g.: `data:;filename=;base64,` @@ -257,8 +261,7 @@ Specify the mentions of the original message (same format as `--mention`). *--preview-url*:: Specify the url for the link preview. -The same url must also appear in the message body, otherwise the preview won't be -displayed by the apps. +The same url must also appear in the message body, otherwise the preview won't be displayed by the apps. *--preview-title*:: Specify the title for the link preview (mandatory). diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 4daaa815cb..4f045345b6 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -71,6 +71,9 @@ static ArgumentParser buildArgumentParser() { 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)."); diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 3da4fbcba6..429cdc9391 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -27,6 +27,7 @@ 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 static void main(String[] args) { final var nsLog = parseArgs(args); final var verboseLevel = nsLog == null ? 0 : nsLog.getInt("verbose"); final var logFile = nsLog == null ? null : nsLog.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 @@ private static Namespace parseArgs(String[] args) { .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 @@ private static Namespace parseArgs(String[] args) { } } - 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("") diff --git a/src/main/java/org/asamk/signal/LogConfigurator.java b/src/main/java/org/asamk/signal/logging/LogConfigurator.java similarity index 80% rename from src/main/java/org/asamk/signal/LogConfigurator.java rename to src/main/java/org/asamk/signal/logging/LogConfigurator.java index 714ec4a133..39b2315b23 100644 --- a/src/main/java/org/asamk/signal/LogConfigurator.java +++ b/src/main/java/org/asamk/signal/logging/LogConfigurator.java @@ -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 static void setLogFile(File logFile) { 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 @@ private LayoutWrappingEncoder createLayoutWrappingEncoder(final L } 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(); + } } } diff --git a/src/main/java/org/asamk/signal/logging/Scrubber.java b/src/main/java/org/asamk/signal/logging/Scrubber.java new file mode 100644 index 0000000000..efa196cede --- /dev/null +++ b/src/main/java/org/asamk/signal/logging/Scrubber.java @@ -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 . + */ + +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 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); + } +} diff --git a/src/main/java/org/asamk/signal/logging/ScrubberPatternLayout.java b/src/main/java/org/asamk/signal/logging/ScrubberPatternLayout.java new file mode 100644 index 0000000000..b15af5a263 --- /dev/null +++ b/src/main/java/org/asamk/signal/logging/ScrubberPatternLayout.java @@ -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(); + } +} diff --git a/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator b/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator index 354cf5bd29..6e92cd393f 100644 --- a/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator +++ b/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator @@ -1 +1 @@ -org.asamk.signal.LogConfigurator +org.asamk.signal.logging.LogConfigurator