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.
*-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:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
*--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).
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;
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();
.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);
}
}
- 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("")
-package org.asamk.signal;
+package org.asamk.signal.logging;
import java.io.File;
private static int verboseLevel = 0;
private static File logFile = null;
+ private static boolean scrubSensitiveInformation = false;
public static void setVerboseLevel(int verboseLevel) {
LogConfigurator.verboseLevel = verboseLevel;
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);
}
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();
+ }
}
}
--- /dev/null
+/*
+ * 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);
+ }
+}