diff --git a/CHANGELOG.md b/CHANGELOG.md index 4972cbea..1af850b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,26 @@ # Changelog ## [Unreleased] + +### Fixed +- Disable registration lock before removing the PIN +- Fix PIN hash version to match the official clients. + If you had previously set a PIN you need to set it again to be able to unlock the registration lock later. + +## [0.7.2] - 2020-12-31 +### Added +- Implement new registration lock PIN with `setPin` and `removePin` (with KBS) +- Include quotes, mentions and reactions in json output (Thanks @Atomic-Bean) + +### Fixed +- Retrieve avatars for v2 groups +- Download attachment thumbnail for quoted attachments + +## [0.7.1] - 2020-12-21 ### Added - Accept group invitation with `updateGroup -g GROUP_ID` - Decline group invitation with `quitGroup -g GROUP_ID` +- Join group via invitation link `joinGroup --uri https://signal.group/#...` ### Fixed - Include group ids for v2 groups in json output diff --git a/build.gradle b/build.gradle index 8b097612..ca346dbd 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_11 mainClassName = 'org.asamk.signal.Main' -version = '0.7.0' +version = '0.7.2' compileJava.options.encoding = 'UTF-8' @@ -18,10 +18,10 @@ repositories { dependencies { implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_15' - implementation 'org.bouncycastle:bcprov-jdk15on:1.67' + implementation 'org.bouncycastle:bcprov-jdk15on:1.68' implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' implementation 'com.github.hypfvieh:dbus-java:3.2.4' - implementation 'org.slf4j:slf4j-nop:1.7.30' + implementation 'org.slf4j:slf4j-simple:1.7.30' } jar { diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index e9b52593..fa2db7c3 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -124,6 +124,15 @@ Only works, if this is the master device. Specify the device you want to remove. Use listDevices to see the deviceIds. +=== getUserStatus + +Uses a list of phone numbers to determine the statuses of those users. Shows if they are registered on the Signal Servers or not. + +[NUMBER [NUMBER ...]]:: +One or more numbers to check. +*--json*:: +Output the statuses as an array of json objects. + === send Send a message to another user or group. @@ -178,6 +187,13 @@ Don’t download attachments of received messages. *--json*:: Output received messages in json format, one object per line. +=== joinGroup + +Join a group via an invitation link. + +*--uri*:: +The invitation link URI (starts with `https://signal.group/#`) + === updateGroup Create or update a group. diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 41b91a48..0cffd7b1 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -1,8 +1,8 @@ package org.asamk.signal; import org.asamk.Signal; -import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.groups.GroupUtils; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -65,7 +65,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { } else if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - byte[] groupId = getGroupId(m, message); + byte[] groupId = getGroupId(message); if (!message.isEndSession() && ( groupId == null || message.getGroupContext().get().getGroupV1Type() == null @@ -91,7 +91,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { .getGroupContext() .isPresent()) { SignalServiceDataMessage message = transcript.getMessage(); - byte[] groupId = getGroupId(m, message); + byte[] groupId = getGroupId(message); try { conn.sendMessage(new Signal.SyncMessageReceived(objectPath, @@ -112,20 +112,9 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { } } - private static byte[] getGroupId(final Manager m, final SignalServiceDataMessage message) { - byte[] groupId; - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - groupId = message.getGroupContext().get().getGroupV1().get().getGroupId(); - } else if (message.getGroupContext().get().getGroupV2().isPresent()) { - groupId = GroupUtils.getGroupId(message.getGroupContext().get().getGroupV2().get().getMasterKey()); - } else { - groupId = null; - } - } else { - groupId = null; - } - return groupId; + private static byte[] getGroupId(final SignalServiceDataMessage message) { + return message.getGroupContext().isPresent() ? GroupUtils.getGroupId(message.getGroupContext().get()) + .serialize() : null; } static private List getAttachments(SignalServiceDataMessage message, Manager m) { diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index dfe51fe7..eb135e13 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -3,7 +3,6 @@ package org.asamk.signal; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -23,8 +22,7 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler public JsonReceiveMessageHandler(Manager m) { this.m = m; this.jsonProcessor = new ObjectMapper(); - jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect - jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } @@ -35,7 +33,7 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler result.putPOJO("error", new JsonError(exception)); } if (envelope != null) { - result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content)); + result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content, m)); } try { jsonProcessor.writeValue(System.out, result); diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 6b0b3811..5b0ff682 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2020 AsamK and contributors + Copyright (C) 2015-2021 AsamK and contributors 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 @@ -32,16 +32,20 @@ import org.asamk.signal.commands.DbusCommand; import org.asamk.signal.commands.ExtendedDbusCommand; import org.asamk.signal.commands.LocalCommand; import org.asamk.signal.commands.ProvisioningCommand; +import org.asamk.signal.commands.RegistrationCommand; import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotRegisteredException; import org.asamk.signal.manager.ProvisioningManager; +import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.ServiceConfig; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.SecurityProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; -import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; @@ -50,10 +54,10 @@ import java.io.IOException; import java.security.Security; import java.util.Map; -import static org.whispersystems.signalservice.internal.util.Util.isEmpty; - public class Main { + final static Logger logger = LoggerFactory.getLogger(Main.class); + public static void main(String[] args) { installSecurityProviderWorkaround(); @@ -62,7 +66,7 @@ public class Main { System.exit(1); } - int res = handleCommands(ns); + int res = init(ns); System.exit(res); } @@ -72,125 +76,148 @@ public class Main { Security.addProvider(new BouncyCastleProvider()); } - private static int handleCommands(Namespace ns) { - final String username = ns.getString("username"); + public static int init(Namespace ns) { + Command command = getCommand(ns); + if (command == null) { + logger.error("Command not implemented!"); + return 3; + } if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { - try { - DBusConnection.DBusBusType busType; - if (ns.getBoolean("dbus_system")) { - busType = DBusConnection.DBusBusType.SYSTEM; - } else { - busType = DBusConnection.DBusBusType.SESSION; - } - try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) { - Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME, - DbusConfig.SIGNAL_OBJECTPATH, - Signal.class); + return initDbusClient(command, ns, ns.getBoolean("dbus_system")); + } - return handleCommands(ns, ts, dBusConn); - } - } catch (UnsatisfiedLinkError e) { - System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); - return 1; - } catch (DBusException | IOException e) { - e.printStackTrace(); + final String username = ns.getString("username"); + + final File dataPath; + String config = ns.getString("config"); + if (config != null) { + dataPath = new File(config); + } else { + dataPath = getDefaultDataPath(); + } + + final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration( + BaseConfig.USER_AGENT); + + if (!ServiceConfig.getCapabilities().isGv2()) { + logger.warn("WARNING: Support for new group V2 is disabled," + + " because the required native library dependency is missing: libzkgroup"); + } + + if (username == null) { + ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT); + return handleCommand(command, ns, pm); + } + + if (command instanceof RegistrationCommand) { + final RegistrationManager manager; + try { + manager = RegistrationManager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT); + } catch (Throwable e) { + logger.error("Error loading or creating state file: {}", e.getMessage()); + return 2; + } + try (RegistrationManager m = manager) { + return handleCommand(command, ns, m); + } catch (Exception e) { + logger.error("Cleanup failed", e); return 3; } - } else { - String dataPath = ns.getString("config"); - if (isEmpty(dataPath)) { - dataPath = getDefaultDataPath(); - } + } - final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration( - BaseConfig.USER_AGENT); + Manager manager; + try { + manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT); + } catch (NotRegisteredException e) { + System.err.println("User is not registered."); + return 1; + } catch (Throwable e) { + logger.error("Error loading state file: {}", e.getMessage()); + return 2; + } - if (username == null) { - ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT); - return handleCommands(ns, pm); - } - - Manager manager; + try (Manager m = manager) { try { - manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT); - } catch (Throwable e) { - System.err.println("Error loading state file: " + e.getMessage()); + m.checkAccountState(); + } catch (IOException e) { + logger.error("Error while checking account: {}", e.getMessage()); return 2; } - try (Manager m = manager) { - try { - m.checkAccountState(); - } catch (AuthorizationFailedException e) { - if (!"register".equals(ns.getString("command"))) { - // Register command should still be possible, if current authorization fails - System.err.println("Authorization failed, was the number registered elsewhere?"); - return 2; - } - } catch (IOException e) { - System.err.println("Error while checking account: " + e.getMessage()); - return 2; - } - - return handleCommands(ns, m); - } catch (IOException e) { - e.printStackTrace(); - return 3; - } + return handleCommand(command, ns, m); + } catch (IOException e) { + logger.error("Cleanup failed", e); + return 3; } } - private static int handleCommands(Namespace ns, Signal ts, DBusConnection dBusConn) { - String commandKey = ns.getString("command"); - final Map commands = Commands.getCommands(); - if (commands.containsKey(commandKey)) { - Command command = commands.get(commandKey); - - if (command instanceof ExtendedDbusCommand) { - return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn); - } else if (command instanceof DbusCommand) { - return ((DbusCommand) command).handleCommand(ns, ts); + private static int initDbusClient(final Command command, final Namespace ns, final boolean systemBus) { + try { + DBusConnection.DBusBusType busType; + if (systemBus) { + busType = DBusConnection.DBusBusType.SYSTEM; } else { - System.err.println(commandKey + " is not yet implemented via dbus"); - return 1; + busType = DBusConnection.DBusBusType.SESSION; } + try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) { + Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME, + DbusConfig.SIGNAL_OBJECTPATH, + Signal.class); + + return handleCommand(command, ns, ts, dBusConn); + } + } catch (DBusException | IOException e) { + logger.error("Dbus client failed", e); + return 3; } - return 0; } - private static int handleCommands(Namespace ns, ProvisioningManager pm) { + private static Command getCommand(Namespace ns) { String commandKey = ns.getString("command"); final Map commands = Commands.getCommands(); - if (commands.containsKey(commandKey)) { - Command command = commands.get(commandKey); - - if (command instanceof ProvisioningCommand) { - return ((ProvisioningCommand) command).handleCommand(ns, pm); - } else { - System.err.println(commandKey + " only works with a username"); - return 1; - } + if (!commands.containsKey(commandKey)) { + return null; } - return 0; + return commands.get(commandKey); } - private static int handleCommands(Namespace ns, Manager m) { - String commandKey = ns.getString("command"); - final Map commands = Commands.getCommands(); - if (commands.containsKey(commandKey)) { - Command command = commands.get(commandKey); - - if (command instanceof LocalCommand) { - return ((LocalCommand) command).handleCommand(ns, m); - } else if (command instanceof DbusCommand) { - return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m)); - } else if (command instanceof ExtendedDbusCommand) { - System.err.println(commandKey + " only works via dbus"); - } + private static int handleCommand(Command command, Namespace ns, Signal ts, DBusConnection dBusConn) { + if (command instanceof ExtendedDbusCommand) { + return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn); + } else if (command instanceof DbusCommand) { + return ((DbusCommand) command).handleCommand(ns, ts); + } else { + System.err.println("Command is not yet implemented via dbus"); + return 1; + } + } + + private static int handleCommand(Command command, Namespace ns, ProvisioningManager pm) { + if (command instanceof ProvisioningCommand) { + return ((ProvisioningCommand) command).handleCommand(ns, pm); + } else { + System.err.println("Command only works with a username"); + return 1; + } + } + + private static int handleCommand(Command command, Namespace ns, RegistrationManager m) { + if (command instanceof RegistrationCommand) { + return ((RegistrationCommand) command).handleCommand(ns, m); + } + return 1; + } + + private static int handleCommand(Command command, Namespace ns, Manager m) { + if (command instanceof LocalCommand) { + return ((LocalCommand) command).handleCommand(ns, m); + } else if (command instanceof DbusCommand) { + return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m)); + } else { + System.err.println("Command only works via dbus"); return 1; } - return 0; } /** @@ -200,19 +227,21 @@ public class Main { * * @return the data directory to be used by signal-cli. */ - private static String getDefaultDataPath() { - String dataPath = IOUtils.getDataHomeDir() + "/signal-cli"; - if (new File(dataPath).exists()) { + private static File getDefaultDataPath() { + File dataPath = new File(IOUtils.getDataHomeDir(), "signal-cli"); + if (dataPath.exists()) { return dataPath; } - String legacySettingsPath = System.getProperty("user.home") + "/.config/signal"; - if (new File(legacySettingsPath).exists()) { + File configPath = new File(System.getProperty("user.home"), ".config"); + + File legacySettingsPath = new File(configPath, "signal"); + if (legacySettingsPath.exists()) { return legacySettingsPath; } - legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure"; - if (new File(legacySettingsPath).exists()) { + legacySettingsPath = new File(configPath, "textsecure"); + if (legacySettingsPath.exists()) { return legacySettingsPath; } @@ -220,34 +249,7 @@ public class Main { } private static Namespace parseArgs(String[] args) { - ArgumentParser parser = ArgumentParsers.newFor("signal-cli") - .build() - .defaultHelp(true) - .description("Commandline interface for Signal.") - .version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION); - - parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version()); - parser.addArgument("--config") - .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli)."); - parser.addArgument("-n", "--busname") - .help("Name of the DBus."); - - MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup(); - mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification."); - mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue()); - mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue()); - - Subparsers subparsers = parser.addSubparsers() - .title("subcommands") - .dest("command") - .description("valid subcommands") - .help("additional help"); - - final Map commands = Commands.getCommands(); - for (Map.Entry entry : commands.entrySet()) { - Subparser subparser = subparsers.addParser(entry.getKey()); - entry.getValue().attachToSubparser(subparser); - } + ArgumentParser parser = buildArgumentParser(); Namespace ns; try { @@ -280,4 +282,36 @@ public class Main { } return ns; } + + private static ArgumentParser buildArgumentParser() { + ArgumentParser parser = ArgumentParsers.newFor("signal-cli") + .build() + .defaultHelp(true) + .description("Commandline interface for Signal.") + .version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION); + + parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version()); + parser.addArgument("--config") + .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli)."); + parser.addArgument("-n", "--busname") + .help("Name of the DBus."); + + MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup(); + mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification."); + mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue()); + mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue()); + + Subparsers subparsers = parser.addSubparsers() + .title("subcommands") + .dest("command") + .description("valid subcommands") + .help("additional help"); + + final Map commands = Commands.getCommands(); + for (Map.Entry entry : commands.entrySet()) { + Subparser subparser = subparsers.addParser(entry.getKey()); + entry.getValue().attachToSubparser(subparser); + } + return parser; + } } diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 5925b2b8..db78f454 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -1,9 +1,10 @@ package org.asamk.signal; -import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.manager.Manager; -import org.asamk.signal.storage.contacts.ContactInfo; -import org.asamk.signal.storage.groups.GroupInfo; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.storage.contacts.ContactInfo; +import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -328,8 +329,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp())); if (typingMessage.getGroupId().isPresent()) { System.out.println(" - Group Info:"); - System.out.println(" Id: " + Base64.encodeBytes(typingMessage.getGroupId().get())); - GroupInfo group = m.getGroup(typingMessage.getGroupId().get()); + final GroupId groupId = GroupId.unknownVersion(typingMessage.getGroupId().get()); + System.out.println(" Id: " + groupId.toBase64()); + GroupInfo group = m.getGroup(groupId); if (group != null) { System.out.println(" Name: " + group.getTitle()); } else { @@ -356,13 +358,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (message.getGroupContext().isPresent()) { System.out.println("Group info:"); final SignalServiceGroupContext groupContext = message.getGroupContext().get(); + final GroupId groupId = GroupUtils.getGroupId(groupContext); if (groupContext.getGroupV1().isPresent()) { SignalServiceGroup groupInfo = groupContext.getGroupV1().get(); - System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); + System.out.println(" Id: " + groupId.toBase64()); if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) { System.out.println(" Name: " + groupInfo.getName().get()); } else { - GroupInfo group = m.getGroup(groupInfo.getGroupId()); + GroupInfo group = m.getGroup(groupId); if (group != null) { System.out.println(" Name: " + group.getTitle()); } else { @@ -381,8 +384,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } } else if (groupContext.getGroupV2().isPresent()) { final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get(); - byte[] groupId = GroupUtils.getGroupId(groupInfo.getMasterKey()); - System.out.println(" Id: " + Base64.encodeBytes(groupId)); + System.out.println(" Id: " + groupId.toBase64()); GroupInfo group = m.getGroup(groupId); if (group != null) { System.out.println(" Name: " + group.getTitle()); @@ -447,14 +449,20 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (message.getQuote().isPresent()) { SignalServiceDataMessage.Quote quote = message.getQuote().get(); System.out.println("Quote: (" + quote.getId() + ")"); - System.out.println(" Author: " + quote.getAuthor().getLegacyIdentifier()); + System.out.println(" Author: " + m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier()); System.out.println(" Text: " + quote.getText()); + if (quote.getMentions() != null && quote.getMentions().size() > 0) { + System.out.println(" Mentions: "); + for (SignalServiceDataMessage.Mention mention : quote.getMentions()) { + printMention(mention, m); + } + } if (quote.getAttachments().size() > 0) { System.out.println(" Attachments: "); for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : quote.getAttachments()) { - System.out.println(" Filename: " + attachment.getFileName()); - System.out.println(" Type: " + attachment.getContentType()); - System.out.println(" Thumbnail:"); + System.out.println(" - Filename: " + attachment.getFileName()); + System.out.println(" Type: " + attachment.getContentType()); + System.out.println(" Thumbnail:"); if (attachment.getThumbnail() != null) { printAttachment(attachment.getThumbnail()); } @@ -467,16 +475,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Remote delete message: timestamp = " + remoteDelete.getTargetSentTimestamp()); } if (message.getMentions().isPresent()) { - final List mentions = message.getMentions().get(); System.out.println("Mentions: "); - for (SignalServiceDataMessage.Mention mention : mentions) { - System.out.println("- " - + mention.getUuid() - + ": " - + mention.getStart() - + " (length: " - + mention.getLength() - + ")"); + for (SignalServiceDataMessage.Mention mention : message.getMentions().get()) { + printMention(mention, m); } } @@ -488,6 +489,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } } + private void printMention(SignalServiceDataMessage.Mention mention, Manager m) { + System.out.println("- " + m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null)) + .getLegacyIdentifier() + ": " + mention.getStart() + " (length: " + mention.getLength() + ")"); + } + private void printAttachment(SignalServiceAttachment attachment) { System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + ( attachment.isStream() ? "Stream" : "" diff --git a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java index dab886d7..c5d18ab1 100644 --- a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java @@ -23,10 +23,6 @@ public class AddDeviceCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { m.addDeviceLink(new URI(ns.getString("uri"))); return 0; diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 95c3738c..60009cfb 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -3,9 +3,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -20,11 +21,6 @@ public class BlockCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - for (String contact_number : ns.getList("contact")) { try { m.setContactBlocked(contact_number, true); @@ -36,7 +32,7 @@ public class BlockCommand implements LocalCommand { if (ns.getList("group") != null) { for (String groupIdString : ns.getList("group")) { try { - byte[] groupId = Util.decodeGroupId(groupIdString); + GroupId groupId = Util.decodeGroupId(groupIdString); m.setGroupBlocked(groupId, true); } catch (GroupIdFormatException | GroupNotFoundException e) { System.err.println(e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 183b40a0..1e081dff 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -11,11 +11,13 @@ public class Commands { addCommand("addDevice", new AddDeviceCommand()); addCommand("block", new BlockCommand()); addCommand("daemon", new DaemonCommand()); + addCommand("getUserStatus", new GetUserStatusCommand()); addCommand("link", new LinkCommand()); addCommand("listContacts", new ListContactsCommand()); addCommand("listDevices", new ListDevicesCommand()); addCommand("listGroups", new ListGroupsCommand()); addCommand("listIdentities", new ListIdentitiesCommand()); + addCommand("joinGroup", new JoinGroupCommand()); addCommand("quitGroup", new QuitGroupCommand()); addCommand("receive", new ReceiveCommand()); addCommand("register", new RegisterCommand()); diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 138bbe0d..f6e13450 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -35,10 +35,6 @@ public class DaemonCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } DBusConnection conn = null; try { try { diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java new file mode 100644 index 00000000..0a1ddc4c --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -0,0 +1,77 @@ +package org.asamk.signal.commands; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.manager.Manager; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class GetUserStatusCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("number").help("Phone number").nargs("+"); + subparser.help("Check if the specified phone number/s have been registered"); + subparser.addArgument("--json") + .help("Output received messages in json format, one json object per line.") + .action(Arguments.storeTrue()); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + // Setup the json object mapper + ObjectMapper jsonProcessor = new ObjectMapper(); + jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + + // Get a map of registration statuses + Map registered; + try { + registered = m.areUsersRegistered(new HashSet<>(ns.getList("number"))); + } catch (IOException e) { + System.err.println("Unable to check if users are registered"); + return 1; + } + + // Output + if (ns.getBoolean("json")) { + List objects = registered.entrySet() + .stream() + .map(entry -> new JsonIsRegistered(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + + try { + jsonProcessor.writeValue(System.out, objects); + System.out.println(); + } catch (IOException e) { + System.err.println(e.getMessage()); + } + } else { + for (Map.Entry entry : registered.entrySet()) { + System.out.println(entry.getKey() + ": " + entry.getValue()); + } + } + + return 0; + } + + private static final class JsonIsRegistered { + + public String name; + + public boolean isRegistered; + + public JsonIsRegistered(String name, boolean isRegistered) { + this.name = name; + this.isRegistered = isRegistered; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java new file mode 100644 index 00000000..c5975b0c --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -0,0 +1,79 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.Signal; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException; + +import java.io.IOException; +import java.util.List; + +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; +import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults; + +public class JoinGroupCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("--uri").required(true).help("Specify the uri with the group invitation link."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + final GroupInviteLinkUrl linkUrl; + String uri = ns.getString("uri"); + try { + linkUrl = GroupInviteLinkUrl.fromUri(uri); + } catch (GroupInviteLinkUrl.InvalidGroupLinkException e) { + System.err.println("Group link is invalid: " + e.getMessage()); + return 2; + } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) { + System.err.println("Group link was created with an incompatible version: " + e.getMessage()); + return 2; + } + + if (linkUrl == null) { + System.err.println("Link is not a signal group invitation link"); + return 2; + } + + try { + final Pair> results = m.joinGroup(linkUrl); + GroupId newGroupId = results.first(); + if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) { + System.out.println("Requested to join group \"" + newGroupId.toBase64() + "\""); + } else { + System.out.println("Joined group \"" + newGroupId.toBase64() + "\""); + } + return handleTimestampAndSendMessageResults(0, results.second()); + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } catch (GroupPatchNotAcceptedException e) { + System.err.println("Failed to join group, maybe already a member"); + return 1; + } catch (IOException e) { + e.printStackTrace(); + handleIOException(e); + return 1; + } catch (Signal.Error.AttachmentInvalid e) { + System.err.println("Failed to add avatar attachment for group\": " + e.getMessage()); + return 1; + } catch (DBusExecutionException e) { + System.err.println("Failed to send message: " + e.getMessage()); + return 1; + } catch (GroupLinkNotActiveException e) { + System.err.println("Group link is not valid: " + e.getMessage()); + return 2; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index 24d6898c..1a14e8df 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; -import org.asamk.signal.storage.contacts.ContactInfo; +import org.asamk.signal.manager.storage.contacts.ContactInfo; import java.util.List; @@ -16,10 +16,6 @@ public class ListContactsCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } List contacts = m.getContacts(); for (ContactInfo c : contacts) { System.out.println(String.format("Number: %s Name: %s Blocked: %b", c.number, c.name, c.blocked)); diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java index 4b9dac5c..a03b078f 100644 --- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java @@ -18,10 +18,6 @@ public class ListDevicesCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { List devices = m.getLinkedDevices(); for (DeviceInfo d : devices) { diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index b9f54a6b..97af502e 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -5,9 +5,9 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; -import org.asamk.signal.storage.groups.GroupInfo; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.storage.groups.GroupInfo; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.util.Base64; import java.util.List; import java.util.Set; @@ -35,18 +35,21 @@ public class ListGroupsCommand implements LocalCommand { .map(SignalServiceAddress::getLegacyIdentifier) .collect(Collectors.toSet()); + final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink(); + System.out.println(String.format( - "Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s", - Base64.encodeBytes(group.groupId), + "Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s", + group.getGroupId().toBase64(), group.getTitle(), group.isMember(m.getSelfAddress()), group.isBlocked(), members, pendingMembers, - requestingMembers)); + requestingMembers, + groupInviteLink == null ? '-' : groupInviteLink.getUrl())); } else { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", - Base64.encodeBytes(group.groupId), + group.getGroupId().toBase64(), group.getTitle(), group.isMember(m.getSelfAddress()), group.isBlocked())); @@ -61,11 +64,6 @@ public class ListGroupsCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - List groups = m.getGroups(); boolean detailed = ns.getBoolean("detailed"); diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index a75e4328..4caeca29 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; -import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; +import org.asamk.signal.manager.storage.protocol.IdentityInfo; import org.asamk.signal.util.Hex; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -13,7 +13,7 @@ import java.util.List; public class ListIdentitiesCommand implements LocalCommand { - private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) { + private static void printIdentityFingerprint(Manager m, IdentityInfo theirId) { String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey())); System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirId.getAddress().getNumber().orNull(), @@ -30,19 +30,15 @@ public class ListIdentitiesCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } if (ns.get("number") == null) { - for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) { + for (IdentityInfo identity : m.getIdentities()) { printIdentityFingerprint(m, identity); } } else { String number = ns.getString("number"); try { - List identities = m.getIdentities(number); - for (JsonIdentityKeyStore.Identity id : identities) { + List identities = m.getIdentities(number); + for (IdentityInfo id : identities) { printIdentityFingerprint(m, id); } } catch (InvalidNumberException e) { diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 20d06eba..f258ae24 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -3,10 +3,11 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.NotAGroupMemberException; -import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -30,13 +31,8 @@ public class QuitGroupCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - try { - final byte[] groupId = Util.decodeGroupId(ns.getString("group")); + final GroupId groupId = Util.decodeGroupId(ns.getString("group")); final Pair> results = m.sendQuitGroupMessage(groupId); return handleTimestampAndSendMessageResults(results.first(), results.second()); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index bc68565a..7dc9dcaf 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -44,7 +44,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { final ObjectMapper jsonProcessor; if (ns.getBoolean("json")) { jsonProcessor = new ObjectMapper(); - jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } else { jsonProcessor = null; @@ -146,10 +146,6 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } double timeout = 5; if (ns.getDouble("timeout") != null) { timeout = ns.getDouble("timeout"); diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index f69e0844..da652f7d 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -4,12 +4,12 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.RegistrationManager; import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; import java.io.IOException; -public class RegisterCommand implements LocalCommand { +public class RegisterCommand implements RegistrationCommand { @Override public void attachToSubparser(final Subparser subparser) { @@ -21,7 +21,7 @@ public class RegisterCommand implements LocalCommand { } @Override - public int handleCommand(final Namespace ns, final Manager m) { + public int handleCommand(final Namespace ns, final RegistrationManager m) { try { final boolean voiceVerification = ns.getBoolean("voice"); final String captcha = ns.getString("captcha"); diff --git a/src/main/java/org/asamk/signal/commands/RegistrationCommand.java b/src/main/java/org/asamk/signal/commands/RegistrationCommand.java new file mode 100644 index 00000000..8683570f --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/RegistrationCommand.java @@ -0,0 +1,10 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; + +import org.asamk.signal.manager.RegistrationManager; + +public interface RegistrationCommand extends Command { + + int handleCommand(Namespace ns, RegistrationManager m); +} diff --git a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java index 1e2343e7..78d14bbd 100644 --- a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java @@ -19,10 +19,6 @@ public class RemoveDeviceCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { int deviceId = ns.getInt("deviceId"); m.removeLinkedDevices(deviceId); diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java index b7de5402..ada9c446 100644 --- a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java @@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import java.io.IOException; @@ -16,14 +17,10 @@ public class RemovePinCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { m.setRegistrationLockPin(Optional.absent()); return 0; - } catch (IOException e) { + } catch (IOException | UnauthenticatedResponseException e) { System.err.println("Remove pin error: " + e.getMessage()); return 3; } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 551cf938..ee51dd91 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -5,7 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; @@ -79,7 +79,7 @@ public class SendCommand implements DbusCommand { if (ns.getString("group") != null) { byte[] groupId; try { - groupId = Util.decodeGroupId(ns.getString("group")); + groupId = Util.decodeGroupId(ns.getString("group")).serialize(); } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; diff --git a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java index 20e81a60..aaca283a 100644 --- a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java @@ -17,10 +17,6 @@ public class SendContactsCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { m.sendContacts(); return 0; diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 6e5f24bb..2a9afa74 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -4,10 +4,11 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.NotAGroupMemberException; -import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -46,11 +47,6 @@ public class SendReactionCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && ns.getString("group") == null) { System.err.println("No recipients given"); System.err.println("Aborting sending."); @@ -65,7 +61,7 @@ public class SendReactionCommand implements LocalCommand { try { final Pair> results; if (ns.getString("group") != null) { - byte[] groupId = Util.decodeGroupId(ns.getString("group")); + GroupId groupId = Util.decodeGroupId(ns.getString("group")); results = m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId); } else { results = m.sendMessageReaction(emoji, @@ -74,8 +70,7 @@ public class SendReactionCommand implements LocalCommand { targetTimestamp, ns.getList("recipient")); } - handleTimestampAndSendMessageResults(results.first(), results.second()); - return 0; + return handleTimestampAndSendMessageResults(results.first(), results.second()); } catch (IOException e) { handleIOException(e); return 3; diff --git a/src/main/java/org/asamk/signal/commands/SetPinCommand.java b/src/main/java/org/asamk/signal/commands/SetPinCommand.java index 9351dad0..ac601b3b 100644 --- a/src/main/java/org/asamk/signal/commands/SetPinCommand.java +++ b/src/main/java/org/asamk/signal/commands/SetPinCommand.java @@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import java.io.IOException; @@ -18,15 +19,11 @@ public class SetPinCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { String registrationLockPin = ns.getString("registrationLockPin"); m.setRegistrationLockPin(Optional.of(registrationLockPin)); return 0; - } catch (IOException e) { + } catch (IOException | UnauthenticatedResponseException e) { System.err.println("Set pin error: " + e.getMessage()); return 3; } diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 076a86db..58c7371f 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -27,10 +27,6 @@ public class TrustCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } String number = ns.getString("number"); if (ns.getBoolean("trust_all_known_keys")) { boolean res = m.trustIdentityAllKeys(number); diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index b4f6cc3b..d191ef22 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -3,9 +3,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -20,11 +21,6 @@ public class UnblockCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - for (String contact_number : ns.getList("contact")) { try { m.setContactBlocked(contact_number, false); @@ -36,7 +32,7 @@ public class UnblockCommand implements LocalCommand { if (ns.getList("group") != null) { for (String groupIdString : ns.getList("group")) { try { - byte[] groupId = Util.decodeGroupId(groupIdString); + GroupId groupId = Util.decodeGroupId(groupIdString); m.setGroupBlocked(groupId, false); } catch (GroupIdFormatException | GroupNotFoundException e) { System.err.println(e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java index 7a7616bd..079070e6 100644 --- a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java @@ -16,10 +16,6 @@ public class UnregisterCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { m.unregister(); return 0; diff --git a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java index 79459fe6..8211e190 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java @@ -16,10 +16,6 @@ public class UpdateAccountCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { m.updateAccountAttributes(); return 0; diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index da090209..c4da94a2 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -23,11 +23,6 @@ public class UpdateContactCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - String number = ns.getString("number"); String name = ns.getString("name"); diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 4216fd9b..a6f40ef2 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.util.Base64; @@ -35,7 +35,7 @@ public class UpdateGroupCommand implements DbusCommand { byte[] groupId = null; if (ns.getString("group") != null) { try { - groupId = Util.decodeGroupId(ns.getString("group")); + groupId = Util.decodeGroupId(ns.getString("group")).serialize(); } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index 1e332fb4..968a8733 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -25,11 +25,6 @@ public class UpdateProfileCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - String name = ns.getString("name"); String avatarPath = ns.getString("avatar"); boolean removeAvatar = ns.getBoolean("remove_avatar"); diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java index 77df2b22..f9f5d95b 100644 --- a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -6,6 +6,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.StickerPackInvalidException; +import java.io.File; import java.io.IOException; public class UploadStickerPackCommand implements LocalCommand { @@ -19,7 +20,7 @@ public class UploadStickerPackCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { try { - String path = ns.getString("path"); + File path = new File(ns.getString("path")); String url = m.uploadStickerPack(path); System.out.println(url); return 0; diff --git a/src/main/java/org/asamk/signal/commands/VerifyCommand.java b/src/main/java/org/asamk/signal/commands/VerifyCommand.java index b6ad100b..7fa10b6a 100644 --- a/src/main/java/org/asamk/signal/commands/VerifyCommand.java +++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java @@ -3,12 +3,14 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.RegistrationManager; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; import org.whispersystems.signalservice.internal.push.LockedException; import java.io.IOException; -public class VerifyCommand implements LocalCommand { +public class VerifyCommand implements RegistrationCommand { @Override public void attachToSubparser(final Subparser subparser) { @@ -17,11 +19,7 @@ public class VerifyCommand implements LocalCommand { } @Override - public int handleCommand(final Namespace ns, final Manager m) { - if (m.isRegistered()) { - System.err.println("User registration is already verified"); - return 1; - } + public int handleCommand(final Namespace ns, final RegistrationManager m) { try { String verificationCode = ns.getString("verificationCode"); String pin = ns.getString("pin"); @@ -31,6 +29,12 @@ public class VerifyCommand implements LocalCommand { System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: " + (e.getTimeRemaining() / 1000 / 60 / 60)); System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN"); + return 1; + } catch (KeyBackupServicePinException e) { + System.err.println("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining()); + return 1; + } catch (KeyBackupSystemNoDataException e) { + System.err.println("Verification failed! No KBS data."); return 3; } catch (IOException e) { System.err.println("Verify error: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 396063f2..278fbbd4 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -2,10 +2,11 @@ package org.asamk.signal.dbus; import org.asamk.Signal; import org.asamk.signal.manager.AttachmentInvalidException; -import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.NotAGroupMemberException; -import org.asamk.signal.storage.groups.GroupInfo; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.util.ErrorUtils; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.util.Pair; @@ -15,7 +16,6 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -92,7 +92,9 @@ public class DbusSignalImpl implements Signal { @Override public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { try { - Pair> results = m.sendGroupMessage(message, attachments, groupId); + Pair> results = m.sendGroupMessage(message, + attachments, + GroupId.unknownVersion(groupId)); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (IOException e) { @@ -134,7 +136,7 @@ public class DbusSignalImpl implements Signal { @Override public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { - m.setGroupBlocked(groupId, blocked); + m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } @@ -145,14 +147,14 @@ public class DbusSignalImpl implements Signal { List groups = m.getGroups(); List ids = new ArrayList<>(groups.size()); for (GroupInfo group : groups) { - ids.add(group.groupId); + ids.add(group.getGroupId().serialize()); } return ids; } @Override public String getGroupName(final byte[] groupId) { - GroupInfo group = m.getGroup(groupId); + GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { return ""; } else { @@ -162,9 +164,9 @@ public class DbusSignalImpl implements Signal { @Override public List getGroupMembers(final byte[] groupId) { - GroupInfo group = m.getGroup(groupId); + GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { - return Collections.emptyList(); + return List.of(); } else { return group.getMembers() .stream() @@ -189,9 +191,11 @@ public class DbusSignalImpl implements Signal { if (avatar.isEmpty()) { avatar = null; } - final Pair> results = m.updateGroup(groupId, name, members, avatar); + final Pair> results = m.updateGroup(groupId == null + ? null + : GroupId.unknownVersion(groupId), name, members, avatar); checkSendMessageResults(0, results.second()); - return results.first(); + return results.first().serialize(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { diff --git a/src/main/java/org/asamk/signal/json/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java index 653a59e6..57201eda 100644 --- a/src/main/java/org/asamk/signal/json/JsonDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java @@ -1,12 +1,11 @@ package org.asamk.signal.json; import org.asamk.Signal; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -15,10 +14,14 @@ class JsonDataMessage { long timestamp; String message; int expiresInSeconds; + + JsonReaction reaction; + JsonQuote quote; + List mentions; List attachments; JsonGroupInfo groupInfo; - JsonDataMessage(SignalServiceDataMessage dataMessage) { + JsonDataMessage(SignalServiceDataMessage dataMessage, Manager m) { this.timestamp = dataMessage.getTimestamp(); if (dataMessage.getGroupContext().isPresent()) { if (dataMessage.getGroupContext().get().getGroupV1().isPresent()) { @@ -33,13 +36,29 @@ class JsonDataMessage { this.message = dataMessage.getBody().get(); } this.expiresInSeconds = dataMessage.getExpiresInSeconds(); - if (dataMessage.getAttachments().isPresent()) { - this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size()); - for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) { - this.attachments.add(new JsonAttachment(attachment)); - } + if (dataMessage.getReaction().isPresent()) { + this.reaction = new JsonReaction(dataMessage.getReaction().get(), m); + } + if (dataMessage.getQuote().isPresent()) { + this.quote = new JsonQuote(dataMessage.getQuote().get(), m); + } + if (dataMessage.getMentions().isPresent()) { + this.mentions = dataMessage.getMentions() + .get() + .stream() + .map(mention -> new JsonMention(mention, m)) + .collect(Collectors.toList()); } else { - this.attachments = new ArrayList<>(); + this.mentions = List.of(); + } + if (dataMessage.getAttachments().isPresent()) { + this.attachments = dataMessage.getAttachments() + .get() + .stream() + .map(JsonAttachment::new) + .collect(Collectors.toList()); + } else { + this.attachments = List.of(); } } @@ -47,6 +66,9 @@ class JsonDataMessage { timestamp = messageReceived.getTimestamp(); message = messageReceived.getMessage(); groupInfo = new JsonGroupInfo(messageReceived.getGroupId()); + reaction = null; // TODO Replace these 3 with the proper commands + quote = null; + mentions = null; attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList()); } @@ -54,6 +76,9 @@ class JsonDataMessage { timestamp = messageReceived.getTimestamp(); message = messageReceived.getMessage(); groupInfo = new JsonGroupInfo(messageReceived.getGroupId()); + reaction = null; // TODO Replace these 3 with the proper commands + quote = null; + mentions = null; attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList()); } } diff --git a/src/main/java/org/asamk/signal/json/JsonGroupInfo.java b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java index 970cde52..79967955 100644 --- a/src/main/java/org/asamk/signal/json/JsonGroupInfo.java +++ b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java @@ -1,6 +1,6 @@ package org.asamk.signal.json; -import org.asamk.signal.manager.GroupUtils; +import org.asamk.signal.manager.groups.GroupUtils; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -31,7 +31,7 @@ class JsonGroupInfo { } JsonGroupInfo(SignalServiceGroupV2 groupInfo) { - this.groupId = Base64.encodeBytes(GroupUtils.getGroupId(groupInfo.getMasterKey())); + this.groupId = GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64(); this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER"; } diff --git a/src/main/java/org/asamk/signal/json/JsonMention.java b/src/main/java/org/asamk/signal/json/JsonMention.java new file mode 100644 index 00000000..302128ed --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonMention.java @@ -0,0 +1,19 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public class JsonMention { + + String name; + int start; + int length; + + JsonMention(SignalServiceDataMessage.Mention mention, Manager m) { + this.name = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null)) + .getLegacyIdentifier(); + this.start = mention.getStart(); + this.length = mention.getLength(); + } +} diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index 5e5e6a33..787f62e2 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -1,6 +1,7 @@ package org.asamk.signal.json; import org.asamk.Signal; +import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -18,7 +19,7 @@ public class JsonMessageEnvelope { JsonCallMessage callMessage; JsonReceiptMessage receiptMessage; - public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content) { + public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content, Manager m) { if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { SignalServiceAddress source = envelope.getSourceAddress(); this.source = source.getLegacyIdentifier(); @@ -35,10 +36,10 @@ public class JsonMessageEnvelope { this.sourceDevice = content.getSenderDevice(); } if (content.getDataMessage().isPresent()) { - this.dataMessage = new JsonDataMessage(content.getDataMessage().get()); + this.dataMessage = new JsonDataMessage(content.getDataMessage().get(), m); } if (content.getSyncMessage().isPresent()) { - this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get()); + this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get(), m); } if (content.getCallMessage().isPresent()) { this.callMessage = new JsonCallMessage(content.getCallMessage().get()); diff --git a/src/main/java/org/asamk/signal/json/JsonQuote.java b/src/main/java/org/asamk/signal/json/JsonQuote.java new file mode 100644 index 00000000..10cd0bf4 --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonQuote.java @@ -0,0 +1,40 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class JsonQuote { + + long id; + String author; + String text; + + List mentions; + List attachments; + + JsonQuote(SignalServiceDataMessage.Quote quote, Manager m) { + this.id = quote.getId(); + this.author = m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier(); + this.text = quote.getText(); + + if (quote.getMentions() != null && quote.getMentions().size() > 0) { + this.mentions = quote.getMentions() + .stream() + .map(quotedMention -> new JsonMention(quotedMention, m)) + .collect(Collectors.toList()); + } + + if (quote.getAttachments().size() > 0) { + this.attachments = quote.getAttachments() + .stream() + .map(JsonQuotedAttachment::new) + .collect(Collectors.toList()); + } else { + this.attachments = new ArrayList<>(); + } + } +} diff --git a/src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java b/src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java new file mode 100644 index 00000000..bcbbe2a5 --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java @@ -0,0 +1,20 @@ +package org.asamk.signal.json; + +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; + +public class JsonQuotedAttachment { + + String contentType; + String filename; + JsonAttachment thumbnail; + + JsonQuotedAttachment(SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment) { + contentType = quotedAttachment.getContentType(); + filename = quotedAttachment.getFileName(); + if (quotedAttachment.getThumbnail() != null) { + thumbnail = new JsonAttachment(quotedAttachment.getThumbnail()); + } else { + thumbnail = null; + } + } +} diff --git a/src/main/java/org/asamk/signal/json/JsonReaction.java b/src/main/java/org/asamk/signal/json/JsonReaction.java new file mode 100644 index 00000000..5e978fe0 --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonReaction.java @@ -0,0 +1,19 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction; + +public class JsonReaction { + + String emoji; + String targetAuthor; + long targetSentTimestamp; + boolean isRemove; + + JsonReaction(Reaction reaction, Manager m) { + this.emoji = reaction.getEmoji(); + this.targetAuthor = m.resolveSignalServiceAddress(reaction.getTargetAuthor()).getLegacyIdentifier(); + this.targetSentTimestamp = reaction.getTargetSentTimestamp(); + this.isRemove = reaction.isRemove(); + } +} diff --git a/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java index c6571a93..7ea75bbd 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java @@ -1,14 +1,15 @@ package org.asamk.signal.json; import org.asamk.Signal; +import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; class JsonSyncDataMessage extends JsonDataMessage { String destination; - JsonSyncDataMessage(SentTranscriptMessage transcriptMessage) { - super(transcriptMessage.getMessage()); + JsonSyncDataMessage(SentTranscriptMessage transcriptMessage, Manager m) { + super(transcriptMessage.getMessage(), m); if (transcriptMessage.getDestination().isPresent()) { this.destination = transcriptMessage.getDestination().get().getLegacyIdentifier(); } diff --git a/src/main/java/org/asamk/signal/json/JsonSyncMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java index 31c39a3f..f29bc02e 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java @@ -1,6 +1,7 @@ package org.asamk.signal.json; import org.asamk.Signal; +import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -21,9 +22,9 @@ class JsonSyncMessage { List readMessages; JsonSyncMessageType type; - JsonSyncMessage(SignalServiceSyncMessage syncMessage) { + JsonSyncMessage(SignalServiceSyncMessage syncMessage, Manager m) { if (syncMessage.getSent().isPresent()) { - this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get()); + this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get(), m); } if (syncMessage.getBlockedList().isPresent()) { this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size()); diff --git a/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java b/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java new file mode 100644 index 00000000..779642b6 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java @@ -0,0 +1,65 @@ +package org.asamk.signal.manager; + +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.util.Base64; + +import java.io.IOException; +import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.whispersystems.signalservice.internal.util.Util.isEmpty; + +public class DeviceLinkInfo { + + final String deviceIdentifier; + final ECPublicKey deviceKey; + + public static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException { + final String rawQuery = linkUri.getRawQuery(); + if (isEmpty(rawQuery)) { + throw new RuntimeException("Invalid device link uri"); + } + + Map query = getQueryMap(rawQuery); + String deviceIdentifier = query.get("uuid"); + String publicKeyEncoded = query.get("pub_key"); + + if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) { + throw new RuntimeException("Invalid device link uri"); + } + + ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); + + return new DeviceLinkInfo(deviceIdentifier, deviceKey); + } + + private static Map getQueryMap(String query) { + String[] params = query.split("&"); + Map map = new HashMap<>(); + for (String param : params) { + final String[] paramParts = param.split("="); + String name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8); + String value = URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8); + map.put(name, value); + } + return map; + } + + public DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) { + this.deviceIdentifier = deviceIdentifier; + this.deviceKey = deviceKey; + } + + public String createDeviceLinkUri() { + return "tsdevice:/?uuid=" + + URLEncoder.encode(deviceIdentifier, StandardCharsets.UTF_8) + + "&pub_key=" + + URLEncoder.encode(Base64.encodeBytesWithoutPadding(deviceKey.serialize()), StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java deleted file mode 100644 index 0c0d6d2d..00000000 --- a/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.asamk.signal.manager; - -import org.whispersystems.util.Base64; - -public class GroupNotFoundException extends Exception { - - public GroupNotFoundException(byte[] groupId) { - super("Group not found: " + Base64.encodeBytes(groupId)); - } -} diff --git a/src/main/java/org/asamk/signal/manager/HandleAction.java b/src/main/java/org/asamk/signal/manager/HandleAction.java index 9bdd3885..0dd151a9 100644 --- a/src/main/java/org/asamk/signal/manager/HandleAction.java +++ b/src/main/java/org/asamk/signal/manager/HandleAction.java @@ -1,8 +1,8 @@ package org.asamk.signal.manager; +import org.asamk.signal.manager.groups.GroupIdV1; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import java.util.Arrays; import java.util.Objects; interface HandleAction { @@ -93,9 +93,9 @@ class SendSyncBlockedListAction implements HandleAction { class SendGroupInfoRequestAction implements HandleAction { private final SignalServiceAddress address; - private final byte[] groupId; + private final GroupIdV1 groupId; - public SendGroupInfoRequestAction(final SignalServiceAddress address, final byte[] groupId) { + public SendGroupInfoRequestAction(final SignalServiceAddress address, final GroupIdV1 groupId) { this.address = address; this.groupId = groupId; } @@ -109,14 +109,17 @@ class SendGroupInfoRequestAction implements HandleAction { public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o; - return address.equals(that.address) && Arrays.equals(groupId, that.groupId); + + if (!address.equals(that.address)) return false; + return groupId.equals(that.groupId); } @Override public int hashCode() { - int result = Objects.hash(address); - result = 31 * result + Arrays.hashCode(groupId); + int result = address.hashCode(); + result = 31 * result + groupId.hashCode(); return result; } } @@ -124,9 +127,9 @@ class SendGroupInfoRequestAction implements HandleAction { class SendGroupUpdateAction implements HandleAction { private final SignalServiceAddress address; - private final byte[] groupId; + private final GroupIdV1 groupId; - public SendGroupUpdateAction(final SignalServiceAddress address, final byte[] groupId) { + public SendGroupUpdateAction(final SignalServiceAddress address, final GroupIdV1 groupId) { this.address = address; this.groupId = groupId; } @@ -140,14 +143,17 @@ class SendGroupUpdateAction implements HandleAction { public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + final SendGroupUpdateAction that = (SendGroupUpdateAction) o; - return address.equals(that.address) && Arrays.equals(groupId, that.groupId); + + if (!address.equals(that.address)) return false; + return groupId.equals(that.groupId); } @Override public int hashCode() { - int result = Objects.hash(address); - result = 31 * result + Arrays.hashCode(groupId); + int result = address.hashCode(); + result = 31 * result + groupId.hashCode(); return result; } } diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java deleted file mode 100644 index 1f12193c..00000000 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.asamk.signal.manager; - -import org.asamk.signal.util.RandomUtils; -import org.signal.zkgroup.InvalidInputException; -import org.signal.zkgroup.profiles.ProfileKey; -import org.whispersystems.util.Base64; - -class KeyUtils { - - private KeyUtils() { - } - - static String createSignalingKey() { - return getSecret(52); - } - - static ProfileKey createProfileKey() { - try { - return new ProfileKey(getSecretBytes(32)); - } catch (InvalidInputException e) { - throw new AssertionError("Profile key is guaranteed to be 32 bytes here"); - } - } - - static String createPassword() { - return getSecret(18); - } - - static byte[] createGroupId() { - return getSecretBytes(16); - } - - static byte[] createStickerUploadKey() { - return getSecretBytes(32); - } - - private static String getSecret(int size) { - byte[] secret = getSecretBytes(size); - return Base64.encodeBytes(secret); - } - - private static byte[] getSecretBytes(int size) { - byte[] secret = new byte[size]; - RandomUtils.getSecureRandom().nextBytes(secret); - return secret; - } -} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 6db40566..d0a9e277 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2020 AsamK and contributors + Copyright (C) 2015-2021 AsamK and contributors 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 @@ -18,20 +18,31 @@ package org.asamk.signal.manager; import com.fasterxml.jackson.databind.ObjectMapper; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.GroupHelper; +import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; -import org.asamk.signal.storage.SignalAccount; -import org.asamk.signal.storage.contacts.ContactInfo; -import org.asamk.signal.storage.groups.GroupInfo; -import org.asamk.signal.storage.groups.GroupInfoV1; -import org.asamk.signal.storage.groups.GroupInfoV2; -import org.asamk.signal.storage.profiles.SignalProfile; -import org.asamk.signal.storage.profiles.SignalProfileEntry; -import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; -import org.asamk.signal.storage.stickers.Sticker; -import org.asamk.signal.util.IOUtils; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.contacts.ContactInfo; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.storage.messageCache.CachedMessage; +import org.asamk.signal.manager.storage.profiles.SignalProfile; +import org.asamk.signal.manager.storage.profiles.SignalProfileEntry; +import org.asamk.signal.manager.storage.protocol.IdentityInfo; +import org.asamk.signal.manager.storage.stickers.Sticker; +import org.asamk.signal.manager.util.AttachmentUtils; +import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.KeyUtils; +import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.InvalidMetadataMessageException; import org.signal.libsignal.metadata.InvalidMetadataVersionException; import org.signal.libsignal.metadata.ProtocolDuplicateMessageException; @@ -43,8 +54,10 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException; import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.SelfSendException; +import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.VerificationFailedException; @@ -54,6 +67,8 @@ import org.signal.zkgroup.groups.GroupSecretParams; import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -64,10 +79,10 @@ import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.libsignal.util.Medium; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.KeyBackupService; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; @@ -78,10 +93,11 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; -import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; +import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -115,6 +131,7 @@ import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; @@ -125,7 +142,6 @@ import org.whispersystems.signalservice.internal.contacts.crypto.Unauthenticated import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; -import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.util.Base64; @@ -149,14 +165,11 @@ import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; @@ -172,28 +185,30 @@ import static org.asamk.signal.manager.ServiceConfig.getIasKeyStore; public class Manager implements Closeable { - private final SleepTimer timer = new UptimeSleepTimer(); + final static Logger logger = LoggerFactory.getLogger(Manager.class); + + private final CertificateValidator certificateValidator = new CertificateValidator(ServiceConfig.getUnidentifiedSenderTrustRoot()); private final SignalServiceConfiguration serviceConfiguration; private final String userAgent; - private final boolean discoverableByPhoneNumber = true; - private final boolean unrestrictedUnidentifiedAccess = false; - private final SignalAccount account; + private SignalAccount account; private final PathConfig pathConfig; - private SignalServiceAccountManager accountManager; - private GroupsV2Api groupsV2Api; + private final SignalServiceAccountManager accountManager; + private final GroupsV2Api groupsV2Api; private final GroupsV2Operations groupsV2Operations; + private final SignalServiceMessageReceiver messageReceiver; + private final ClientZkProfileOperations clientZkProfileOperations; - private SignalServiceMessageReceiver messageReceiver = null; private SignalServiceMessagePipe messagePipe = null; private SignalServiceMessagePipe unidentifiedMessagePipe = null; private final UnidentifiedAccessHelper unidentifiedAccessHelper; private final ProfileHelper profileHelper; private final GroupHelper groupHelper; + private final PinHelper pinHelper; - public Manager( + Manager( SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, @@ -205,8 +220,31 @@ public class Manager implements Closeable { this.userAgent = userAgent; this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create( serviceConfiguration)) : null; - this.accountManager = createSignalServiceAccountManager(); + final SleepTimer timer = new UptimeSleepTimer(); + this.accountManager = new SignalServiceAccountManager(serviceConfiguration, + new DynamicCredentialsProvider(account.getUuid(), + account.getUsername(), + account.getPassword(), + account.getSignalingKey(), + account.getDeviceId()), + userAgent, + groupsV2Operations, + timer); this.groupsV2Api = accountManager.getGroupsV2Api(); + final KeyBackupService keyBackupService = ServiceConfig.createKeyBackupService(accountManager); + this.pinHelper = new PinHelper(keyBackupService); + this.clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create(serviceConfiguration) + .getProfileOperations() : null; + this.messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, + account.getUuid(), + account.getUsername(), + account.getPassword(), + account.getDeviceId(), + account.getSignalingKey(), + userAgent, + null, + timer, + clientZkProfileOperations); this.account.setResolver(this::resolveSignalServiceAddress); @@ -217,7 +255,7 @@ public class Manager implements Closeable { this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(), - this::getOrCreateMessageReceiver); + () -> messageReceiver); this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, account::getSelfAddress, @@ -234,18 +272,6 @@ public class Manager implements Closeable { return account.getSelfAddress(); } - private SignalServiceAccountManager createSignalServiceAccountManager() { - return new SignalServiceAccountManager(serviceConfiguration, - new DynamicCredentialsProvider(account.getUuid(), - account.getUsername(), - account.getPassword(), - null, - account.getDeviceId()), - userAgent, - groupsV2Operations, - timer); - } - private IdentityKeyPair getIdentityKeyPair() { return account.getSignalProtocolStore().getIdentityKeyPair(); } @@ -254,76 +280,22 @@ public class Manager implements Closeable { return account.getDeviceId(); } - private String getMessageCachePath() { - return pathConfig.getDataPath() + "/" + account.getUsername() + ".d/msg-cache"; - } - - private String getMessageCachePath(String sender) { - if (sender == null || sender.isEmpty()) { - return getMessageCachePath(); - } - - return getMessageCachePath() + "/" + sender.replace("/", "_"); - } - - private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException { - String cachePath = getMessageCachePath(sender); - IOUtils.createPrivateDirectories(cachePath); - return new File(cachePath + "/" + now + "_" + timestamp); - } - public static Manager init( - String username, String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent - ) throws IOException { + String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent + ) throws IOException, NotRegisteredException { PathConfig pathConfig = PathConfig.createDefault(settingsPath); if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { - IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); - int registrationId = KeyHelper.generateRegistrationId(false); - - ProfileKey profileKey = KeyUtils.createProfileKey(); - SignalAccount account = SignalAccount.create(pathConfig.getDataPath(), - username, - identityKey, - registrationId, - profileKey); - account.save(); - - return new Manager(account, pathConfig, serviceConfiguration, userAgent); + throw new NotRegisteredException(); } SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username); - Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent); - - m.migrateLegacyConfigs(); - - return m; - } - - private void migrateLegacyConfigs() { - if (account.getProfileKey() == null && isRegistered()) { - // Old config file, creating new profile key - account.setProfileKey(KeyUtils.createProfileKey()); - account.save(); + if (!account.isRegistered()) { + throw new NotRegisteredException(); } - // Store profile keys only in profile store - for (ContactInfo contact : account.getContactStore().getContacts()) { - String profileKeyString = contact.profileKey; - if (profileKeyString == null) { - continue; - } - final ProfileKey profileKey; - try { - profileKey = new ProfileKey(Base64.decode(profileKeyString)); - } catch (InvalidInputException | IOException e) { - continue; - } - contact.profileKey = null; - account.getProfileStore().storeProfileKey(contact.getAddress(), profileKey); - } - // Ensure our profile key is stored in profile store - account.getProfileStore().storeProfileKey(getSelfAddress(), account.getProfileKey()); + + return new Manager(account, pathConfig, serviceConfiguration, userAgent); } public void checkAccountState() throws IOException { @@ -344,36 +316,35 @@ public class Manager implements Closeable { return account.isRegistered(); } - public void register(boolean voiceVerification, String captcha) throws IOException { - account.setPassword(KeyUtils.createPassword()); + /** + * This is used for checking a set of phone numbers for registration on Signal + * + * @param numbers The set of phone number in question + * @return A map of numbers to booleans. True if registered, false otherwise. Should never be null + * @throws IOException if its unable to check if the users are registered + */ + public Map areUsersRegistered(Set numbers) throws IOException { + // Note "contactDetails" has no optionals. It only gives us info on users who are registered + List contactDetails = this.accountManager.getContacts(numbers); - // Resetting UUID, because registering doesn't work otherwise - account.setUuid(null); - accountManager = createSignalServiceAccountManager(); - this.groupsV2Api = accountManager.getGroupsV2Api(); + Set registeredUsers = contactDetails.stream() + .map(ContactTokenDetails::getNumber) + .collect(Collectors.toSet()); - if (voiceVerification) { - accountManager.requestVoiceVerificationCode(Locale.getDefault(), - Optional.fromNullable(captcha), - Optional.absent()); - } else { - accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent()); - } - - account.setRegistered(false); - account.save(); + return numbers.stream().collect(Collectors.toMap(x -> x, registeredUsers::contains)); } public void updateAccountAttributes() throws IOException { accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, - account.getRegistrationLockPin(), - account.getRegistrationLock(), - unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(), - unrestrictedUnidentifiedAccess, + // set legacy pin only if no KBS master key is set + account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null, + account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), capabilities, - discoverableByPhoneNumber); + account.isDiscoverableByPhoneNumber()); } public void setProfile(String name, File avatar) throws IOException { @@ -407,7 +378,7 @@ public class Manager implements Closeable { } public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - Utils.DeviceLinkInfo info = Utils.parseDeviceLinkUri(linkUri); + DeviceLinkInfo info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); addDevice(info.deviceIdentifier, info.deviceKey); } @@ -462,43 +433,25 @@ public class Manager implements Closeable { } } - public void verifyAccount(String verificationCode, String pin) throws IOException { - verificationCode = verificationCode.replace("-", ""); - account.setSignalingKey(KeyUtils.createSignalingKey()); - // TODO make unrestricted unidentified access configurable - VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, - account.getSignalingKey(), - account.getSignalProtocolStore().getLocalRegistrationId(), - true, - pin, - null, - unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(), - unrestrictedUnidentifiedAccess, - capabilities, - discoverableByPhoneNumber); - - UUID uuid = UuidUtil.parseOrNull(response.getUuid()); - // TODO response.isStorageCapable() - //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); - account.setRegistered(true); - account.setUuid(uuid); - account.setRegistrationLockPin(pin); - account.getSignalProtocolStore() - .saveIdentity(account.getSelfAddress(), - getIdentityKeyPair().getPublicKey(), - TrustLevel.TRUSTED_VERIFIED); - - refreshPreKeys(); - account.save(); - } - - public void setRegistrationLockPin(Optional pin) throws IOException { + public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException { if (pin.isPresent()) { + final MasterKey masterKey = account.getPinMasterKey() != null + ? account.getPinMasterKey() + : KeyUtils.createMasterKey(); + + pinHelper.setRegistrationLockPin(pin.get(), masterKey); + account.setRegistrationLockPin(pin.get()); - throw new RuntimeException("Not implemented anymore, will be replaced with KBS"); + account.setPinMasterKey(masterKey); } else { - account.setRegistrationLockPin(null); + // Remove legacy registration lock accountManager.removeRegistrationLockV1(); + + // Remove KBS Pin + pinHelper.removeRegistrationLockPin(); + + account.setRegistrationLockPin(null); + account.setPinMasterKey(null); } account.save(); } @@ -511,45 +464,21 @@ public class Manager implements Closeable { accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); } - private SignalServiceMessageReceiver createMessageReceiver() { - final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create( - serviceConfiguration).getProfileOperations() : null; - return new SignalServiceMessageReceiver(serviceConfiguration, - account.getUuid(), - account.getUsername(), - account.getPassword(), - account.getDeviceId(), - account.getSignalingKey(), - userAgent, - null, - timer, - clientZkProfileOperations); - } - - private SignalServiceMessageReceiver getOrCreateMessageReceiver() { - if (messageReceiver == null) { - messageReceiver = createMessageReceiver(); - } - return messageReceiver; - } - private SignalServiceMessagePipe getOrCreateMessagePipe() { if (messagePipe == null) { - messagePipe = getOrCreateMessageReceiver().createMessagePipe(); + messagePipe = messageReceiver.createMessagePipe(); } return messagePipe; } private SignalServiceMessagePipe getOrCreateUnidentifiedMessagePipe() { if (unidentifiedMessagePipe == null) { - unidentifiedMessagePipe = getOrCreateMessageReceiver().createUnidentifiedMessagePipe(); + unidentifiedMessagePipe = messageReceiver.createUnidentifiedMessagePipe(); } return unidentifiedMessagePipe; } private SignalServiceMessageSender createMessageSender() { - final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create( - serviceConfiguration).getProfileOperations() : null; final ExecutorService executor = null; return new SignalServiceMessageSender(serviceConfiguration, account.getUuid(), @@ -589,7 +518,7 @@ public class Manager implements Closeable { try { profile = retrieveRecipientProfile(address, profileKey); } catch (IOException e) { - System.err.println("Failed to retrieve profile, ignoring: " + e.getMessage()); + logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); profileEntry.setRequestPending(false); return null; } @@ -612,7 +541,7 @@ public class Manager implements Closeable { profileAndCredential = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); } catch (IOException e) { - System.err.println("Failed to retrieve profile key credential, ignoring: " + e.getMessage()); + logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); return null; } @@ -645,7 +574,7 @@ public class Manager implements Closeable { ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey); } catch (Throwable e) { - System.err.println("Failed to retrieve profile avatar, ignoring: " + e.getMessage()); + logger.warn("Failed to retrieve profile avatar, ignoring: {}", e.getMessage()); } ProfileCipher profileCipher = new ProfileCipher(profileKey); @@ -678,13 +607,13 @@ public class Manager implements Closeable { } } - private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { + private Optional createGroupAvatarAttachment(GroupId groupId) throws IOException { File file = getGroupAvatarFile(groupId); if (!file.exists()) { return Optional.absent(); } - return Optional.of(Utils.createAttachment(file)); + return Optional.of(AttachmentUtils.createAttachment(file)); } private Optional createContactAvatarAttachment(String number) throws IOException { @@ -693,11 +622,11 @@ public class Manager implements Closeable { return Optional.absent(); } - return Optional.of(Utils.createAttachment(file)); + return Optional.of(AttachmentUtils.createAttachment(file)); } - private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException { - GroupInfo g = account.getGroupStore().getGroup(groupId); + private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { + GroupInfo g = getGroup(groupId); if (g == null) { throw new GroupNotFoundException(groupId); } @@ -707,8 +636,8 @@ public class Manager implements Closeable { return g; } - private GroupInfo getGroupForUpdating(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException { - GroupInfo g = account.getGroupStore().getGroup(groupId); + private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { + GroupInfo g = getGroup(groupId); if (g == null) { throw new GroupNotFoundException(groupId); } @@ -723,7 +652,7 @@ public class Manager implements Closeable { } public Pair> sendGroupMessage( - SignalServiceDataMessage.Builder messageBuilder, byte[] groupId + SignalServiceDataMessage.Builder messageBuilder, GroupId groupId ) throws IOException, GroupNotFoundException, NotAGroupMemberException { final GroupInfo g = getGroupForSending(groupId); @@ -734,19 +663,19 @@ public class Manager implements Closeable { } public Pair> sendGroupMessage( - String messageText, List attachments, byte[] groupId + String messageText, List attachments, GroupId groupId ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withBody(messageText); if (attachments != null) { - messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); + messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); } return sendGroupMessage(messageBuilder, groupId); } public Pair> sendGroupMessageReaction( - String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, byte[] groupId + String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, @@ -758,7 +687,7 @@ public class Manager implements Closeable { return sendGroupMessage(messageBuilder, groupId); } - public Pair> sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { + public Pair> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { SignalServiceDataMessage.Builder messageBuilder; @@ -766,7 +695,7 @@ public class Manager implements Closeable { if (g instanceof GroupInfoV1) { GroupInfoV1 groupInfoV1 = (GroupInfoV1) g; SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) - .withId(groupId) + .withId(groupId.serialize()) .build(); messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); groupInfoV1.removeMember(account.getSelfAddress()); @@ -782,8 +711,8 @@ public class Manager implements Closeable { return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - private Pair> sendUpdateGroupMessage( - byte[] groupId, String name, Collection members, String avatarFile + private Pair> sendUpdateGroupMessage( + GroupId groupId, String name, Collection members, String avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { GroupInfo g; SignalServiceDataMessage.Builder messageBuilder; @@ -791,8 +720,8 @@ public class Manager implements Closeable { // Create new group GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile); if (gv2 == null) { - GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId()); - gv1.addMembers(Collections.singleton(account.getSelfAddress())); + GroupInfoV1 gv1 = new GroupInfoV1(GroupIdV1.createRandom()); + gv1.addMembers(List.of(account.getSelfAddress())); updateGroupV1(gv1, name, members, avatarFile); messageBuilder = getGroupUpdateMessageBuilder(gv1); g = gv1; @@ -815,7 +744,10 @@ public class Manager implements Closeable { if (members != null) { final Set newMembers = new HashSet<>(members); - newMembers.removeAll(group.getMembers()); + newMembers.removeAll(group.getMembers() + .stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toSet())); if (newMembers.size() > 0) { Pair groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers); @@ -833,7 +765,7 @@ public class Manager implements Closeable { groupGroupChangePair.second()); } - return new Pair<>(group.groupId, result.second()); + return new Pair<>(group.getGroupId(), result.second()); } else { GroupInfoV1 gv1 = (GroupInfoV1) group; updateGroupV1(gv1, name, members, avatarFile); @@ -846,7 +778,35 @@ public class Manager implements Closeable { final Pair> result = sendMessage(messageBuilder, g.getMembersIncludingPendingWithout(account.getSelfAddress())); - return new Pair<>(g.groupId, result.second()); + return new Pair<>(g.getGroupId(), result.second()); + } + + public Pair> joinGroup( + GroupInviteLinkUrl inviteLinkUrl + ) throws IOException, GroupLinkNotActiveException { + return sendJoinGroupMessage(inviteLinkUrl); + } + + private Pair> sendJoinGroupMessage( + GroupInviteLinkUrl inviteLinkUrl + ) throws IOException, GroupLinkNotActiveException { + final DecryptedGroupJoinInfo groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), + inviteLinkUrl.getPassword()); + final GroupChange groupChange = groupHelper.joinGroup(inviteLinkUrl.getGroupMasterKey(), + inviteLinkUrl.getPassword(), + groupJoinInfo); + final GroupInfoV2 group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), + groupJoinInfo.getRevision() + 1, + groupChange.toByteArray()); + + if (group.getGroup() == null) { + // Only requested member, can't send update to group members + return new Pair<>(group.getGroupId(), List.of()); + } + + final Pair> result = sendUpdateGroupMessage(group, group.getGroup(), groupChange); + + return new Pair<>(group.getGroupId(), result.second()); } private Pair> sendUpdateGroupMessage( @@ -885,7 +845,7 @@ public class Manager implements Closeable { newE164Members.remove(contact.getNumber()); } throw new IOException("Failed to add members " - + Util.join(", ", newE164Members) + + String.join(", ", newE164Members) + " to group: Not registered on Signal"); } @@ -894,13 +854,13 @@ public class Manager implements Closeable { if (avatarFile != null) { IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); - File aFile = getGroupAvatarFile(g.groupId); + File aFile = getGroupAvatarFile(g.getGroupId()); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } } Pair> sendUpdateGroupMessage( - byte[] groupId, SignalServiceAddress recipient + GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { GroupInfoV1 g; GroupInfo group = getGroupForSending(groupId); @@ -916,19 +876,19 @@ public class Manager implements Closeable { SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - return sendMessage(messageBuilder, Collections.singleton(recipient)); + return sendMessage(messageBuilder, List.of(recipient)); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) - .withId(g.groupId) + .withId(g.getGroupId().serialize()) .withName(g.name) .withMembers(new ArrayList<>(g.getMembers())); - File aFile = getGroupAvatarFile(g.groupId); + File aFile = getGroupAvatarFile(g.getGroupId()); if (aFile.exists()) { try { - group.withAvatar(Utils.createAttachment(aFile)); + group.withAvatar(AttachmentUtils.createAttachment(aFile)); } catch (IOException e) { throw new AttachmentInvalidException(aFile.toString(), e); } @@ -949,23 +909,23 @@ public class Manager implements Closeable { } Pair> sendGroupInfoRequest( - byte[] groupId, SignalServiceAddress recipient + GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO) - .withId(groupId); + .withId(groupId.serialize()); SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()); // Send group info request message to the recipient who sent us a message with this groupId - return sendMessage(messageBuilder, Collections.singleton(recipient)); + return sendMessage(messageBuilder, List.of(recipient)); } void sendReceipt( SignalServiceAddress remoteAddress, long messageId ) throws IOException, UntrustedIdentityException { SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, - Collections.singletonList(messageId), + List.of(messageId), System.currentTimeMillis()); createMessageSender().sendReceipt(remoteAddress, @@ -979,7 +939,7 @@ public class Manager implements Closeable { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withBody(messageText); if (attachments != null) { - List attachmentStreams = Utils.getSignalServiceAttachments(attachments); + List attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); // Upload attachments here, so we only upload once even for multiple recipients SignalServiceMessageSender messageSender = createMessageSender(); @@ -1058,7 +1018,7 @@ public class Manager implements Closeable { account.save(); } - public void setGroupBlocked(final byte[] groupId, final boolean blocked) throws GroupNotFoundException { + public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { GroupInfo group = getGroup(groupId); if (group == null) { throw new GroupNotFoundException(groupId); @@ -1069,8 +1029,8 @@ public class Manager implements Closeable { account.save(); } - public Pair> updateGroup( - byte[] groupId, String name, List members, String avatar + public Pair> updateGroup( + GroupId groupId, String name, List members, String avatar ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { return sendUpdateGroupMessage(groupId, name, @@ -1092,7 +1052,7 @@ public class Manager implements Closeable { private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asExpirationUpdate(); - sendMessage(messageBuilder, Collections.singleton(address)); + sendMessage(messageBuilder, List.of(address)); } /** @@ -1108,8 +1068,8 @@ public class Manager implements Closeable { /** * Change the expiration timer for a group */ - public void setExpirationTimer(byte[] groupId, int messageExpirationTimer) { - GroupInfo g = account.getGroupStore().getGroup(groupId); + public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) { + GroupInfo g = getGroup(groupId); if (g instanceof GroupInfoV1) { GroupInfoV1 groupInfoV1 = (GroupInfoV1) g; groupInfoV1.messageExpirationTime = messageExpirationTimer; @@ -1125,7 +1085,7 @@ public class Manager implements Closeable { * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file * @return if successful, returns the URL to install the sticker pack in the signal app */ - public String uploadStickerPack(String path) throws IOException, StickerPackInvalidException { + public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException { SignalServiceStickerManifestUpload manifest = getSignalServiceStickerManifestUpload(path); SignalServiceMessageSender messageSender = createMessageSender(); @@ -1150,12 +1110,11 @@ public class Manager implements Closeable { } private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload( - final String path + final File file ) throws IOException, StickerPackInvalidException { ZipFile zip = null; String rootPath = null; - final File file = new File(path); if (file.getName().endsWith(".zip")) { zip = new ZipFile(file); } else if (file.getName().equals("manifest.json")) { @@ -1295,7 +1254,7 @@ public class Manager implements Closeable { try { certificate = accountManager.getSenderCertificate(); } catch (IOException e) { - System.err.println("Failed to get sender certificate: " + e); + logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); return null; } // TODO cache for a day @@ -1334,7 +1293,7 @@ public class Manager implements Closeable { missingUuids.stream().map(a -> a.getNumber().get()).collect(Collectors.toSet()), CDS_MRENCLAVE); } catch (IOException | Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException e) { - System.err.println("Failed to resolve uuids from server: " + e.getMessage()); + logger.warn("Failed to resolve uuids from server, ignoring: {}", e.getMessage()); registeredUsers = new HashMap<>(); } @@ -1386,7 +1345,7 @@ public class Manager implements Closeable { .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); - return new Pair<>(timestamp, Collections.emptyList()); + return new Pair<>(timestamp, List.of()); } } else { // Send to all individually, so sync messages are sent correctly @@ -1429,7 +1388,7 @@ public class Manager implements Closeable { message.getTimestamp(), message, message.getExpiresInSeconds(), - Collections.singletonMap(recipient, unidentifiedAccess.isPresent()), + Map.of(recipient, unidentifiedAccess.isPresent()), false); SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); @@ -1468,7 +1427,7 @@ public class Manager implements Closeable { private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException { SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), - Utils.getCertificateValidator()); + certificateValidator); try { return cipher.decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { @@ -1522,23 +1481,25 @@ public class Manager implements Closeable { if (message.getGroupContext().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - GroupInfo group = account.getGroupStore().getGroupByV1Id(groupInfo.getGroupId()); + GroupIdV1 groupId = GroupId.v1(groupInfo.getGroupId()); + GroupInfo group = getGroup(groupId); if (group == null || group instanceof GroupInfoV1) { GroupInfoV1 groupV1 = (GroupInfoV1) group; switch (groupInfo.getType()) { case UPDATE: { if (groupV1 == null) { - groupV1 = new GroupInfoV1(groupInfo.getGroupId()); + groupV1 = new GroupInfoV1(groupId); } if (groupInfo.getAvatar().isPresent()) { SignalServiceAttachment avatar = groupInfo.getAvatar().get(); if (avatar.isPointer()) { try { - retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.groupId); + retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.getGroupId()); } catch (IOException | InvalidMessageException | MissingConfigurationException e) { - System.err.println("Failed to retrieve group avatar (" + avatar.asPointer() - .getRemoteId() + "): " + e.getMessage()); + logger.warn("Failed to retrieve avatar for group {}, ignoring: {}", + groupId.toBase64(), + e.getMessage()); } } } @@ -1560,7 +1521,7 @@ public class Manager implements Closeable { } case DELIVER: if (groupV1 == null && !isSync) { - actions.add(new SendGroupInfoRequestAction(source, groupInfo.getGroupId())); + actions.add(new SendGroupInfoRequestAction(source, groupId)); } break; case QUIT: { @@ -1572,7 +1533,7 @@ public class Manager implements Closeable { } case REQUEST_INFO: if (groupV1 != null && !isSync) { - actions.add(new SendGroupUpdateAction(source, groupV1.groupId)); + actions.add(new SendGroupUpdateAction(source, groupV1.getGroupId())); } break; } @@ -1584,57 +1545,21 @@ public class Manager implements Closeable { final SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get(); final GroupMasterKey groupMasterKey = groupContext.getMasterKey(); - final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - - byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); - GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId); - if (groupInfo instanceof GroupInfoV1) { - // Received a v2 group message for a v2 group, we need to locally migrate the group - account.getGroupStore().deleteGroup(groupInfo.groupId); - GroupInfoV2 groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); - groupInfoV2.setGroup(getDecryptedGroup(groupSecretParams)); - account.getGroupStore().updateGroup(groupInfoV2); - System.err.println("Locally migrated group " - + Base64.encodeBytes(groupInfo.groupId) - + " to group v2, id: " - + Base64.encodeBytes(groupInfoV2.groupId) - + " !!!"); - } else if (groupInfo == null || groupInfo instanceof GroupInfoV2) { - GroupInfoV2 groupInfoV2 = groupInfo == null - ? new GroupInfoV2(groupId, groupMasterKey) - : (GroupInfoV2) groupInfo; - - if (groupInfoV2.getGroup() == null - || groupInfoV2.getGroup().getRevision() < groupContext.getRevision()) { - DecryptedGroup group = null; - if (groupContext.hasSignedGroupChange() - && groupInfoV2.getGroup() != null - && groupInfoV2.getGroup().getRevision() + 1 == groupContext.getRevision()) { - group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), - groupContext.getSignedGroupChange(), - groupMasterKey); - if (group != null) { - storeProfileKeysFromMembers(group); - } - } - if (group == null) { - group = getDecryptedGroup(groupSecretParams); - } - groupInfoV2.setGroup(group); - account.getGroupStore().updateGroup(groupInfoV2); - } - } + getOrMigrateGroup(groupMasterKey, + groupContext.getRevision(), + groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); } } + final SignalServiceAddress conversationPartnerAddress = isSync ? destination : source; - if (message.isEndSession()) { + if (conversationPartnerAddress != null && message.isEndSession()) { handleEndSession(conversationPartnerAddress); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { if (message.getGroupContext().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - GroupInfoV1 group = account.getGroupStore().getOrCreateGroupV1(groupInfo.getGroupId()); + GroupInfoV1 group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId())); if (group != null) { if (group.messageExpirationTime != message.getExpiresInSeconds()) { group.messageExpirationTime = message.getExpiresInSeconds(); @@ -1644,7 +1569,7 @@ public class Manager implements Closeable { } else if (message.getGroupContext().get().getGroupV2().isPresent()) { // disappearing message timer already stored in the DecryptedGroup } - } else { + } else if (conversationPartnerAddress != null) { ContactInfo contact = account.getContactStore().getContact(conversationPartnerAddress); if (contact == null) { contact = new ContactInfo(conversationPartnerAddress); @@ -1661,10 +1586,9 @@ public class Manager implements Closeable { try { retrieveAttachment(attachment.asPointer()); } catch (IOException | InvalidMessageException | MissingConfigurationException e) { - System.err.println("Failed to retrieve attachment (" - + attachment.asPointer().getRemoteId() - + "): " - + e.getMessage()); + logger.warn("Failed to retrieve attachment ({}), ignoring: {}", + attachment.asPointer().getRemoteId(), + e.getMessage()); } } } @@ -1689,10 +1613,25 @@ public class Manager implements Closeable { try { retrieveAttachment(attachment); } catch (IOException | InvalidMessageException | MissingConfigurationException e) { - System.err.println("Failed to retrieve attachment (" - + attachment.getRemoteId() - + "): " - + e.getMessage()); + logger.warn("Failed to retrieve preview image ({}), ignoring: {}", + attachment.getRemoteId(), + e.getMessage()); + } + } + } + } + if (message.getQuote().isPresent()) { + final SignalServiceDataMessage.Quote quote = message.getQuote().get(); + + for (SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment : quote.getAttachments()) { + final SignalServiceAttachment attachment = quotedAttachment.getThumbnail(); + if (attachment != null && attachment.isPointer()) { + try { + retrieveAttachment(attachment.asPointer()); + } catch (IOException | InvalidMessageException | MissingConfigurationException e) { + logger.warn("Failed to retrieve quote attachment thumbnail ({}), ignoring: {}", + attachment.asPointer().getRemoteId(), + e.getMessage()); } } } @@ -1708,16 +1647,53 @@ public class Manager implements Closeable { return actions; } - private DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) { - try { - final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); - DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString); - storeProfileKeysFromMembers(group); - return group; - } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { - System.err.println("Failed to retrieve Group V2 info, ignoring ..."); - return null; + private GroupInfoV2 getOrMigrateGroup( + final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange + ) { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + + GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams); + GroupInfo groupInfo = getGroup(groupId); + final GroupInfoV2 groupInfoV2; + if (groupInfo instanceof GroupInfoV1) { + // Received a v2 group message for a v1 group, we need to locally migrate the group + account.getGroupStore().deleteGroup(groupInfo.getGroupId()); + groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); + logger.info("Locally migrated group {} to group v2, id: {}", + groupInfo.getGroupId().toBase64(), + groupInfoV2.getGroupId().toBase64()); + } else if (groupInfo instanceof GroupInfoV2) { + groupInfoV2 = (GroupInfoV2) groupInfo; + } else { + groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); } + + if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) { + DecryptedGroup group = null; + if (signedGroupChange != null + && groupInfoV2.getGroup() != null + && groupInfoV2.getGroup().getRevision() + 1 == revision) { + group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey); + } + if (group == null) { + group = groupHelper.getDecryptedGroup(groupSecretParams); + } + if (group != null) { + storeProfileKeysFromMembers(group); + final String avatar = group.getAvatar(); + if (avatar != null && !avatar.isEmpty()) { + try { + retrieveGroupAvatar(groupId, groupSecretParams, avatar); + } catch (IOException e) { + logger.warn("Failed to download group avatar, ignoring: {}", e.getMessage()); + } + } + } + groupInfoV2.setGroup(group); + account.getGroupStore().updateGroup(groupInfoV2); + } + + return groupInfoV2; } private void storeProfileKeysFromMembers(final DecryptedGroup group) { @@ -1732,41 +1708,17 @@ public class Manager implements Closeable { } } - private void retryFailedReceivedMessages( - ReceiveMessageHandler handler, boolean ignoreAttachments - ) { - final File cachePath = new File(getMessageCachePath()); - if (!cachePath.exists()) { - return; - } - for (final File dir : Objects.requireNonNull(cachePath.listFiles())) { - if (!dir.isDirectory()) { - retryFailedReceivedMessage(handler, ignoreAttachments, dir); - continue; - } - - for (final File fileEntry : Objects.requireNonNull(dir.listFiles())) { - if (!fileEntry.isFile()) { - continue; - } - retryFailedReceivedMessage(handler, ignoreAttachments, fileEntry); - } - // Try to delete directory if empty - dir.delete(); + private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { + for (CachedMessage cachedMessage : account.getMessageCache().getCachedMessages()) { + retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); } } private void retryFailedReceivedMessage( - final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry + final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage ) { - SignalServiceEnvelope envelope; - try { - envelope = Utils.loadEnvelope(fileEntry); - if (envelope == null) { - return; - } - } catch (IOException e) { - e.printStackTrace(); + SignalServiceEnvelope envelope = cachedMessage.loadEnvelope(); + if (envelope == null) { return; } SignalServiceContent content = null; @@ -1777,11 +1729,7 @@ public class Manager implements Closeable { return; } catch (Exception er) { // All other errors are not recoverable, so delete the cached message - try { - Files.delete(fileEntry.toPath()); - } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); - } + cachedMessage.delete(); return; } List actions = handleMessage(envelope, content, ignoreAttachments); @@ -1795,11 +1743,7 @@ public class Manager implements Closeable { } account.save(); handler.handleMessage(envelope, content, null); - try { - Files.delete(fileEntry.toPath()); - } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); - } + cachedMessage.delete(); } public void receiveMessages( @@ -1813,7 +1757,7 @@ public class Manager implements Closeable { Set queuedActions = null; - getOrCreateMessagePipe(); + final SignalServiceMessagePipe messagePipe = getOrCreateMessagePipe(); boolean hasCaughtUpWithOldMessages = false; @@ -1821,18 +1765,11 @@ public class Manager implements Closeable { SignalServiceEnvelope envelope; SignalServiceContent content = null; Exception exception = null; - final long now = new Date().getTime(); + final CachedMessage[] cachedMessage = {null}; try { Optional result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> { // store message on disk, before acknowledging receipt to the server - try { - String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : ""; - File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp()); - Utils.storeEnvelope(envelope1, cacheFile); - } catch (IOException e) { - System.err.println("Failed to store encrypted message in disk cache, ignoring: " - + e.getMessage()); - } + cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1); }); if (result.isPresent()) { envelope = result.get(); @@ -1860,7 +1797,7 @@ public class Manager implements Closeable { if (returnOnTimeout) return; continue; } catch (InvalidVersionException e) { - System.err.println("Ignoring error: " + e.getMessage()); + logger.warn("Error while receiving messages, ignoring: {}", e.getMessage()); continue; } @@ -1896,15 +1833,8 @@ public class Manager implements Closeable { handler.handleMessage(envelope, content, exception); } if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { - File cacheFile = null; - try { - String source = envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : ""; - cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp()); - Files.delete(cacheFile.toPath()); - // Try to delete directory if empty - new File(getMessageCachePath()).delete(); - } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); + if (cachedMessage[0] != null) { + cachedMessage[0].delete(); } } } @@ -1929,19 +1859,14 @@ public class Manager implements Closeable { if (content != null && content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); if (message.getGroupContext().isPresent()) { - GroupInfo group = null; if (message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - if (groupInfo.getType() == SignalServiceGroup.Type.DELIVER) { - group = getGroup(groupInfo.getGroupId()); + if (groupInfo.getType() != SignalServiceGroup.Type.DELIVER) { + return false; } } - if (message.getGroupContext().get().getGroupV2().isPresent()) { - SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get(); - final GroupMasterKey groupMasterKey = groupContext.getMasterKey(); - byte[] groupId = GroupUtils.getGroupId(groupMasterKey); - group = account.getGroupStore().getGroupByV2Id(groupId); - } + GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + GroupInfo group = getGroup(groupId); if (group != null && group.isBlocked()) { return true; } @@ -1983,13 +1908,11 @@ public class Manager implements Closeable { if (syncMessage.getSent().isPresent()) { SentTranscriptMessage message = syncMessage.getSent().get(); final SignalServiceAddress destination = message.getDestination().orNull(); - if (destination != null) { - actions.addAll(handleSignalServiceDataMessage(message.getMessage(), - true, - sender, - destination, - ignoreAttachments)); - } + actions.addAll(handleSignalServiceDataMessage(message.getMessage(), + true, + sender, + destination, + ignoreAttachments)); } if (syncMessage.getRequest().isPresent()) { RequestMessage rm = syncMessage.getRequest().get(); @@ -2014,7 +1937,8 @@ public class Manager implements Closeable { DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroup g; while ((g = s.read()) != null) { - GroupInfoV1 syncGroup = account.getGroupStore().getOrCreateGroupV1(g.getId()); + GroupInfoV1 syncGroup = account.getGroupStore() + .getOrCreateGroupV1(GroupId.v1(g.getId())); if (syncGroup != null) { if (g.getName().isPresent()) { syncGroup.name = g.getName().get(); @@ -2027,7 +1951,7 @@ public class Manager implements Closeable { syncGroup.removeMember(account.getSelfAddress()); } else { // Add ourself to the member set as it's marked as active - syncGroup.addMembers(Collections.singleton(account.getSelfAddress())); + syncGroup.addMembers(List.of(account.getSelfAddress())); } syncGroup.blocked = g.isBlocked(); if (g.getColor().isPresent()) { @@ -2035,7 +1959,7 @@ public class Manager implements Closeable { } if (g.getAvatar().isPresent()) { - retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); + retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.getGroupId()); } syncGroup.inboxPosition = g.getInboxPosition().orNull(); syncGroup.archived = g.isArchived(); @@ -2044,16 +1968,18 @@ public class Manager implements Closeable { } } } catch (Exception e) { + logger.warn("Failed to handle received sync groups “{}”, ignoring: {}", + tmpFile, + e.getMessage()); e.printStackTrace(); } finally { if (tmpFile != null) { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete received groups temp file “" - + tmpFile - + "”: " - + e.getMessage()); + logger.warn("Failed to delete received groups temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); } } } @@ -2063,12 +1989,15 @@ public class Manager implements Closeable { for (SignalServiceAddress address : blockedListMessage.getAddresses()) { setContactBlocked(resolveSignalServiceAddress(address), true); } - for (byte[] groupId : blockedListMessage.getGroupIds()) { + for (GroupId groupId : blockedListMessage.getGroupIds() + .stream() + .map(GroupId::unknownVersion) + .collect(Collectors.toSet())) { try { setGroupBlocked(groupId, true); } catch (GroupNotFoundException e) { - System.err.println("BlockedListMessage contained groupID that was not found in GroupStore: " - + Base64.encodeBytes(groupId)); + logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", + groupId.toBase64()); } } } @@ -2129,10 +2058,9 @@ public class Manager implements Closeable { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete received contacts temp file “" - + tmpFile - + "”: " - + e.getMessage()); + logger.warn("Failed to delete received contacts temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); } } } @@ -2184,16 +2112,16 @@ public class Manager implements Closeable { return retrieveAttachment(pointer, getContactAvatarFile(number), false); } else { SignalServiceAttachmentStream stream = attachment.asStream(); - return Utils.retrieveAttachment(stream, getContactAvatarFile(number)); + return AttachmentUtils.retrieveAttachment(stream, getContactAvatarFile(number)); } } - private File getGroupAvatarFile(byte[] groupId) { - return new File(pathConfig.getAvatarsPath(), "group-" + Base64.encodeBytes(groupId).replace("/", "_")); + private File getGroupAvatarFile(GroupId groupId) { + return new File(pathConfig.getAvatarsPath(), "group-" + groupId.toBase64().replace("/", "_")); } private File retrieveGroupAvatarAttachment( - SignalServiceAttachment attachment, byte[] groupId + SignalServiceAttachment attachment, GroupId groupId ) throws IOException, InvalidMessageException, MissingConfigurationException { IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); if (attachment.isPointer()) { @@ -2201,10 +2129,40 @@ public class Manager implements Closeable { return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); } else { SignalServiceAttachmentStream stream = attachment.asStream(); - return Utils.retrieveAttachment(stream, getGroupAvatarFile(groupId)); + return AttachmentUtils.retrieveAttachment(stream, getGroupAvatarFile(groupId)); } } + private File retrieveGroupAvatar( + GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey + ) throws IOException { + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); + File outputFile = getGroupAvatarFile(groupId); + GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); + + File tmpFile = IOUtils.createTempFile(); + tmpFile.deleteOnExit(); + try (InputStream input = messageReceiver.retrieveGroupsV2ProfileAvatar(cdnKey, + tmpFile, + ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { + byte[] encryptedData = IOUtils.readFully(input); + + byte[] decryptedData = groupOperations.decryptAvatar(encryptedData); + try (OutputStream output = new FileOutputStream(outputFile)) { + output.write(decryptedData); + } + } finally { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); + } + } + return outputFile; + } + private File getProfileAvatarFile(SignalServiceAddress address) { return new File(pathConfig.getAvatarsPath(), "profile-" + address.getLegacyIdentifier()); } @@ -2213,11 +2171,10 @@ public class Manager implements Closeable { SignalServiceAddress address, String avatarPath, ProfileKey profileKey ) throws IOException { IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); - SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver(); File outputFile = getProfileAvatarFile(address); File tmpFile = IOUtils.createTempFile(); - try (InputStream input = receiver.retrieveProfileAvatar(avatarPath, + try (InputStream input = messageReceiver.retrieveProfileAvatar(avatarPath, tmpFile, profileKey, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { @@ -2227,7 +2184,9 @@ public class Manager implements Closeable { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete received avatar temp file “" + tmpFile + "”: " + e.getMessage()); + logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); } } return outputFile; @@ -2256,8 +2215,6 @@ public class Manager implements Closeable { } } - final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver(); - File tmpFile = IOUtils.createTempFile(); try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, @@ -2267,10 +2224,9 @@ public class Manager implements Closeable { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete received attachment temp file “" - + tmpFile - + "”: " - + e.getMessage()); + logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); } } return outputFile; @@ -2279,7 +2235,6 @@ public class Manager implements Closeable { private InputStream retrieveAttachmentAsStream( SignalServiceAttachmentPointer pointer, File tmpFile ) throws IOException, InvalidMessageException, MissingConfigurationException { - final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver(); return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); } @@ -2289,13 +2244,13 @@ public class Manager implements Closeable { try { try (OutputStream fos = new FileOutputStream(groupsFile)) { DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos); - for (GroupInfo record : account.getGroupStore().getGroups()) { + for (GroupInfo record : getGroups()) { if (record instanceof GroupInfoV1) { GroupInfoV1 groupInfo = (GroupInfoV1) record; - out.write(new DeviceGroup(groupInfo.groupId, + out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), Optional.fromNullable(groupInfo.name), new ArrayList<>(groupInfo.getMembers()), - createGroupAvatarAttachment(groupInfo.groupId), + createGroupAvatarAttachment(groupInfo.getGroupId()), groupInfo.isMember(account.getSelfAddress()), Optional.of(groupInfo.messageExpirationTime), Optional.fromNullable(groupInfo.color), @@ -2321,7 +2276,7 @@ public class Manager implements Closeable { try { Files.delete(groupsFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete groups temp file “" + groupsFile + "”: " + e.getMessage()); + logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage()); } } } @@ -2334,8 +2289,7 @@ public class Manager implements Closeable { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : account.getContactStore().getContacts()) { VerifiedMessage verifiedMessage = null; - JsonIdentityKeyStore.Identity currentIdentity = account.getSignalProtocolStore() - .getIdentity(record.getAddress()); + IdentityInfo currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress()); if (currentIdentity != null) { verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), @@ -2386,7 +2340,7 @@ public class Manager implements Closeable { try { Files.delete(contactsFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete contacts temp file “" + contactsFile + "”: " + e.getMessage()); + logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage()); } } } @@ -2399,9 +2353,9 @@ public class Manager implements Closeable { } } List groupIds = new ArrayList<>(); - for (GroupInfo record : account.getGroupStore().getGroups()) { + for (GroupInfo record : getGroups()) { if (record.isBlocked()) { - groupIds.add(record.groupId); + groupIds.add(record.getGroupId().serialize()); } } sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); @@ -2422,18 +2376,24 @@ public class Manager implements Closeable { } public ContactInfo getContact(String number) { - return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number)); + return account.getContactStore().getContact(Utils.getSignalServiceAddressFromIdentifier(number)); } - public GroupInfo getGroup(byte[] groupId) { - return account.getGroupStore().getGroup(groupId); + public GroupInfo getGroup(GroupId groupId) { + final GroupInfo group = account.getGroupStore().getGroup(groupId); + if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); + ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams)); + account.getGroupStore().updateGroup(group); + } + return group; } - public List getIdentities() { + public List getIdentities() { return account.getSignalProtocolStore().getIdentities(); } - public List getIdentities(String number) throws InvalidNumberException { + public List getIdentities(String number) throws InvalidNumberException { return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number)); } @@ -2445,11 +2405,11 @@ public class Manager implements Closeable { */ public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException { SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); - List ids = account.getSignalProtocolStore().getIdentities(address); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } - for (JsonIdentityKeyStore.Identity id : ids) { + for (IdentityInfo id : ids) { if (!Arrays.equals(id.getIdentityKey().serialize(), fingerprint)) { continue; } @@ -2475,11 +2435,11 @@ public class Manager implements Closeable { */ public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException { SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); - List ids = account.getSignalProtocolStore().getIdentities(address); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } - for (JsonIdentityKeyStore.Identity id : ids) { + for (IdentityInfo id : ids) { if (!safetyNumber.equals(computeSafetyNumber(address, id.getIdentityKey()))) { continue; } @@ -2504,11 +2464,11 @@ public class Manager implements Closeable { */ public boolean trustIdentityAllKeys(String name) { SignalServiceAddress address = resolveSignalServiceAddress(name); - List ids = account.getSignalProtocolStore().getIdentities(address); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } - for (JsonIdentityKeyStore.Identity id : ids) { + for (IdentityInfo id : ids) { if (id.getTrustLevel() == TrustLevel.UNTRUSTED) { account.getSignalProtocolStore() .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); @@ -2526,7 +2486,8 @@ public class Manager implements Closeable { public String computeSafetyNumber( SignalServiceAddress theirAddress, IdentityKey theirIdentityKey ) { - return Utils.computeSafetyNumber(account.getSelfAddress(), + return Utils.computeSafetyNumber(ServiceConfig.capabilities.isUuid(), + account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); @@ -2539,12 +2500,12 @@ public class Manager implements Closeable { public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException { String canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier - : Util.canonicalizeNumber(identifier, account.getUsername()); + : PhoneNumberFormatter.formatNumber(identifier, account.getUsername()); return resolveSignalServiceAddress(canonicalizedNumber); } public SignalServiceAddress resolveSignalServiceAddress(String identifier) { - SignalServiceAddress address = Util.getSignalServiceAddressFromIdentifier(identifier); + SignalServiceAddress address = Utils.getSignalServiceAddressFromIdentifier(identifier); return resolveSignalServiceAddress(address); } @@ -2559,6 +2520,10 @@ public class Manager implements Closeable { @Override public void close() throws IOException { + close(true); + } + + void close(boolean closeAccount) throws IOException { if (messagePipe != null) { messagePipe.shutdown(); messagePipe = null; @@ -2569,7 +2534,10 @@ public class Manager implements Closeable { unidentifiedMessagePipe = null; } - account.close(); + if (closeAccount && account != null) { + account.close(); + } + account = null; } public interface ReceiveMessageHandler { diff --git a/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java deleted file mode 100644 index 8c0e9be0..00000000 --- a/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.asamk.signal.manager; - -import org.whispersystems.util.Base64; - -public class NotAGroupMemberException extends Exception { - - public NotAGroupMemberException(byte[] groupId, String groupName) { - super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")"); - } -} diff --git a/src/main/java/org/asamk/signal/manager/NotRegisteredException.java b/src/main/java/org/asamk/signal/manager/NotRegisteredException.java new file mode 100644 index 00000000..c1b35a1c --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/NotRegisteredException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager; + +public class NotRegisteredException extends Exception { + + public NotRegisteredException() { + super("User is not registered."); + } +} diff --git a/src/main/java/org/asamk/signal/manager/PathConfig.java b/src/main/java/org/asamk/signal/manager/PathConfig.java index c0c9e1e7..d96034df 100644 --- a/src/main/java/org/asamk/signal/manager/PathConfig.java +++ b/src/main/java/org/asamk/signal/manager/PathConfig.java @@ -1,30 +1,34 @@ package org.asamk.signal.manager; +import java.io.File; + public class PathConfig { - private final String dataPath; - private final String attachmentsPath; - private final String avatarsPath; + private final File dataPath; + private final File attachmentsPath; + private final File avatarsPath; - public static PathConfig createDefault(final String settingsPath) { - return new PathConfig(settingsPath + "/data", settingsPath + "/attachments", settingsPath + "/avatars"); + public static PathConfig createDefault(final File settingsPath) { + return new PathConfig(new File(settingsPath, "data"), + new File(settingsPath, "attachments"), + new File(settingsPath, "avatars")); } - private PathConfig(final String dataPath, final String attachmentsPath, final String avatarsPath) { + private PathConfig(final File dataPath, final File attachmentsPath, final File avatarsPath) { this.dataPath = dataPath; this.attachmentsPath = attachmentsPath; this.avatarsPath = avatarsPath; } - public String getDataPath() { + public File getDataPath() { return dataPath; } - public String getAttachmentsPath() { + public File getAttachmentsPath() { return attachmentsPath; } - public String getAvatarsPath() { + public File getAvatarsPath() { return avatarsPath; } } diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index eb70b351..0648c0d3 100644 --- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2020 AsamK and contributors + Copyright (C) 2015-2021 AsamK and contributors 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 @@ -16,7 +16,8 @@ */ package org.asamk.signal.manager; -import org.asamk.signal.storage.SignalAccount; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.util.KeyUtils; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.IdentityKeyPair; @@ -31,6 +32,7 @@ import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; +import java.io.File; import java.io.IOException; import java.util.concurrent.TimeoutException; @@ -45,12 +47,12 @@ public class ProvisioningManager { private final int registrationId; private final String password; - public ProvisioningManager(String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) { + public ProvisioningManager(File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) { this.pathConfig = PathConfig.createDefault(settingsPath); this.serviceConfiguration = serviceConfiguration; this.userAgent = userAgent; - identityKey = KeyHelper.generateIdentityKeyPair(); + identityKey = KeyUtils.generateIdentityKeyPair(); registrationId = KeyHelper.generateRegistrationId(false); password = KeyUtils.createPassword(); final SleepTimer timer = new UptimeSleepTimer(); @@ -70,8 +72,7 @@ public class ProvisioningManager { public String getDeviceLinkUri() throws TimeoutException, IOException { String deviceUuid = accountManager.getNewDeviceUuid(); - return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid, - identityKey.getPublicKey().getPublicKey())); + return new DeviceLinkInfo(deviceUuid, identityKey.getPublicKey().getPublicKey()).createDeviceLinkUri(); } public String finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { @@ -123,8 +124,10 @@ public class ProvisioningManager { m.requestSyncBlocked(); m.requestSyncConfiguration(); - m.saveAccount(); + m.close(false); } + + account.save(); } return username; diff --git a/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/src/main/java/org/asamk/signal/manager/RegistrationManager.java new file mode 100644 index 00000000..e740bb91 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -0,0 +1,194 @@ +/* + Copyright (C) 2015-2021 AsamK and contributors + + 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.manager; + +import org.asamk.signal.manager.helper.PinHelper; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.util.KeyUtils; +import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.util.KeyHelper; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.KbsPinData; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.SleepTimer; +import org.whispersystems.signalservice.api.util.UptimeSleepTimer; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.push.LockedException; +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; +import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +public class RegistrationManager implements AutoCloseable { + + private SignalAccount account; + private final PathConfig pathConfig; + private final SignalServiceConfiguration serviceConfiguration; + private final String userAgent; + + private final SignalServiceAccountManager accountManager; + private final PinHelper pinHelper; + + public RegistrationManager( + SignalAccount account, + PathConfig pathConfig, + SignalServiceConfiguration serviceConfiguration, + String userAgent + ) { + this.account = account; + this.pathConfig = pathConfig; + this.serviceConfiguration = serviceConfiguration; + this.userAgent = userAgent; + + final SleepTimer timer = new UptimeSleepTimer(); + this.accountManager = new SignalServiceAccountManager(serviceConfiguration, new DynamicCredentialsProvider( + // Using empty UUID, because registering doesn't work otherwise + null, + account.getUsername(), + account.getPassword(), + account.getSignalingKey(), + SignalServiceAddress.DEFAULT_DEVICE_ID), userAgent, null, timer); + final KeyBackupService keyBackupService = ServiceConfig.createKeyBackupService(accountManager); + this.pinHelper = new PinHelper(keyBackupService); + } + + public static RegistrationManager init( + String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent + ) throws IOException { + PathConfig pathConfig = PathConfig.createDefault(settingsPath); + + if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { + IdentityKeyPair identityKey = KeyUtils.generateIdentityKeyPair(); + int registrationId = KeyHelper.generateRegistrationId(false); + + ProfileKey profileKey = KeyUtils.createProfileKey(); + SignalAccount account = SignalAccount.create(pathConfig.getDataPath(), + username, + identityKey, + registrationId, + profileKey); + account.save(); + + return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); + } + + SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username); + + return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); + } + + public void register(boolean voiceVerification, String captcha) throws IOException { + if (account.getPassword() == null) { + account.setPassword(KeyUtils.createPassword()); + } + + if (voiceVerification) { + accountManager.requestVoiceVerificationCode(Locale.getDefault(), + Optional.fromNullable(captcha), + Optional.absent()); + } else { + accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent()); + } + + account.setRegistered(false); + account.save(); + } + + public void verifyAccount( + String verificationCode, String pin + ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { + verificationCode = verificationCode.replace("-", ""); + if (account.getSignalingKey() == null) { + account.setSignalingKey(KeyUtils.createSignalingKey()); + } + VerifyAccountResponse response; + try { + response = verifyAccountWithCode(verificationCode, pin, null); + account.setPinMasterKey(null); + } catch (LockedException e) { + if (pin == null) { + throw e; + } + + KbsPinData registrationLockData = pinHelper.getRegistrationLockData(pin, e); + if (registrationLockData == null) { + throw e; + } + + String registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); + try { + response = verifyAccountWithCode(verificationCode, null, registrationLock); + } catch (LockedException _e) { + throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); + } + account.setPinMasterKey(registrationLockData.getMasterKey()); + } + + // TODO response.isStorageCapable() + //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); + + account.setDeviceId(SignalServiceAddress.DEFAULT_DEVICE_ID); + account.setMultiDevice(false); + account.setRegistered(true); + account.setUuid(UuidUtil.parseOrNull(response.getUuid())); + account.setRegistrationLockPin(pin); + account.getSignalProtocolStore() + .saveIdentity(account.getSelfAddress(), + account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(), + TrustLevel.TRUSTED_VERIFIED); + + try (Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent)) { + + m.refreshPreKeys(); + + m.close(false); + } + + account.save(); + } + + private VerifyAccountResponse verifyAccountWithCode( + final String verificationCode, final String legacyPin, final String registrationLock + ) throws IOException { + return accountManager.verifyAccountWithCode(verificationCode, + account.getSignalingKey(), + account.getSignalProtocolStore().getLocalRegistrationId(), + true, + legacyPin, + registrationLock, + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), + ServiceConfig.capabilities, + account.isDiscoverableByPhoneNumber()); + } + + @Override + public void close() throws Exception { + if (account != null) { + account.close(); + account = null; + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java index 5721b166..b6d4f4fd 100644 --- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -1,7 +1,13 @@ package org.asamk.signal.manager; +import org.bouncycastle.util.encoders.Hex; import org.signal.zkgroup.ServerPublicParams; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; @@ -10,14 +16,13 @@ import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupSe import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; -import org.whispersystems.util.Base64; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.util.Collections; +import java.util.Base64; import java.util.List; import java.util.Map; @@ -26,7 +31,8 @@ import okhttp3.Interceptor; public class ServiceConfig { - final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; + final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() + .decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"); final static int PREKEY_MINIMUM_COUNT = 20; final static int PREKEY_BATCH_SIZE = 100; final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; @@ -35,6 +41,11 @@ public class ServiceConfig { final static String CDS_MRENCLAVE = "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15"; + final static String KEY_BACKUP_ENCLAVE_NAME = "fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe"; + final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode( + "fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe"); + final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87"; + private final static String URL = "https://textsecure-service.whispersystems.org"; private final static String CDN_URL = "https://cdn.signal.org"; private final static String CDN2_URL = "https://cdn2.signal.org"; @@ -46,18 +57,12 @@ public class ServiceConfig { private final static Optional dns = Optional.absent(); - private final static String zkGroupServerPublicParamsHex = "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0="; - private final static byte[] zkGroupServerPublicParams; + private final static byte[] zkGroupServerPublicParams = Base64.getDecoder() + .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0="); static final AccountAttributes.Capabilities capabilities; static { - try { - zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex); - } catch (IOException e) { - throw new AssertionError(e); - } - boolean zkGroupAvailable; try { new ServerPublicParams(zkGroupServerPublicParams); @@ -74,7 +79,7 @@ public class ServiceConfig { .header("User-Agent", userAgent) .build()); - final List interceptors = Collections.singletonList(userAgentInterceptor); + final List interceptors = List.of(userAgentInterceptor); return new SignalServiceConfiguration(new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, @@ -88,6 +93,10 @@ public class ServiceConfig { zkGroupServerPublicParams); } + public static AccountAttributes.Capabilities getCapabilities() { + return capabilities; + } + static KeyStore getIasKeyStore() { try { TrustStore contactTrustStore = IAS_TRUST_STORE; @@ -102,6 +111,24 @@ public class ServiceConfig { } } + static KeyBackupService createKeyBackupService(SignalServiceAccountManager accountManager) { + KeyStore keyStore = ServiceConfig.getIasKeyStore(); + + return accountManager.getKeyBackupService(keyStore, + ServiceConfig.KEY_BACKUP_ENCLAVE_NAME, + ServiceConfig.KEY_BACKUP_SERVICE_ID, + ServiceConfig.KEY_BACKUP_MRENCLAVE, + 10); + } + + static ECPublicKey getUnidentifiedSenderTrustRoot() { + try { + return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); + } catch (InvalidKeyException e) { + throw new AssertionError(e); + } + } + private static Map makeSignalCdnUrlMapFor( SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls ) { diff --git a/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java b/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java index a07c455b..d506f0c6 100644 --- a/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java +++ b/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java @@ -1,11 +1,13 @@ package org.asamk.signal.manager; +import java.io.File; + public class UserAlreadyExists extends Exception { private final String username; - private final String fileName; + private final File fileName; - public UserAlreadyExists(String username, String fileName) { + public UserAlreadyExists(String username, File fileName) { this.username = username; this.fileName = fileName; } @@ -14,7 +16,7 @@ public class UserAlreadyExists extends Exception { return username; } - public String getFileName() { + public File getFileName() { return fileName; } } diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java deleted file mode 100644 index 0a815ea9..00000000 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ /dev/null @@ -1,304 +0,0 @@ -package org.asamk.signal.manager; - -import org.signal.libsignal.metadata.certificate.CertificateValidator; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.ecc.Curve; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.fingerprint.Fingerprint; -import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.StreamDetails; -import org.whispersystems.signalservice.api.util.UuidUtil; -import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; -import org.whispersystems.util.Base64; - -import java.io.BufferedInputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URLConnection; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static org.whispersystems.signalservice.internal.util.Util.isEmpty; - -class Utils { - - static List getSignalServiceAttachments(List attachments) throws AttachmentInvalidException { - List signalServiceAttachments = null; - if (attachments != null) { - signalServiceAttachments = new ArrayList<>(attachments.size()); - for (String attachment : attachments) { - try { - signalServiceAttachments.add(createAttachment(new File(attachment))); - } catch (IOException e) { - throw new AttachmentInvalidException(attachment, e); - } - } - } - return signalServiceAttachments; - } - - static String getFileMimeType(File file, String defaultMimeType) throws IOException { - String mime = Files.probeContentType(file.toPath()); - if (mime == null) { - try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { - mime = URLConnection.guessContentTypeFromStream(bufferedStream); - } - } - if (mime == null) { - return defaultMimeType; - } - return mime; - } - - static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { - InputStream attachmentStream = new FileInputStream(attachmentFile); - final long attachmentSize = attachmentFile.length(); - final String mime = getFileMimeType(attachmentFile, "application/octet-stream"); - // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option - final long uploadTimestamp = System.currentTimeMillis(); - Optional preview = Optional.absent(); - Optional caption = Optional.absent(); - Optional blurHash = Optional.absent(); - final Optional resumableUploadSpec = Optional.absent(); - return new SignalServiceAttachmentStream(attachmentStream, - mime, - attachmentSize, - Optional.of(attachmentFile.getName()), - false, - false, - preview, - 0, - 0, - uploadTimestamp, - caption, - blurHash, - null, - null, - resumableUploadSpec); - } - - static StreamDetails createStreamDetailsFromFile(File file) throws IOException { - InputStream stream = new FileInputStream(file); - final long size = file.length(); - String mime = Files.probeContentType(file.toPath()); - if (mime == null) { - mime = "application/octet-stream"; - } - return new StreamDetails(stream, mime, size); - } - - static CertificateValidator getCertificateValidator() { - try { - ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), - 0); - return new CertificateValidator(unidentifiedSenderTrustRoot); - } catch (InvalidKeyException | IOException e) { - throw new AssertionError(e); - } - } - - private static Map getQueryMap(String query) { - String[] params = query.split("&"); - Map map = new HashMap<>(); - for (String param : params) { - final String[] paramParts = param.split("="); - String name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8); - String value = URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8); - map.put(name, value); - } - return map; - } - - static String createDeviceLinkUri(DeviceLinkInfo info) { - return "tsdevice:/?uuid=" - + URLEncoder.encode(info.deviceIdentifier, StandardCharsets.UTF_8) - + "&pub_key=" - + URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()), - StandardCharsets.UTF_8); - } - - static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException { - Map query = getQueryMap(linkUri.getRawQuery()); - String deviceIdentifier = query.get("uuid"); - String publicKeyEncoded = query.get("pub_key"); - - if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) { - throw new RuntimeException("Invalid device link uri"); - } - - ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); - - return new DeviceLinkInfo(deviceIdentifier, deviceKey); - } - - static SignalServiceEnvelope loadEnvelope(File file) throws IOException { - try (FileInputStream f = new FileInputStream(file)) { - DataInputStream in = new DataInputStream(f); - int version = in.readInt(); - if (version > 4) { - return null; - } - int type = in.readInt(); - String source = in.readUTF(); - UUID sourceUuid = null; - if (version >= 3) { - sourceUuid = UuidUtil.parseOrNull(in.readUTF()); - } - int sourceDevice = in.readInt(); - if (version == 1) { - // read legacy relay field - in.readUTF(); - } - long timestamp = in.readLong(); - byte[] content = null; - int contentLen = in.readInt(); - if (contentLen > 0) { - content = new byte[contentLen]; - in.readFully(content); - } - byte[] legacyMessage = null; - int legacyMessageLen = in.readInt(); - if (legacyMessageLen > 0) { - legacyMessage = new byte[legacyMessageLen]; - in.readFully(legacyMessage); - } - long serverReceivedTimestamp = 0; - String uuid = null; - if (version >= 2) { - serverReceivedTimestamp = in.readLong(); - uuid = in.readUTF(); - if ("".equals(uuid)) { - uuid = null; - } - } - long serverDeliveredTimestamp = 0; - if (version >= 4) { - serverDeliveredTimestamp = in.readLong(); - } - Optional addressOptional = sourceUuid == null && source.isEmpty() - ? Optional.absent() - : Optional.of(new SignalServiceAddress(sourceUuid, source)); - return new SignalServiceEnvelope(type, - addressOptional, - sourceDevice, - timestamp, - legacyMessage, - content, - serverReceivedTimestamp, - serverDeliveredTimestamp, - uuid); - } - } - - static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { - try (FileOutputStream f = new FileOutputStream(file)) { - try (DataOutputStream out = new DataOutputStream(f)) { - out.writeInt(4); // version - out.writeInt(envelope.getType()); - out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : ""); - out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : ""); - out.writeInt(envelope.getSourceDevice()); - out.writeLong(envelope.getTimestamp()); - if (envelope.hasContent()) { - out.writeInt(envelope.getContent().length); - out.write(envelope.getContent()); - } else { - out.writeInt(0); - } - if (envelope.hasLegacyMessage()) { - out.writeInt(envelope.getLegacyMessage().length); - out.write(envelope.getLegacyMessage()); - } else { - out.writeInt(0); - } - out.writeLong(envelope.getServerReceivedTimestamp()); - String uuid = envelope.getUuid(); - out.writeUTF(uuid == null ? "" : uuid); - out.writeLong(envelope.getServerDeliveredTimestamp()); - } - } - } - - static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException { - InputStream input = stream.getInputStream(); - - try (OutputStream output = new FileOutputStream(outputFile)) { - byte[] buffer = new byte[4096]; - int read; - - while ((read = input.read(buffer)) != -1) { - output.write(buffer, 0, read); - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - return null; - } - return outputFile; - } - - static String computeSafetyNumber( - SignalServiceAddress ownAddress, - IdentityKey ownIdentityKey, - SignalServiceAddress theirAddress, - IdentityKey theirIdentityKey - ) { - int version; - byte[] ownId; - byte[] theirId; - - if (ServiceConfig.capabilities.isUuid() && ownAddress.getUuid().isPresent() && theirAddress.getUuid() - .isPresent()) { - // Version 2: UUID user - version = 2; - ownId = UuidUtil.toByteArray(ownAddress.getUuid().get()); - theirId = UuidUtil.toByteArray(theirAddress.getUuid().get()); - } else { - // Version 1: E164 user - version = 1; - if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) { - return "INVALID ID"; - } - ownId = ownAddress.getNumber().get().getBytes(); - theirId = theirAddress.getNumber().get().getBytes(); - } - - Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, - ownId, - ownIdentityKey, - theirId, - theirIdentityKey); - return fingerprint.getDisplayableFingerprint().getDisplayText(); - } - - static class DeviceLinkInfo { - - final String deviceIdentifier; - final ECPublicKey deviceKey; - - DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) { - this.deviceIdentifier = deviceIdentifier; - this.deviceKey = deviceKey; - } - } -} diff --git a/src/main/java/org/asamk/signal/manager/groups/GroupId.java b/src/main/java/org/asamk/signal/manager/groups/GroupId.java new file mode 100644 index 00000000..9a15de65 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/groups/GroupId.java @@ -0,0 +1,63 @@ +package org.asamk.signal.manager.groups; + +import org.whispersystems.util.Base64; + +import java.util.Arrays; + +public abstract class GroupId { + + private final byte[] id; + + public static GroupIdV1 v1(byte[] id) { + return new GroupIdV1(id); + } + + public static GroupIdV2 v2(byte[] id) { + return new GroupIdV2(id); + } + + public static GroupId unknownVersion(byte[] id) { + if (id.length == 16) { + return new GroupIdV1(id); + } else if (id.length == 32) { + return new GroupIdV2(id); + } + + throw new AssertionError("Invalid group id of size " + id.length); + } + + public static GroupId fromBase64(String id) throws GroupIdFormatException { + try { + return unknownVersion(java.util.Base64.getDecoder().decode(id)); + } catch (Throwable e) { + throw new GroupIdFormatException(id, e); + } + } + + public GroupId(final byte[] id) { + this.id = id; + } + + public byte[] serialize() { + return id; + } + + public String toBase64() { + return Base64.encodeBytes(id); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final GroupId groupId = (GroupId) o; + + return Arrays.equals(id, groupId.id); + } + + @Override + public int hashCode() { + return Arrays.hashCode(id); + } +} diff --git a/src/main/java/org/asamk/signal/manager/groups/GroupIdFormatException.java b/src/main/java/org/asamk/signal/manager/groups/GroupIdFormatException.java new file mode 100644 index 00000000..8050da22 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/groups/GroupIdFormatException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.groups; + +public class GroupIdFormatException extends Exception { + + public GroupIdFormatException(String groupId, Throwable e) { + super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage(), e); + } +} diff --git a/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java b/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java new file mode 100644 index 00000000..237a34b6 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java @@ -0,0 +1,14 @@ +package org.asamk.signal.manager.groups; + +import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes; + +public class GroupIdV1 extends GroupId { + + public static GroupIdV1 createRandom() { + return new GroupIdV1(getSecretBytes(16)); + } + + public GroupIdV1(final byte[] id) { + super(id); + } +} diff --git a/src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java b/src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java new file mode 100644 index 00000000..913a9e93 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java @@ -0,0 +1,14 @@ +package org.asamk.signal.manager.groups; + +import java.util.Base64; + +public class GroupIdV2 extends GroupId { + + public static GroupIdV2 fromBase64(String groupId) { + return new GroupIdV2(Base64.getDecoder().decode(groupId)); + } + + public GroupIdV2(final byte[] id) { + super(id); + } +} diff --git a/src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java b/src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java new file mode 100644 index 00000000..bf9e0e55 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java @@ -0,0 +1,140 @@ +package org.asamk.signal.manager.groups; + +import com.google.protobuf.ByteString; + +import org.signal.storageservice.protos.groups.GroupInviteLink; +import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.whispersystems.util.Base64UrlSafe; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +public final class GroupInviteLinkUrl { + + private static final String GROUP_URL_HOST = "signal.group"; + private static final String GROUP_URL_PREFIX = "https://" + GROUP_URL_HOST + "/#"; + + private final GroupMasterKey groupMasterKey; + private final GroupLinkPassword password; + private final String url; + + public static GroupInviteLinkUrl forGroup(GroupMasterKey groupMasterKey, DecryptedGroup group) { + return new GroupInviteLinkUrl(groupMasterKey, + GroupLinkPassword.fromBytes(group.getInviteLinkPassword().toByteArray())); + } + + public static boolean isGroupLink(String urlString) { + return getGroupUrl(urlString) != null; + } + + /** + * @return null iff not a group url. + * @throws InvalidGroupLinkException If group url, but cannot be parsed. + */ + public static GroupInviteLinkUrl fromUri(String urlString) throws InvalidGroupLinkException, UnknownGroupLinkVersionException { + URI uri = getGroupUrl(urlString); + + if (uri == null) { + return null; + } + + try { + if (!"/".equals(uri.getPath()) && uri.getPath().length() > 0) { + throw new InvalidGroupLinkException("No path was expected in uri"); + } + + String encoding = uri.getFragment(); + + if (encoding == null || encoding.length() == 0) { + throw new InvalidGroupLinkException("No reference was in the uri"); + } + + byte[] bytes = Base64UrlSafe.decodePaddingAgnostic(encoding); + GroupInviteLink groupInviteLink = GroupInviteLink.parseFrom(bytes); + + switch (groupInviteLink.getContentsCase()) { + case V1CONTENTS: { + GroupInviteLink.GroupInviteLinkContentsV1 groupInviteLinkContentsV1 = groupInviteLink.getV1Contents(); + GroupMasterKey groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.getGroupMasterKey() + .toByteArray()); + GroupLinkPassword password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.getInviteLinkPassword() + .toByteArray()); + + return new GroupInviteLinkUrl(groupMasterKey, password); + } + default: + throw new UnknownGroupLinkVersionException("Url contains no known group link content"); + } + } catch (InvalidInputException | IOException e) { + throw new InvalidGroupLinkException(e); + } + } + + /** + * @return {@link URI} if the host name matches. + */ + private static URI getGroupUrl(String urlString) { + try { + URI url = new URI(urlString); + + if (!"https".equalsIgnoreCase(url.getScheme()) && !"sgnl".equalsIgnoreCase(url.getScheme())) { + return null; + } + + return GROUP_URL_HOST.equalsIgnoreCase(url.getHost()) ? url : null; + } catch (URISyntaxException e) { + return null; + } + } + + private GroupInviteLinkUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) { + this.groupMasterKey = groupMasterKey; + this.password = password; + this.url = createUrl(groupMasterKey, password); + } + + protected static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) { + GroupInviteLink groupInviteLink = GroupInviteLink.newBuilder() + .setV1Contents(GroupInviteLink.GroupInviteLinkContentsV1.newBuilder() + .setGroupMasterKey(ByteString.copyFrom(groupMasterKey.serialize())) + .setInviteLinkPassword(ByteString.copyFrom(password.serialize()))) + .build(); + + String encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.toByteArray()); + + return GROUP_URL_PREFIX + encoding; + } + + public String getUrl() { + return url; + } + + public GroupMasterKey getGroupMasterKey() { + return groupMasterKey; + } + + public GroupLinkPassword getPassword() { + return password; + } + + public final static class InvalidGroupLinkException extends Exception { + + public InvalidGroupLinkException(String message) { + super(message); + } + + public InvalidGroupLinkException(Throwable cause) { + super(cause); + } + } + + public final static class UnknownGroupLinkVersionException extends Exception { + + public UnknownGroupLinkVersionException(String message) { + super(message); + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java b/src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java new file mode 100644 index 00000000..7edc7afb --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java @@ -0,0 +1,42 @@ +package org.asamk.signal.manager.groups; + +import org.asamk.signal.manager.util.KeyUtils; + +import java.util.Arrays; + +public final class GroupLinkPassword { + + private static final int SIZE = 16; + + private final byte[] bytes; + + public static GroupLinkPassword createNew() { + return new GroupLinkPassword(KeyUtils.getSecretBytes(SIZE)); + } + + public static GroupLinkPassword fromBytes(byte[] bytes) { + return new GroupLinkPassword(bytes); + } + + private GroupLinkPassword(byte[] bytes) { + this.bytes = bytes; + } + + public byte[] serialize() { + return bytes.clone(); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof GroupLinkPassword)) { + return false; + } + + return Arrays.equals(bytes, ((GroupLinkPassword) other).bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } +} diff --git a/src/main/java/org/asamk/signal/manager/groups/GroupNotFoundException.java b/src/main/java/org/asamk/signal/manager/groups/GroupNotFoundException.java new file mode 100644 index 00000000..0fc0c444 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/groups/GroupNotFoundException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.groups; + +public class GroupNotFoundException extends Exception { + + public GroupNotFoundException(GroupId groupId) { + super("Group not found: " + groupId.toBase64()); + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupUtils.java b/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java similarity index 52% rename from src/main/java/org/asamk/signal/manager/GroupUtils.java rename to src/main/java/org/asamk/signal/manager/groups/GroupUtils.java index 0d192002..f56639e3 100644 --- a/src/main/java/org/asamk/signal/manager/GroupUtils.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java @@ -1,14 +1,15 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.groups; -import org.asamk.signal.storage.groups.GroupInfo; -import org.asamk.signal.storage.groups.GroupInfoV1; -import org.asamk.signal.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupSecretParams; import org.whispersystems.libsignal.kdf.HKDFv3; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; public class GroupUtils { @@ -18,7 +19,7 @@ public class GroupUtils { ) { if (groupInfo instanceof GroupInfoV1) { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) - .withId(groupInfo.groupId) + .withId(groupInfo.getGroupId().serialize()) .build(); messageBuilder.asGroupMessage(group); } else { @@ -30,14 +31,34 @@ public class GroupUtils { } } - public static byte[] getGroupId(GroupMasterKey groupMasterKey) { - final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - return groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); + public static GroupId getGroupId(SignalServiceGroupContext context) { + if (context.getGroupV1().isPresent()) { + return GroupId.v1(context.getGroupV1().get().getGroupId()); + } else if (context.getGroupV2().isPresent()) { + return getGroupIdV2(context.getGroupV2().get().getMasterKey()); + } else { + return null; + } } - public static GroupMasterKey deriveV2MigrationMasterKey(byte[] groupId) { + public static GroupIdV2 getGroupIdV2(GroupSecretParams groupSecretParams) { + return GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier().serialize()); + } + + public static GroupIdV2 getGroupIdV2(GroupMasterKey groupMasterKey) { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + return getGroupIdV2(groupSecretParams); + } + + public static GroupIdV2 getGroupIdV2(GroupIdV1 groupIdV1) { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(deriveV2MigrationMasterKey( + groupIdV1)); + return getGroupIdV2(groupSecretParams); + } + + private static GroupMasterKey deriveV2MigrationMasterKey(GroupIdV1 groupIdV1) { try { - return new GroupMasterKey(new HKDFv3().deriveSecrets(groupId, + return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1.serialize(), "GV2 Migration".getBytes(), GroupMasterKey.SIZE)); } catch (InvalidInputException e) { diff --git a/src/main/java/org/asamk/signal/manager/groups/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/manager/groups/NotAGroupMemberException.java new file mode 100644 index 00000000..08cbcacd --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/groups/NotAGroupMemberException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.groups; + +public class NotAGroupMemberException extends Exception { + + public NotAGroupMemberException(GroupId groupId, String groupName) { + super("User is not a member in group: " + groupName + " (" + groupId.toBase64() + ")"); + } +} diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 1f7e69e3..8a2320e0 100644 --- a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -2,12 +2,18 @@ package org.asamk.signal.manager.helper; import com.google.protobuf.InvalidProtocolBufferException; -import org.asamk.signal.storage.groups.GroupInfoV2; -import org.asamk.signal.util.IOUtils; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupLinkPassword; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.storage.profiles.SignalProfile; +import org.asamk.signal.manager.util.IOUtils; +import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; +import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.VerificationFailedException; @@ -15,10 +21,13 @@ import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupSecretParams; import org.signal.zkgroup.groups.UuidCiphertext; import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.GroupCandidate; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; @@ -38,6 +47,8 @@ import java.util.stream.Collectors; public class GroupHelper { + final static Logger logger = LoggerFactory.getLogger(GroupHelper.class); + private final ProfileKeyCredentialProvider profileKeyCredentialProvider; private final ProfileProvider profileProvider; @@ -66,6 +77,27 @@ public class GroupHelper { this.groupAuthorizationProvider = groupAuthorizationProvider; } + public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) { + try { + final GroupsV2AuthorizationString groupsV2AuthorizationString = groupAuthorizationProvider.getAuthorizationForToday( + groupSecretParams); + return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString); + } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { + logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); + return null; + } + } + + public DecryptedGroupJoinInfo getDecryptedGroupJoinInfo( + GroupMasterKey groupMasterKey, GroupLinkPassword password + ) throws IOException, GroupLinkNotActiveException { + GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + + return groupsV2Api.getGroupJoinInfo(groupSecretParams, + Optional.fromNullable(password).transform(GroupLinkPassword::serialize), + groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams)); + } + public GroupInfoV2 createGroupV2( String name, Collection members, String avatarFile ) throws IOException { @@ -84,15 +116,15 @@ public class GroupHelper { groupsV2Api.putNewGroup(newGroup, groupAuthForToday); decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday); } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { - System.err.println("Failed to create V2 group: " + e.getMessage()); + logger.warn("Failed to create V2 group: {}", e.getMessage()); return null; } if (decryptedGroup == null) { - System.err.println("Failed to create V2 group!"); + logger.warn("Failed to create V2 group, unknown error!"); return null; } - final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); + final GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams); final GroupMasterKey masterKey = groupSecretParams.getMasterKey(); GroupInfoV2 g = new GroupInfoV2(groupId, masterKey); g.setGroup(decryptedGroup); @@ -114,7 +146,7 @@ public class GroupHelper { final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential( selfAddressProvider.getSelfAddress()); if (profileKeyCredential == null) { - System.err.println("Cannot create a V2 group as self does not have a versioned profile"); + logger.warn("Cannot create a V2 group as self does not have a versioned profile"); return null; } @@ -138,22 +170,23 @@ public class GroupHelper { } private boolean areMembersValid(final Collection members) { - final int noUuidCapability = members.stream() + final Set noUuidCapability = members.stream() .filter(address -> !address.getUuid().isPresent()) - .collect(Collectors.toUnmodifiableSet()) - .size(); - if (noUuidCapability > 0) { - System.err.println("Cannot create a V2 group as " + noUuidCapability + " members don't have a UUID."); + .map(SignalServiceAddress::getLegacyIdentifier) + .collect(Collectors.toSet()); + if (noUuidCapability.size() > 0) { + logger.warn("Cannot create a V2 group as some members don't have a UUID: {}", + String.join(", ", noUuidCapability)); return false; } - final int noGv2Capability = members.stream() + final Set noGv2Capability = members.stream() .map(profileProvider::getProfile) .filter(profile -> profile != null && !profile.getCapabilities().gv2) - .collect(Collectors.toUnmodifiableSet()) - .size(); - if (noGv2Capability > 0) { - System.err.println("Cannot create a V2 group as " + noGv2Capability + " members don't support Groups V2."); + .collect(Collectors.toSet()); + if (noGv2Capability.size() > 0) { + logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}", + noGv2Capability.stream().map(SignalProfile::getName).collect(Collectors.joining(", "))); return false; } @@ -192,7 +225,9 @@ public class GroupHelper { final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); - if (!areMembersValid(newMembers)) return null; + if (!areMembersValid(newMembers)) { + throw new IOException("Failed to update group"); + } Set candidates = newMembers.stream() .map(member -> new GroupCandidate(member.getUuid().get(), @@ -223,6 +258,32 @@ public class GroupHelper { } } + public GroupChange joinGroup( + GroupMasterKey groupMasterKey, + GroupLinkPassword groupLinkPassword, + DecryptedGroupJoinInfo decryptedGroupJoinInfo + ) throws IOException { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); + + final SignalServiceAddress selfAddress = this.selfAddressProvider.getSelfAddress(); + final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential( + selfAddress); + if (profileKeyCredential == null) { + throw new IOException("Cannot join a V2 group as self does not have a versioned profile"); + } + + boolean requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink() + == AccessControl.AccessRequired.ADMINISTRATOR; + GroupChange.Actions.Builder change = requestToJoin + ? groupOperations.createGroupJoinRequest(profileKeyCredential) + : groupOperations.createGroupJoinDirect(profileKeyCredential); + + change.setSourceUuid(UuidUtil.toByteString(selfAddress.getUuid().get())); + + return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword); + } + public Pair acceptInvite(GroupInfoV2 groupInfoV2) throws IOException { final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); @@ -284,13 +345,27 @@ public class GroupHelper { throw new IOException(e); } - GroupChange signedGroupChange = groupsV2Api.patchGroup(change.build(), + GroupChange signedGroupChange = groupsV2Api.patchGroup(changeActions, groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams), Optional.absent()); return new Pair<>(decryptedGroupState, signedGroupChange); } + private GroupChange commitChange( + GroupSecretParams groupSecretParams, + int currentRevision, + GroupChange.Actions.Builder change, + GroupLinkPassword password + ) throws IOException { + final int nextRevision = currentRevision + 1; + final GroupChange.Actions changeActions = change.setRevision(nextRevision).build(); + + return groupsV2Api.patchGroup(changeActions, + groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams), + Optional.fromNullable(password).transform(GroupLinkPassword::serialize)); + } + public DecryptedGroup getUpdatedDecryptedGroup( DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey ) { diff --git a/src/main/java/org/asamk/signal/manager/helper/PinHelper.java b/src/main/java/org/asamk/signal/manager/helper/PinHelper.java new file mode 100644 index 00000000..b4fa04c4 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/helper/PinHelper.java @@ -0,0 +1,89 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.util.PinHashing; +import org.whispersystems.signalservice.api.KbsPinData; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; +import org.whispersystems.signalservice.api.kbs.HashedPin; +import org.whispersystems.signalservice.api.kbs.MasterKey; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import org.whispersystems.signalservice.internal.push.LockedException; + +import java.io.IOException; + +public class PinHelper { + + private final KeyBackupService keyBackupService; + + public PinHelper(final KeyBackupService keyBackupService) { + this.keyBackupService = keyBackupService; + } + + public void setRegistrationLockPin( + String pin, MasterKey masterKey + ) throws IOException, UnauthenticatedResponseException { + final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); + final HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession); + + pinChangeSession.setPin(hashedPin, masterKey); + pinChangeSession.enableRegistrationLock(masterKey); + } + + public void removeRegistrationLockPin() throws IOException, UnauthenticatedResponseException { + final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); + pinChangeSession.disableRegistrationLock(); + pinChangeSession.removePin(); + } + + public KbsPinData getRegistrationLockData( + String pin, LockedException e + ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { + String basicStorageCredentials = e.getBasicStorageCredentials(); + if (basicStorageCredentials == null) { + return null; + } + + return getRegistrationLockData(pin, basicStorageCredentials); + } + + private KbsPinData getRegistrationLockData( + String pin, String basicStorageCredentials + ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { + TokenResponse tokenResponse = keyBackupService.getToken(basicStorageCredentials); + if (tokenResponse == null || tokenResponse.getTries() == 0) { + throw new IOException("KBS Account locked"); + } + + KbsPinData registrationLockData = restoreMasterKey(pin, basicStorageCredentials, tokenResponse); + if (registrationLockData == null) { + throw new AssertionError("Failed to restore master key"); + } + return registrationLockData; + } + + private KbsPinData restoreMasterKey( + String pin, String basicStorageCredentials, TokenResponse tokenResponse + ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { + if (pin == null) return null; + + if (basicStorageCredentials == null) { + throw new AssertionError("Cannot restore KBS key, no storage credentials supplied"); + } + + KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials, + tokenResponse); + + try { + HashedPin hashedPin = PinHashing.hashPin(pin, session); + KbsPinData kbsData = session.restorePin(hashedPin); + if (kbsData == null) { + throw new AssertionError("Null not expected"); + } + return kbsData; + } catch (UnauthenticatedResponseException e) { + throw new IOException(e); + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index c81e2ff7..60c47d8b 100644 --- a/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -95,7 +95,17 @@ public final class ProfileHelper { ? unidentifiedPipe : messagePipeProvider.getMessagePipe(false); if (pipe != null) { - return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType); + try { + return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType); + } catch (NoClassDefFoundError e) { + // Native zkgroup lib not available for ProfileKey + if (!address.getNumber().isPresent()) { + throw new NotFoundException("Can't request profile without number"); + } + SignalServiceAddress addressWithoutUuid = new SignalServiceAddress(Optional.absent(), + address.getNumber()); + return pipe.getProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType); + } } throw new IOException("No pipe available!"); @@ -106,9 +116,18 @@ public final class ProfileHelper { Optional profileKey, Optional unidentifiedAccess, SignalServiceProfile.RequestType requestType - ) { + ) throws NotFoundException { SignalServiceMessageReceiver receiver = messageReceiverProvider.getMessageReceiver(); - return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType); + try { + return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType); + } catch (NoClassDefFoundError e) { + // Native zkgroup lib not available for ProfileKey + if (!address.getNumber().isPresent()) { + throw new NotFoundException("Can't request profile without number"); + } + SignalServiceAddress addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber()); + return receiver.retrieveProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType); + } } private Optional getUnidentifiedAccess(SignalServiceAddress recipient) { diff --git a/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java b/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java index 1ff4cb05..c16b5e0d 100644 --- a/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java +++ b/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java @@ -1,6 +1,6 @@ package org.asamk.signal.manager.helper; -import org.asamk.signal.storage.profiles.SignalProfile; +import org.asamk.signal.manager.storage.profiles.SignalProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; public interface ProfileProvider { diff --git a/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java index 97331cf3..3930154c 100644 --- a/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java @@ -1,6 +1,6 @@ package org.asamk.signal.manager.helper; -import org.asamk.signal.storage.profiles.SignalProfile; +import org.asamk.signal.manager.storage.profiles.SignalProfile; import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.util.guava.Optional; @@ -36,7 +36,7 @@ public class UnidentifiedAccessHelper { this.senderCertificateProvider = senderCertificateProvider; } - public byte[] getSelfUnidentifiedAccessKey() { + private byte[] getSelfUnidentifiedAccessKey() { return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey()); } diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java similarity index 71% rename from src/main/java/org/asamk/signal/storage/SignalAccount.java rename to src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 4f9d8628..6d592573 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage; +package org.asamk.signal.manager.storage; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; @@ -10,29 +10,36 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.asamk.signal.storage.contacts.ContactInfo; -import org.asamk.signal.storage.contacts.JsonContactsStore; -import org.asamk.signal.storage.groups.GroupInfo; -import org.asamk.signal.storage.groups.GroupInfoV1; -import org.asamk.signal.storage.groups.JsonGroupStore; -import org.asamk.signal.storage.profiles.ProfileStore; -import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; -import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; -import org.asamk.signal.storage.protocol.RecipientStore; -import org.asamk.signal.storage.protocol.SessionInfo; -import org.asamk.signal.storage.protocol.SignalServiceAddressResolver; -import org.asamk.signal.storage.stickers.StickerStore; -import org.asamk.signal.storage.threads.LegacyJsonThreadStore; -import org.asamk.signal.storage.threads.ThreadInfo; -import org.asamk.signal.util.IOUtils; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.storage.contacts.ContactInfo; +import org.asamk.signal.manager.storage.contacts.JsonContactsStore; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.groups.JsonGroupStore; +import org.asamk.signal.manager.storage.messageCache.MessageCache; +import org.asamk.signal.manager.storage.profiles.ProfileStore; +import org.asamk.signal.manager.storage.protocol.IdentityInfo; +import org.asamk.signal.manager.storage.protocol.JsonSignalProtocolStore; +import org.asamk.signal.manager.storage.protocol.RecipientStore; +import org.asamk.signal.manager.storage.protocol.SessionInfo; +import org.asamk.signal.manager.storage.protocol.SignalServiceAddressResolver; +import org.asamk.signal.manager.storage.stickers.StickerStore; +import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore; +import org.asamk.signal.manager.storage.threads.ThreadInfo; +import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.KeyUtils; +import org.asamk.signal.manager.util.Utils; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Medium; import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; +import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.util.Base64; @@ -52,6 +59,8 @@ import java.util.stream.Collectors; public class SignalAccount implements Closeable { + final static Logger logger = LoggerFactory.getLogger(SignalAccount.class); + private final ObjectMapper jsonProcessor = new ObjectMapper(); private final FileChannel fileChannel; private final FileLock lock; @@ -61,6 +70,7 @@ public class SignalAccount implements Closeable { private boolean isMultiDevice = false; private String password; private String registrationLockPin; + private MasterKey pinMasterKey; private String signalingKey; private ProfileKey profileKey; private int preKeyIdOffset; @@ -75,6 +85,8 @@ public class SignalAccount implements Closeable { private ProfileStore profileStore; private StickerStore stickerStore; + private MessageCache messageCache; + private SignalAccount(final FileChannel fileChannel, final FileLock lock) { this.fileChannel = fileChannel; this.lock = lock; @@ -85,12 +97,14 @@ public class SignalAccount implements Closeable { jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } - public static SignalAccount load(String dataPath, String username) throws IOException { - final String fileName = getFileName(dataPath, username); + public static SignalAccount load(File dataPath, String username) throws IOException { + final File fileName = getFileName(dataPath, username); final Pair pair = openFileChannel(fileName); try { SignalAccount account = new SignalAccount(pair.first(), pair.second()); account.load(dataPath); + account.migrateLegacyConfigs(); + return account; } catch (Throwable e) { pair.second().close(); @@ -100,11 +114,11 @@ public class SignalAccount implements Closeable { } public static SignalAccount create( - String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey + File dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey ) throws IOException { IOUtils.createPrivateDirectories(dataPath); - String fileName = getFileName(dataPath, username); - if (!new File(fileName).exists()) { + File fileName = getFileName(dataPath, username); + if (!fileName.exists()) { IOUtils.createPrivateFile(fileName); } @@ -119,13 +133,16 @@ public class SignalAccount implements Closeable { account.recipientStore = new RecipientStore(); account.profileStore = new ProfileStore(); account.stickerStore = new StickerStore(); + + account.messageCache = new MessageCache(getMessageCachePath(dataPath, username)); + account.registered = false; return account; } public static SignalAccount createLinkedAccount( - String dataPath, + File dataPath, String username, UUID uuid, String password, @@ -136,8 +153,8 @@ public class SignalAccount implements Closeable { ProfileKey profileKey ) throws IOException { IOUtils.createPrivateDirectories(dataPath); - String fileName = getFileName(dataPath, username); - if (!new File(fileName).exists()) { + File fileName = getFileName(dataPath, username); + if (!fileName.exists()) { IOUtils.createPrivateFile(fileName); } @@ -156,29 +173,65 @@ public class SignalAccount implements Closeable { account.recipientStore = new RecipientStore(); account.profileStore = new ProfileStore(); account.stickerStore = new StickerStore(); + + account.messageCache = new MessageCache(getMessageCachePath(dataPath, username)); + account.registered = true; account.isMultiDevice = true; return account; } - public static String getFileName(String dataPath, String username) { - return dataPath + "/" + username; + public void migrateLegacyConfigs() { + if (getProfileKey() == null && isRegistered()) { + // Old config file, creating new profile key + setProfileKey(KeyUtils.createProfileKey()); + save(); + } + // Store profile keys only in profile store + for (ContactInfo contact : getContactStore().getContacts()) { + String profileKeyString = contact.profileKey; + if (profileKeyString == null) { + continue; + } + final ProfileKey profileKey; + try { + profileKey = new ProfileKey(Base64.decode(profileKeyString)); + } catch (InvalidInputException | IOException e) { + continue; + } + contact.profileKey = null; + getProfileStore().storeProfileKey(contact.getAddress(), profileKey); + } + // Ensure our profile key is stored in profile store + getProfileStore().storeProfileKey(getSelfAddress(), getProfileKey()); } - private static File getGroupCachePath(String dataPath, String username) { - return new File(new File(dataPath, username + ".d"), "group-cache"); + public static File getFileName(File dataPath, String username) { + return new File(dataPath, username); } - public static boolean userExists(String dataPath, String username) { + private static File getUserPath(final File dataPath, final String username) { + return new File(dataPath, username + ".d"); + } + + public static File getMessageCachePath(File dataPath, String username) { + return new File(getUserPath(dataPath, username), "msg-cache"); + } + + private static File getGroupCachePath(File dataPath, String username) { + return new File(getUserPath(dataPath, username), "group-cache"); + } + + public static boolean userExists(File dataPath, String username) { if (username == null) { return false; } - File f = new File(getFileName(dataPath, username)); + File f = getFileName(dataPath, username); return !(!f.exists() || f.isDirectory()); } - private void load(String dataPath) throws IOException { + private void load(File dataPath) throws IOException { JsonNode rootNode; synchronized (fileChannel) { fileChannel.position(0); @@ -198,28 +251,32 @@ public class SignalAccount implements Closeable { deviceId = node.asInt(); } if (rootNode.has("isMultiDevice")) { - isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean(); + isMultiDevice = Utils.getNotNullNode(rootNode, "isMultiDevice").asBoolean(); } - username = Util.getNotNullNode(rootNode, "username").asText(); - password = Util.getNotNullNode(rootNode, "password").asText(); + username = Utils.getNotNullNode(rootNode, "username").asText(); + password = Utils.getNotNullNode(rootNode, "password").asText(); JsonNode pinNode = rootNode.get("registrationLockPin"); registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText(); + JsonNode pinMasterKeyNode = rootNode.get("pinMasterKey"); + pinMasterKey = pinMasterKeyNode == null || pinMasterKeyNode.isNull() + ? null + : new MasterKey(Base64.decode(pinMasterKeyNode.asText())); if (rootNode.has("signalingKey")) { - signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText(); + signalingKey = Utils.getNotNullNode(rootNode, "signalingKey").asText(); } if (rootNode.has("preKeyIdOffset")) { - preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0); + preKeyIdOffset = Utils.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0); } else { preKeyIdOffset = 0; } if (rootNode.has("nextSignedPreKeyId")) { - nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt(); + nextSignedPreKeyId = Utils.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt(); } else { nextSignedPreKeyId = 0; } if (rootNode.has("profileKey")) { try { - profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText())); + profileKey = new ProfileKey(Base64.decode(Utils.getNotNullNode(rootNode, "profileKey").asText())); } catch (InvalidInputException e) { throw new IOException( "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", @@ -227,9 +284,9 @@ public class SignalAccount implements Closeable { } } - signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), + signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); - registered = Util.getNotNullNode(rootNode, "registered").asBoolean(); + registered = Utils.getNotNullNode(rootNode, "registered").asBoolean(); JsonNode groupStoreNode = rootNode.get("groupStore"); if (groupStoreNode != null) { groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class); @@ -273,7 +330,7 @@ public class SignalAccount implements Closeable { session.address = recipientStore.resolveServiceAddress(session.address); } - for (JsonIdentityKeyStore.Identity identity : signalProtocolStore.getIdentities()) { + for (IdentityInfo identity : signalProtocolStore.getIdentities()) { identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress())); } } @@ -294,6 +351,8 @@ public class SignalAccount implements Closeable { stickerStore = new StickerStore(); } + messageCache = new MessageCache(getMessageCachePath(dataPath, username)); + JsonNode threadStoreNode = rootNode.get("threadStore"); if (threadStoreNode != null) { LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, @@ -309,7 +368,7 @@ public class SignalAccount implements Closeable { contactInfo.messageExpirationTime = thread.messageExpirationTime; contactStore.updateContact(contactInfo); } else { - GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id)); + GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id)); if (groupInfo instanceof GroupInfoV1) { ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime; groupStore.updateGroup(groupInfo); @@ -332,6 +391,7 @@ public class SignalAccount implements Closeable { .put("isMultiDevice", isMultiDevice) .put("password", password) .put("registrationLockPin", registrationLockPin) + .put("pinMasterKey", pinMasterKey == null ? null : Base64.encodeBytes(pinMasterKey.serialize())) .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) @@ -356,17 +416,17 @@ public class SignalAccount implements Closeable { } } } catch (Exception e) { - System.err.println(String.format("Error saving file: %s", e.getMessage())); + logger.error("Error saving file: {}", e.getMessage()); } } - private static Pair openFileChannel(String fileName) throws IOException { - FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel(); + private static Pair openFileChannel(File fileName) throws IOException { + FileChannel fileChannel = new RandomAccessFile(fileName, "rw").getChannel(); FileLock lock = fileChannel.tryLock(); if (lock == null) { - System.err.println("Config file is in use by another instance, waiting…"); + logger.info("Config file is in use by another instance, waiting…"); lock = fileChannel.lock(); - System.err.println("Config file lock acquired."); + logger.info("Config file lock acquired."); } return new Pair<>(fileChannel, lock); } @@ -411,6 +471,10 @@ public class SignalAccount implements Closeable { return stickerStore; } + public MessageCache getMessageCache() { + return messageCache; + } + public String getUsername() { return username; } @@ -431,6 +495,10 @@ public class SignalAccount implements Closeable { return deviceId; } + public void setDeviceId(final int deviceId) { + this.deviceId = deviceId; + } + public String getPassword() { return password; } @@ -443,14 +511,18 @@ public class SignalAccount implements Closeable { return registrationLockPin; } - public String getRegistrationLock() { - return null; // TODO implement KBS - } - public void setRegistrationLockPin(final String registrationLockPin) { this.registrationLockPin = registrationLockPin; } + public MasterKey getPinMasterKey() { + return pinMasterKey; + } + + public void setPinMasterKey(final MasterKey pinMasterKey) { + this.pinMasterKey = pinMasterKey; + } + public String getSignalingKey() { return signalingKey; } @@ -467,6 +539,10 @@ public class SignalAccount implements Closeable { this.profileKey = profileKey; } + public byte[] getSelfUnidentifiedAccessKey() { + return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey()); + } + public int getPreKeyIdOffset() { return preKeyIdOffset; } @@ -491,8 +567,19 @@ public class SignalAccount implements Closeable { isMultiDevice = multiDevice; } + public boolean isUnrestrictedUnidentifiedAccess() { + // TODO make configurable + return false; + } + + public boolean isDiscoverableByPhoneNumber() { + // TODO make configurable + return true; + } + @Override public void close() throws IOException { + save(); synchronized (fileChannel) { try { lock.close(); diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java similarity index 95% rename from src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java rename to src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java index 3b155210..4dd132f7 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java +++ b/src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.contacts; +package org.asamk.signal.manager.storage.contacts; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java similarity index 96% rename from src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java rename to src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java index bb81b0c9..d2859f3f 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.contacts; +package org.asamk.signal.manager.storage.contacts; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java similarity index 86% rename from src/main/java/org/asamk/signal/storage/groups/GroupInfo.java rename to src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index 4cd410b8..a644b620 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -1,8 +1,9 @@ -package org.asamk.signal.storage.groups; +package org.asamk.signal.manager.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Set; @@ -11,16 +12,15 @@ import java.util.stream.Stream; public abstract class GroupInfo { - @JsonProperty - public final byte[] groupId; - - public GroupInfo(byte[] groupId) { - this.groupId = groupId; - } + @JsonIgnore + public abstract GroupId getGroupId(); @JsonIgnore public abstract String getTitle(); + @JsonIgnore + public abstract GroupInviteLinkUrl getGroupInviteLink(); + @JsonIgnore public abstract Set getMembers(); diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java similarity index 82% rename from src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java rename to src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index 42c40e94..d1230b27 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.groups; +package org.asamk.signal.manager.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -13,6 +13,11 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupUtils; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.IOException; @@ -25,8 +30,9 @@ public class GroupInfoV1 extends GroupInfo { private static final ObjectMapper jsonProcessor = new ObjectMapper(); - @JsonProperty - public byte[] expectedV2Id; + private final GroupIdV1 groupId; + + private GroupIdV2 expectedV2Id; @JsonProperty public String name; @@ -46,13 +52,8 @@ public class GroupInfoV1 extends GroupInfo { @JsonProperty(defaultValue = "false") public boolean archived; - public GroupInfoV1(byte[] groupId) { - super(groupId); - } - - @Override - public String getTitle() { - return name; + public GroupInfoV1(GroupIdV1 groupId) { + this.groupId = groupId; } public GroupInfoV1( @@ -68,8 +69,8 @@ public class GroupInfoV1 extends GroupInfo { @JsonProperty("messageExpirationTime") int messageExpirationTime, @JsonProperty("active") boolean _ignored_active ) { - super(groupId); - this.expectedV2Id = expectedV2Id; + this.groupId = GroupId.v1(groupId); + this.expectedV2Id = GroupId.v2(expectedV2Id); this.name = name; this.members.addAll(members); this.color = color; @@ -79,6 +80,40 @@ public class GroupInfoV1 extends GroupInfo { this.messageExpirationTime = messageExpirationTime; } + @Override + @JsonIgnore + public GroupIdV1 getGroupId() { + return groupId; + } + + @JsonProperty("groupId") + private byte[] getGroupIdJackson() { + return groupId.serialize(); + } + + @JsonIgnore + public GroupIdV2 getExpectedV2Id() { + if (expectedV2Id == null) { + expectedV2Id = GroupUtils.getGroupIdV2(groupId); + } + return expectedV2Id; + } + + @JsonProperty("expectedV2Id") + private byte[] getExpectedV2IdJackson() { + return getExpectedV2Id().serialize(); + } + + @Override + public String getTitle() { + return name; + } + + @Override + public GroupInviteLinkUrl getGroupInviteLink() { + return null; + } + @JsonIgnore public Set getMembers() { return members; diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java similarity index 69% rename from src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java rename to src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index a205d140..2092c03a 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -1,26 +1,34 @@ -package org.asamk.signal.storage.groups; +package org.asamk.signal.manager.storage.groups; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.zkgroup.groups.GroupMasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; -import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; public class GroupInfoV2 extends GroupInfo { + private final GroupIdV2 groupId; private final GroupMasterKey masterKey; private boolean blocked; private DecryptedGroup group; // stored as a file with hexadecimal groupId as name - public GroupInfoV2(final byte[] groupId, final GroupMasterKey masterKey) { - super(groupId); + public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) { + this.groupId = groupId; this.masterKey = masterKey; } + @Override + public GroupIdV2 getGroupId() { + return groupId; + } + public GroupMasterKey getMasterKey() { return masterKey; } @@ -41,10 +49,23 @@ public class GroupInfoV2 extends GroupInfo { return this.group.getTitle(); } + @Override + public GroupInviteLinkUrl getGroupInviteLink() { + if (this.group == null || this.group.getInviteLinkPassword() == null || ( + this.group.getAccessControl().getAddFromInviteLink() != AccessControl.AccessRequired.ANY + && this.group.getAccessControl().getAddFromInviteLink() + != AccessControl.AccessRequired.ADMINISTRATOR + )) { + return null; + } + + return GroupInviteLinkUrl.forGroup(masterKey, group); + } + @Override public Set getMembers() { if (this.group == null) { - return Collections.emptySet(); + return Set.of(); } return group.getMembersList() .stream() @@ -55,7 +76,7 @@ public class GroupInfoV2 extends GroupInfo { @Override public Set getPendingMembers() { if (this.group == null) { - return Collections.emptySet(); + return Set.of(); } return group.getPendingMembersList() .stream() @@ -66,7 +87,7 @@ public class GroupInfoV2 extends GroupInfo { @Override public Set getRequestingMembers() { if (this.group == null) { - return Collections.emptySet(); + return Set.of(); } return group.getRequestingMembersList() .stream() diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java similarity index 65% rename from src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java rename to src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java index 2175e293..fdcd28a3 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.groups; +package org.asamk.signal.manager.storage.groups; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; @@ -12,12 +12,17 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.asamk.signal.manager.GroupUtils; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.util.Hex; -import org.asamk.signal.util.IOUtils; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.util.Base64; import java.io.File; @@ -25,7 +30,6 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -33,13 +37,15 @@ import java.util.Map; public class JsonGroupStore { + final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class); + private static final ObjectMapper jsonProcessor = new ObjectMapper(); public File groupCachePath; @JsonProperty("groups") @JsonSerialize(using = GroupsSerializer.class) @JsonDeserialize(using = GroupsDeserializer.class) - private final Map groups = new HashMap<>(); + private final Map groups = new HashMap<>(); private JsonGroupStore() { } @@ -49,70 +55,78 @@ public class JsonGroupStore { } public void updateGroup(GroupInfo group) { - groups.put(Base64.encodeBytes(group.groupId), group); + groups.put(group.getGroupId(), group); if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) { try { IOUtils.createPrivateDirectories(groupCachePath); - try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.groupId))) { + try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.getGroupId()))) { ((GroupInfoV2) group).getGroup().writeTo(stream); } + final File groupFileLegacy = getGroupFileLegacy(group.getGroupId()); + if (groupFileLegacy.exists()) { + groupFileLegacy.delete(); + } } catch (IOException e) { - System.err.println("Failed to cache group, ignoring ..."); + logger.warn("Failed to cache group, ignoring: {}", e.getMessage()); } } } - public void deleteGroup(byte[] groupId) { - groups.remove(Base64.encodeBytes(groupId)); + public void deleteGroup(GroupId groupId) { + groups.remove(groupId); } - public GroupInfo getGroup(byte[] groupId) { - final GroupInfo group = groups.get(Base64.encodeBytes(groupId)); - if (group == null & groupId.length == 16) { - return getGroupByV1Id(groupId); + public GroupInfo getGroup(GroupId groupId) { + GroupInfo group = groups.get(groupId); + if (group == null) { + if (groupId instanceof GroupIdV1) { + group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId)); + } else if (groupId instanceof GroupIdV2) { + group = getGroupV1ByV2Id((GroupIdV2) groupId); + } } loadDecryptedGroup(group); return group; } - public GroupInfo getGroupByV1Id(byte[] groupIdV1) { - GroupInfo group = groups.get(Base64.encodeBytes(groupIdV1)); - if (group == null) { - group = groups.get(Base64.encodeBytes(GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(groupIdV1)))); - } - loadDecryptedGroup(group); - return group; - } - - public GroupInfo getGroupByV2Id(byte[] groupIdV2) { - GroupInfo group = groups.get(Base64.encodeBytes(groupIdV2)); - if (group == null) { - for (GroupInfo g : groups.values()) { - if (g instanceof GroupInfoV1 && Arrays.equals(groupIdV2, ((GroupInfoV1) g).expectedV2Id)) { - group = g; - break; + private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) { + for (GroupInfo g : groups.values()) { + if (g instanceof GroupInfoV1) { + final GroupInfoV1 gv1 = (GroupInfoV1) g; + if (groupIdV2.equals(gv1.getExpectedV2Id())) { + return gv1; } } } - loadDecryptedGroup(group); - return group; + return null; } private void loadDecryptedGroup(final GroupInfo group) { if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { - try (FileInputStream stream = new FileInputStream(getGroupFile(group.groupId))) { + File groupFile = getGroupFile(group.getGroupId()); + if (!groupFile.exists()) { + groupFile = getGroupFileLegacy(group.getGroupId()); + } + if (!groupFile.exists()) { + return; + } + try (FileInputStream stream = new FileInputStream(groupFile)) { ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream)); } catch (IOException ignored) { } } } - private File getGroupFile(final byte[] groupId) { - return new File(groupCachePath, Hex.toStringCondensed(groupId)); + private File getGroupFileLegacy(final GroupId groupId) { + return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize())); } - public GroupInfoV1 getOrCreateGroupV1(byte[] groupId) { - GroupInfo group = groups.get(Base64.encodeBytes(groupId)); + private File getGroupFile(final GroupId groupId) { + return new File(groupCachePath, groupId.toBase64().replace("/", "_")); + } + + public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) { + GroupInfo group = getGroup(groupId); if (group instanceof GroupInfoV1) { return (GroupInfoV1) group; } @@ -146,7 +160,7 @@ public class JsonGroupStore { } else if (group instanceof GroupInfoV2) { final GroupInfoV2 groupV2 = (GroupInfoV2) group; jgen.writeStartObject(); - jgen.writeStringField("groupId", Base64.encodeBytes(groupV2.groupId)); + jgen.writeStringField("groupId", groupV2.getGroupId().toBase64()); jgen.writeStringField("masterKey", Base64.encodeBytes(groupV2.getMasterKey().serialize())); jgen.writeBooleanField("blocked", groupV2.isBlocked()); jgen.writeEndObject(); @@ -158,34 +172,31 @@ public class JsonGroupStore { } } - private static class GroupsDeserializer extends JsonDeserializer> { + private static class GroupsDeserializer extends JsonDeserializer> { @Override - public Map deserialize( + public Map deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) throws IOException { - Map groups = new HashMap<>(); + Map groups = new HashMap<>(); JsonNode node = jsonParser.getCodec().readTree(jsonParser); for (JsonNode n : node) { GroupInfo g; if (n.has("masterKey")) { // a v2 group - byte[] groupId = Base64.decode(n.get("groupId").asText()); + GroupIdV2 groupId = GroupIdV2.fromBase64(n.get("groupId").asText()); try { GroupMasterKey masterKey = new GroupMasterKey(Base64.decode(n.get("masterKey").asText())); g = new GroupInfoV2(groupId, masterKey); } catch (InvalidInputException e) { - throw new AssertionError("Invalid master key for group " + Base64.encodeBytes(groupId)); + throw new AssertionError("Invalid master key for group " + groupId.toBase64()); } g.setBlocked(n.get("blocked").asBoolean(false)); } else { GroupInfoV1 gv1 = jsonProcessor.treeToValue(n, GroupInfoV1.class); - if (gv1.expectedV2Id == null) { - gv1.expectedV2Id = GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(gv1.groupId)); - } g = gv1; } - groups.put(Base64.encodeBytes(g.groupId), g); + groups.put(g.getGroupId(), g); } return groups; diff --git a/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java b/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java new file mode 100644 index 00000000..6c20cf62 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java @@ -0,0 +1,38 @@ +package org.asamk.signal.manager.storage.messageCache; + +import org.asamk.signal.manager.util.MessageCacheUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public final class CachedMessage { + + final static Logger logger = LoggerFactory.getLogger(CachedMessage.class); + + private final File file; + + CachedMessage(final File file) { + this.file = file; + } + + public SignalServiceEnvelope loadEnvelope() { + try { + return MessageCacheUtils.loadEnvelope(file); + } catch (IOException e) { + logger.error("Failed to load cached message envelope “{}”: {}", file, e.getMessage()); + return null; + } + } + + public void delete() { + try { + Files.delete(file.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete cached message file “{}”, ignoring: {}", file, e.getMessage()); + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java b/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java new file mode 100644 index 00000000..4e48ee76 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java @@ -0,0 +1,79 @@ +package org.asamk.signal.manager.storage.messageCache; + +import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.MessageCacheUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class MessageCache { + + final static Logger logger = LoggerFactory.getLogger(MessageCache.class); + + private final File messageCachePath; + + public MessageCache(final File messageCachePath) { + this.messageCachePath = messageCachePath; + } + + public Iterable getCachedMessages() { + if (!messageCachePath.exists()) { + return Collections.emptyList(); + } + + return Arrays.stream(Objects.requireNonNull(messageCachePath.listFiles())).flatMap(dir -> { + if (dir.isFile()) { + return Stream.of(dir); + } + + final File[] files = Objects.requireNonNull(dir.listFiles()); + if (files.length == 0) { + try { + Files.delete(dir.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete cache dir “{}”, ignoring: {}", dir, e.getMessage()); + } + return Stream.empty(); + } + return Arrays.stream(files).filter(File::isFile); + }).map(CachedMessage::new).collect(Collectors.toList()); + } + + public CachedMessage cacheMessage(SignalServiceEnvelope envelope) { + final long now = new Date().getTime(); + final String source = envelope.hasSource() ? envelope.getSourceAddress().getLegacyIdentifier() : ""; + + try { + File cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp()); + MessageCacheUtils.storeEnvelope(envelope, cacheFile); + return new CachedMessage(cacheFile); + } catch (IOException e) { + logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage()); + return null; + } + } + + private File getMessageCachePath(String sender) { + if (sender == null || sender.isEmpty()) { + return messageCachePath; + } + + return new File(messageCachePath, sender.replace("/", "_")); + } + + private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException { + File cachePath = getMessageCachePath(sender); + IOUtils.createPrivateDirectories(cachePath); + return new File(cachePath, now + "_" + timestamp); + } +} diff --git a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java b/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java similarity index 98% rename from src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java rename to src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java index 527ec15a..bff2f17e 100644 --- a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.profiles; +package org.asamk.signal.manager.storage.profiles; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; @@ -112,7 +112,7 @@ public class ProfileStore { try { profileKeyCredential = new ProfileKeyCredential(Base64.decode(entry.get( "profileKeyCredential").asText())); - } catch (InvalidInputException ignored) { + } catch (Throwable ignored) { } } long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong(); diff --git a/src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java b/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java similarity index 98% rename from src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java rename to src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java index 023458ed..48a38578 100644 --- a/src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java +++ b/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.profiles; +package org.asamk.signal.manager.storage.profiles; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java b/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java similarity index 96% rename from src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java rename to src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java index e6acf30d..a81fbcb5 100644 --- a/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java +++ b/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.profiles; +package org.asamk.signal.manager.storage.profiles; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; diff --git a/src/main/java/org/asamk/signal/manager/storage/protocol/IdentityInfo.java b/src/main/java/org/asamk/signal/manager/storage/protocol/IdentityInfo.java new file mode 100644 index 00000000..d4af11f2 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/IdentityInfo.java @@ -0,0 +1,57 @@ +package org.asamk.signal.manager.storage.protocol; + +import org.asamk.signal.manager.TrustLevel; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.util.Date; + +public class IdentityInfo { + + SignalServiceAddress address; + IdentityKey identityKey; + TrustLevel trustLevel; + Date added; + + public IdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) { + this.address = address; + this.identityKey = identityKey; + this.trustLevel = trustLevel; + this.added = new Date(); + } + + IdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) { + this.address = address; + this.identityKey = identityKey; + this.trustLevel = trustLevel; + this.added = added; + } + + public SignalServiceAddress getAddress() { + return address; + } + + public void setAddress(final SignalServiceAddress address) { + this.address = address; + } + + boolean isTrusted() { + return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; + } + + public IdentityKey getIdentityKey() { + return this.identityKey; + } + + public TrustLevel getTrustLevel() { + return this.trustLevel; + } + + public Date getDateAdded() { + return this.added; + } + + public byte[] getFingerprint() { + return identityKey.getPublicKey().serialize(); + } +} diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java similarity index 77% rename from src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java index 0095e6d2..5bc1c11f 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -9,7 +9,9 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.asamk.signal.manager.TrustLevel; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.util.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -27,7 +29,9 @@ import java.util.UUID; public class JsonIdentityKeyStore implements IdentityKeyStore { - private final List identities = new ArrayList<>(); + final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class); + + private final List identities = new ArrayList<>(); private final IdentityKeyPair identityKeyPair; private final int localRegistrationId; @@ -47,7 +51,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { if (resolver != null) { return resolver.resolveSignalServiceAddress(identifier); } else { - return Util.getSignalServiceAddressFromIdentifier(identifier); + return Utils.getSignalServiceAddressFromIdentifier(identifier); } } @@ -81,7 +85,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { public boolean saveIdentity( SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added ) { - for (Identity id : identities) { + for (IdentityInfo id : identities) { if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) { continue; } @@ -93,7 +97,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { return true; } - identities.add(new Identity(serviceAddress, identityKey, trustLevel, added != null ? added : new Date())); + identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, added != null ? added : new Date())); return false; } @@ -107,7 +111,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { public void setIdentityTrustLevel( SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel ) { - for (Identity id : identities) { + for (IdentityInfo id : identities) { if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) { continue; } @@ -119,7 +123,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { return; } - identities.add(new Identity(serviceAddress, identityKey, trustLevel, new Date())); + identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, new Date())); } @Override @@ -128,7 +132,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); boolean trustOnFirstUse = true; - for (Identity id : identities) { + for (IdentityInfo id : identities) { if (!id.address.matches(serviceAddress)) { continue; } @@ -146,14 +150,14 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { @Override public IdentityKey getIdentity(SignalProtocolAddress address) { SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); - Identity identity = getIdentity(serviceAddress); + IdentityInfo identity = getIdentity(serviceAddress); return identity == null ? null : identity.getIdentityKey(); } - public Identity getIdentity(SignalServiceAddress serviceAddress) { + public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) { long maxDate = 0; - Identity maxIdentity = null; - for (Identity id : this.identities) { + IdentityInfo maxIdentity = null; + for (IdentityInfo id : this.identities) { if (!id.address.matches(serviceAddress)) { continue; } @@ -167,14 +171,14 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { return maxIdentity; } - public List getIdentities() { + public List getIdentities() { // TODO deep copy return identities; } - public List getIdentities(SignalServiceAddress serviceAddress) { - List identities = new ArrayList<>(); - for (Identity identity : this.identities) { + public List getIdentities(SignalServiceAddress serviceAddress) { + List identities = new ArrayList<>(); + for (IdentityInfo identity : this.identities) { if (identity.address.matches(serviceAddress)) { identities.add(identity); } @@ -209,7 +213,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { UUID uuid = trustedKey.hasNonNull("uuid") ? UuidUtil.parseOrNull(trustedKey.get("uuid") .asText()) : null; final SignalServiceAddress serviceAddress = uuid == null - ? Util.getSignalServiceAddressFromIdentifier(trustedKeyName) + ? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName) : new SignalServiceAddress(uuid, trustedKeyName); try { IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0); @@ -219,7 +223,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { .asLong()) : new Date(); keyStore.saveIdentity(serviceAddress, id, trustLevel, added); } catch (InvalidKeyException | IOException e) { - System.out.println(String.format("Error while decoding key for: %s", trustedKeyName)); + logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage()); } } } @@ -242,7 +246,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize())); json.writeArrayFieldStart("trustedKeys"); - for (Identity trustedKey : jsonIdentityKeyStore.identities) { + for (IdentityInfo trustedKey : jsonIdentityKeyStore.identities) { json.writeStartObject(); if (trustedKey.getAddress().getNumber().isPresent()) { json.writeStringField("name", trustedKey.getAddress().getNumber().get()); @@ -260,53 +264,4 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { } } - public static class Identity { - - SignalServiceAddress address; - IdentityKey identityKey; - TrustLevel trustLevel; - Date added; - - public Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) { - this.address = address; - this.identityKey = identityKey; - this.trustLevel = trustLevel; - this.added = new Date(); - } - - Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) { - this.address = address; - this.identityKey = identityKey; - this.trustLevel = trustLevel; - this.added = added; - } - - public SignalServiceAddress getAddress() { - return address; - } - - public void setAddress(final SignalServiceAddress address) { - this.address = address; - } - - boolean isTrusted() { - return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; - } - - public IdentityKey getIdentityKey() { - return this.identityKey; - } - - public TrustLevel getTrustLevel() { - return this.trustLevel; - } - - public Date getDateAdded() { - return this.added; - } - - public byte[] getFingerprint() { - return identityKey.getPublicKey().serialize(); - } - } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonPreKeyStore.java similarity index 91% rename from src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/JsonPreKeyStore.java index dea1996d..4d884c3e 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonPreKeyStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.PreKeyStore; @@ -19,6 +21,8 @@ import java.util.Map; class JsonPreKeyStore implements PreKeyStore { + final static Logger logger = LoggerFactory.getLogger(JsonPreKeyStore.class); + private final Map store = new HashMap<>(); public JsonPreKeyStore() { @@ -72,7 +76,7 @@ class JsonPreKeyStore implements PreKeyStore { try { preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); } catch (IOException e) { - System.err.println(String.format("Error while decoding prekey for: %s", preKeyId)); + logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSessionStore.java similarity index 92% rename from src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/JsonSessionStore.java index fae72bae..6e300214 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSessionStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -8,7 +8,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.util.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionStore; @@ -24,6 +26,8 @@ import java.util.UUID; class JsonSessionStore implements SessionStore { + final static Logger logger = LoggerFactory.getLogger(JsonSessionStore.class); + private final List sessions = new ArrayList<>(); private SignalServiceAddressResolver resolver; @@ -39,7 +43,7 @@ class JsonSessionStore implements SessionStore { if (resolver != null) { return resolver.resolveSignalServiceAddress(identifier); } else { - return Util.getSignalServiceAddressFromIdentifier(identifier); + return Utils.getSignalServiceAddressFromIdentifier(identifier); } } @@ -51,7 +55,7 @@ class JsonSessionStore implements SessionStore { try { return new SessionRecord(info.sessionRecord); } catch (IOException e) { - System.err.println("Failed to load session, resetting session: " + e); + logger.warn("Failed to load session, resetting session: {}", e.getMessage()); final SessionRecord sessionRecord = new SessionRecord(); info.sessionRecord = sessionRecord.serialize(); return sessionRecord; @@ -143,7 +147,7 @@ class JsonSessionStore implements SessionStore { UUID uuid = session.hasNonNull("uuid") ? UuidUtil.parseOrNull(session.get("uuid").asText()) : null; final SignalServiceAddress serviceAddress = uuid == null - ? Util.getSignalServiceAddressFromIdentifier(sessionName) + ? Utils.getSignalServiceAddressFromIdentifier(sessionName) : new SignalServiceAddress(uuid, sessionName); final int deviceId = session.get("deviceId").asInt(); final String record = session.get("record").asText(); @@ -151,7 +155,7 @@ class JsonSessionStore implements SessionStore { SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record)); sessionStore.sessions.add(sessionInfo); } catch (IOException e) { - System.err.println(String.format("Error while decoding session for: %s", sessionName)); + logger.warn("Error while decoding session for {}: {}", sessionName, e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java similarity index 95% rename from src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java index 5939749d..41a63013 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -91,11 +91,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel); } - public List getIdentities() { + public List getIdentities() { return identityKeyStore.getIdentities(); } - public List getIdentities(SignalServiceAddress serviceAddress) { + public List getIdentities(SignalServiceAddress serviceAddress) { return identityKeyStore.getIdentities(serviceAddress); } @@ -109,7 +109,7 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return identityKeyStore.getIdentity(address); } - public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) { + public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) { return identityKeyStore.getIdentity(serviceAddress); } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignedPreKeyStore.java similarity index 92% rename from src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignedPreKeyStore.java index 255dd4e0..5eae4500 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignedPreKeyStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; @@ -21,6 +23,8 @@ import java.util.Map; class JsonSignedPreKeyStore implements SignedPreKeyStore { + final static Logger logger = LoggerFactory.getLogger(JsonSignedPreKeyStore.class); + private final Map store = new HashMap<>(); public JsonSignedPreKeyStore() { @@ -89,7 +93,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { try { preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); } catch (IOException e) { - System.err.println(String.format("Error while decoding prekey for: %s", preKeyId)); + logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/RecipientStore.java similarity index 98% rename from src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/RecipientStore.java index 701eca34..60634ae5 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/RecipientStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; diff --git a/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java b/src/main/java/org/asamk/signal/manager/storage/protocol/SessionInfo.java similarity index 89% rename from src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/SessionInfo.java index 00221233..802b896b 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/SessionInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import org.whispersystems.signalservice.api.push.SignalServiceAddress; diff --git a/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java b/src/main/java/org/asamk/signal/manager/storage/protocol/SignalServiceAddressResolver.java similarity index 88% rename from src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/SignalServiceAddressResolver.java index b1c5fb38..86eea05e 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/SignalServiceAddressResolver.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import org.whispersystems.signalservice.api.push.SignalServiceAddress; diff --git a/src/main/java/org/asamk/signal/storage/stickers/Sticker.java b/src/main/java/org/asamk/signal/manager/storage/stickers/Sticker.java similarity index 93% rename from src/main/java/org/asamk/signal/storage/stickers/Sticker.java rename to src/main/java/org/asamk/signal/manager/storage/stickers/Sticker.java index 386924c4..54e95d0a 100644 --- a/src/main/java/org/asamk/signal/storage/stickers/Sticker.java +++ b/src/main/java/org/asamk/signal/manager/storage/stickers/Sticker.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.stickers; +package org.asamk.signal.manager.storage.stickers; public class Sticker { diff --git a/src/main/java/org/asamk/signal/storage/stickers/StickerStore.java b/src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java similarity index 98% rename from src/main/java/org/asamk/signal/storage/stickers/StickerStore.java rename to src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java index e5d817d2..10cd2e99 100644 --- a/src/main/java/org/asamk/signal/storage/stickers/StickerStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.stickers; +package org.asamk.signal.manager.storage.stickers; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java b/src/main/java/org/asamk/signal/manager/storage/threads/LegacyJsonThreadStore.java similarity index 97% rename from src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java rename to src/main/java/org/asamk/signal/manager/storage/threads/LegacyJsonThreadStore.java index 24463933..f37360a2 100644 --- a/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/threads/LegacyJsonThreadStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.threads; +package org.asamk.signal.manager.storage.threads; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; diff --git a/src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java b/src/main/java/org/asamk/signal/manager/storage/threads/ThreadInfo.java similarity index 78% rename from src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java rename to src/main/java/org/asamk/signal/manager/storage/threads/ThreadInfo.java index 67e6b474..b81a0051 100644 --- a/src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java +++ b/src/main/java/org/asamk/signal/manager/storage/threads/ThreadInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.threads; +package org.asamk.signal.manager.storage.threads; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java b/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java new file mode 100644 index 00000000..b9a97073 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java @@ -0,0 +1,79 @@ +package org.asamk.signal.manager.util; + +import org.asamk.signal.manager.AttachmentInvalidException; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; +import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +public class AttachmentUtils { + + public static List getSignalServiceAttachments(List attachments) throws AttachmentInvalidException { + List signalServiceAttachments = null; + if (attachments != null) { + signalServiceAttachments = new ArrayList<>(attachments.size()); + for (String attachment : attachments) { + try { + signalServiceAttachments.add(createAttachment(new File(attachment))); + } catch (IOException e) { + throw new AttachmentInvalidException(attachment, e); + } + } + } + return signalServiceAttachments; + } + + public static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { + InputStream attachmentStream = new FileInputStream(attachmentFile); + final long attachmentSize = attachmentFile.length(); + final String mime = Utils.getFileMimeType(attachmentFile, "application/octet-stream"); + // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option + final long uploadTimestamp = System.currentTimeMillis(); + Optional preview = Optional.absent(); + Optional caption = Optional.absent(); + Optional blurHash = Optional.absent(); + final Optional resumableUploadSpec = Optional.absent(); + return new SignalServiceAttachmentStream(attachmentStream, + mime, + attachmentSize, + Optional.of(attachmentFile.getName()), + false, + false, + preview, + 0, + 0, + uploadTimestamp, + caption, + blurHash, + null, + null, + resumableUploadSpec); + } + + public static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException { + InputStream input = stream.getInputStream(); + + try (OutputStream output = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[4096]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } + return outputFile; + } +} diff --git a/src/main/java/org/asamk/signal/manager/util/IOUtils.java b/src/main/java/org/asamk/signal/manager/util/IOUtils.java new file mode 100644 index 00000000..06f8aa22 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/IOUtils.java @@ -0,0 +1,72 @@ +package org.asamk.signal.manager.util; + +import org.whispersystems.signalservice.internal.util.Util; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; +import java.util.Set; + +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +public class IOUtils { + + public static File createTempFile() throws IOException { + return File.createTempFile("signal_tmp_", ".tmp"); + } + + public static byte[] readFully(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.copy(in, baos); + return baos.toByteArray(); + } + + public static void createPrivateDirectories(File file) throws IOException { + if (file.exists()) { + return; + } + + final Path path = file.toPath(); + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); + Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms)); + } catch (UnsupportedOperationException e) { + Files.createDirectories(path); + } + } + + public static void createPrivateFile(File path) throws IOException { + final Path file = path.toPath(); + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); + Files.createFile(file, PosixFilePermissions.asFileAttribute(perms)); + } catch (UnsupportedOperationException e) { + Files.createFile(file); + } + } + + public static void copyStreamToFile(InputStream input, File outputFile) throws IOException { + copyStreamToFile(input, outputFile, 8192); + } + + public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException { + try (OutputStream output = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[bufferSize]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/util/KeyUtils.java b/src/main/java/org/asamk/signal/manager/util/KeyUtils.java new file mode 100644 index 00000000..d8861b1b --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/KeyUtils.java @@ -0,0 +1,61 @@ +package org.asamk.signal.manager.util; + +import org.asamk.signal.util.RandomUtils; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECKeyPair; +import org.whispersystems.libsignal.ecc.ECPrivateKey; +import org.whispersystems.signalservice.api.kbs.MasterKey; +import org.whispersystems.util.Base64; + +public class KeyUtils { + + private KeyUtils() { + } + + public static IdentityKeyPair generateIdentityKeyPair() { + ECKeyPair djbKeyPair = Curve.generateKeyPair(); + IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey()); + ECPrivateKey djbPrivateKey = djbKeyPair.getPrivateKey(); + + return new IdentityKeyPair(djbIdentityKey, djbPrivateKey); + } + + public static String createSignalingKey() { + return getSecret(52); + } + + public static ProfileKey createProfileKey() { + try { + return new ProfileKey(getSecretBytes(32)); + } catch (InvalidInputException e) { + throw new AssertionError("Profile key is guaranteed to be 32 bytes here"); + } + } + + public static String createPassword() { + return getSecret(18); + } + + public static byte[] createStickerUploadKey() { + return getSecretBytes(32); + } + + public static MasterKey createMasterKey() { + return MasterKey.createNew(RandomUtils.getSecureRandom()); + } + + private static String getSecret(int size) { + byte[] secret = getSecretBytes(size); + return Base64.encodeBytes(secret); + } + + public static byte[] getSecretBytes(int size) { + byte[] secret = new byte[size]; + RandomUtils.getSecureRandom().nextBytes(secret); + return secret; + } +} diff --git a/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java b/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java new file mode 100644 index 00000000..8661c10b --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java @@ -0,0 +1,105 @@ +package org.asamk.signal.manager.util; + +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.UUID; + +public class MessageCacheUtils { + + public static SignalServiceEnvelope loadEnvelope(File file) throws IOException { + try (FileInputStream f = new FileInputStream(file)) { + DataInputStream in = new DataInputStream(f); + int version = in.readInt(); + if (version > 4) { + return null; + } + int type = in.readInt(); + String source = in.readUTF(); + UUID sourceUuid = null; + if (version >= 3) { + sourceUuid = UuidUtil.parseOrNull(in.readUTF()); + } + int sourceDevice = in.readInt(); + if (version == 1) { + // read legacy relay field + in.readUTF(); + } + long timestamp = in.readLong(); + byte[] content = null; + int contentLen = in.readInt(); + if (contentLen > 0) { + content = new byte[contentLen]; + in.readFully(content); + } + byte[] legacyMessage = null; + int legacyMessageLen = in.readInt(); + if (legacyMessageLen > 0) { + legacyMessage = new byte[legacyMessageLen]; + in.readFully(legacyMessage); + } + long serverReceivedTimestamp = 0; + String uuid = null; + if (version >= 2) { + serverReceivedTimestamp = in.readLong(); + uuid = in.readUTF(); + if ("".equals(uuid)) { + uuid = null; + } + } + long serverDeliveredTimestamp = 0; + if (version >= 4) { + serverDeliveredTimestamp = in.readLong(); + } + Optional addressOptional = sourceUuid == null && source.isEmpty() + ? Optional.absent() + : Optional.of(new SignalServiceAddress(sourceUuid, source)); + return new SignalServiceEnvelope(type, + addressOptional, + sourceDevice, + timestamp, + legacyMessage, + content, + serverReceivedTimestamp, + serverDeliveredTimestamp, + uuid); + } + } + + public static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { + try (FileOutputStream f = new FileOutputStream(file)) { + try (DataOutputStream out = new DataOutputStream(f)) { + out.writeInt(4); // version + out.writeInt(envelope.getType()); + out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : ""); + out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : ""); + out.writeInt(envelope.getSourceDevice()); + out.writeLong(envelope.getTimestamp()); + if (envelope.hasContent()) { + out.writeInt(envelope.getContent().length); + out.write(envelope.getContent()); + } else { + out.writeInt(0); + } + if (envelope.hasLegacyMessage()) { + out.writeInt(envelope.getLegacyMessage().length); + out.write(envelope.getLegacyMessage()); + } else { + out.writeInt(0); + } + out.writeLong(envelope.getServerReceivedTimestamp()); + String uuid = envelope.getUuid(); + out.writeUTF(uuid == null ? "" : uuid); + out.writeLong(envelope.getServerDeliveredTimestamp()); + } + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/util/PinHashing.java b/src/main/java/org/asamk/signal/manager/util/PinHashing.java new file mode 100644 index 00000000..2fd2d802 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/PinHashing.java @@ -0,0 +1,31 @@ +package org.asamk.signal.manager.util; + +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.kbs.HashedPin; +import org.whispersystems.signalservice.internal.registrationpin.PinHasher; + +public final class PinHashing { + + private PinHashing() { + } + + public static HashedPin hashPin(String pin, KeyBackupService.HashSession hashSession) { + final Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id).withParallelism(1) + .withIterations(32) + .withVersion(Argon2Parameters.ARGON2_VERSION_13) + .withMemoryAsKB(16 * 1024) + .withSalt(hashSession.hashSalt()) + .build(); + + final Argon2BytesGenerator generator = new Argon2BytesGenerator(); + generator.init(params); + + return PinHasher.hashPin(PinHasher.normalize(pin), password -> { + byte[] output = new byte[64]; + generator.generateBytes(password, output); + return output; + }); + } +} diff --git a/src/main/java/org/asamk/signal/manager/util/Utils.java b/src/main/java/org/asamk/signal/manager/util/Utils.java new file mode 100644 index 00000000..e68b5ce3 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -0,0 +1,97 @@ +package org.asamk.signal.manager.util; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; +import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.StreamDetails; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidObjectException; +import java.net.URLConnection; +import java.nio.file.Files; + +public class Utils { + + public static String getFileMimeType(File file, String defaultMimeType) throws IOException { + String mime = Files.probeContentType(file.toPath()); + if (mime == null) { + try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { + mime = URLConnection.guessContentTypeFromStream(bufferedStream); + } + } + if (mime == null) { + return defaultMimeType; + } + return mime; + } + + public static StreamDetails createStreamDetailsFromFile(File file) throws IOException { + InputStream stream = new FileInputStream(file); + final long size = file.length(); + String mime = Files.probeContentType(file.toPath()); + if (mime == null) { + mime = "application/octet-stream"; + } + return new StreamDetails(stream, mime, size); + } + + public static String computeSafetyNumber( + boolean isUuidCapable, + SignalServiceAddress ownAddress, + IdentityKey ownIdentityKey, + SignalServiceAddress theirAddress, + IdentityKey theirIdentityKey + ) { + int version; + byte[] ownId; + byte[] theirId; + + if (isUuidCapable && ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) { + // Version 2: UUID user + version = 2; + ownId = UuidUtil.toByteArray(ownAddress.getUuid().get()); + theirId = UuidUtil.toByteArray(theirAddress.getUuid().get()); + } else { + // Version 1: E164 user + version = 1; + if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) { + return "INVALID ID"; + } + ownId = ownAddress.getNumber().get().getBytes(); + theirId = theirAddress.getNumber().get().getBytes(); + } + + Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, + ownId, + ownIdentityKey, + theirId, + theirIdentityKey); + return fingerprint.getDisplayableFingerprint().getDisplayText(); + } + + public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) { + if (UuidUtil.isUuid(identifier)) { + return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null); + } else { + return new SignalServiceAddress(null, identifier); + } + } + + public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException { + JsonNode node = parent.get(name); + if (node == null) { + throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", + name)); + } + + return node; + } +} diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 8e27dd90..e9553f98 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -1,7 +1,8 @@ package org.asamk.signal.util; -import org.asamk.signal.manager.GroupNotFoundException; -import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -22,7 +23,9 @@ public class ErrorUtils { } public static int handleTimestampAndSendMessageResults(long timestamp, List results) { - System.out.println(timestamp); + if (timestamp != 0) { + System.out.println(timestamp); + } List errors = getErrorMessagesFromSendMessageResults(results); return handleSendMessageResultErrors(errors); } diff --git a/src/main/java/org/asamk/signal/util/GroupIdFormatException.java b/src/main/java/org/asamk/signal/util/GroupIdFormatException.java deleted file mode 100644 index 5a5c4570..00000000 --- a/src/main/java/org/asamk/signal/util/GroupIdFormatException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.asamk.signal.util; - -import java.io.IOException; - -public class GroupIdFormatException extends Exception { - - public GroupIdFormatException(String groupId, IOException e) { - super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage()); - } -} diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index 4d8adea6..766d1905 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -1,35 +1,16 @@ package org.asamk.signal.util; -import org.whispersystems.signalservice.internal.util.Util; - -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.io.StringWriter; import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.EnumSet; -import java.util.Set; - -import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; -import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; public class IOUtils { private IOUtils() { } - public static File createTempFile() throws IOException { - return File.createTempFile("signal_tmp_", ".tmp"); - } - public static String readAll(InputStream in, Charset charset) throws IOException { StringWriter output = new StringWriter(); byte[] buffer = new byte[4096]; @@ -40,62 +21,12 @@ public class IOUtils { return output.toString(); } - public static byte[] readFully(InputStream in) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Util.copy(in, baos); - return baos.toByteArray(); - } - - public static void createPrivateDirectories(String directoryPath) throws IOException { - final File file = new File(directoryPath); - createPrivateDirectories(file); - } - - public static void createPrivateDirectories(File file) throws IOException { - if (file.exists()) { - return; - } - - final Path path = file.toPath(); - try { - Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); - Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms)); - } catch (UnsupportedOperationException e) { - Files.createDirectories(path); - } - } - - public static void createPrivateFile(String path) throws IOException { - final Path file = new File(path).toPath(); - try { - Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); - Files.createFile(file, PosixFilePermissions.asFileAttribute(perms)); - } catch (UnsupportedOperationException e) { - Files.createFile(file); - } - } - - public static String getDataHomeDir() { + public static File getDataHomeDir() { String dataHome = System.getenv("XDG_DATA_HOME"); if (dataHome != null) { - return dataHome; + return new File(dataHome); } - return System.getProperty("user.home") + "/.local/share"; - } - - public static void copyStreamToFile(InputStream input, File outputFile) throws IOException { - copyStreamToFile(input, outputFile, 8192); - } - - public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException { - try (OutputStream output = new FileOutputStream(outputFile)) { - byte[] buffer = new byte[bufferSize]; - int read; - - while ((read = input.read(buffer)) != -1) { - output.write(buffer, 0, read); - } - } + return new File(new File(System.getProperty("user.home"), ".local"), "share"); } } diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index bc2d3377..92bfae7b 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -1,15 +1,7 @@ package org.asamk.signal.util; -import com.fasterxml.jackson.databind.JsonNode; - -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.InvalidNumberException; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; -import org.whispersystems.signalservice.api.util.UuidUtil; -import org.whispersystems.util.Base64; - -import java.io.IOException; -import java.io.InvalidObjectException; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; public class Util { @@ -26,45 +18,7 @@ public class Util { return f.toString(); } - public static String join(CharSequence separator, Iterable list) { - StringBuilder buf = new StringBuilder(); - for (CharSequence str : list) { - if (buf.length() > 0) { - buf.append(separator); - } - buf.append(str); - } - - return buf.toString(); - } - - public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException { - JsonNode node = parent.get(name); - if (node == null) { - throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", - name)); - } - - return node; - } - - public static byte[] decodeGroupId(String groupId) throws GroupIdFormatException { - try { - return Base64.decode(groupId); - } catch (IOException e) { - throw new GroupIdFormatException(groupId, e); - } - } - - public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { - return PhoneNumberFormatter.formatNumber(number, localNumber); - } - - public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) { - if (UuidUtil.isUuid(identifier)) { - return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null); - } else { - return new SignalServiceAddress(null, identifier); - } + public static GroupId decodeGroupId(String groupId) throws GroupIdFormatException { + return GroupId.fromBase64(groupId); } }