diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index edf8a5a2..8736e619 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Setup Java JDK
- uses: actions/setup-java@v1.3.0
+ uses: actions/setup-java@v1
with:
java-version: 11
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 4953eaca..d72f15eb 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -28,14 +28,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..38907d6f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,42 @@
+# Changelog
+
+## [Unreleased]
+
+## [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
+
+## [0.7.0] - 2020-12-15
+### Added
+Support for groups of new type/v2
+- Sending and receiving
+- Updating name, avatar and adding members with `updateGroup`
+- Quit group and decline invitation with `quitGroup`
+- In the `listGroups` output v2 groups can be recognized by the longer groupId
+
+**Attention**: For the new group support to work the native libzkgroup library is required.
+See https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal for more information.
+
+### Fixed
+- Rare NullPointerException when receiving messages
+
+## [0.6.12] - 2020-11-22
+### Added
+- Show additional message content (view once, remote delete, mention, …) for received messages
+- `--captcha` parameter for `register` command, required for some IP ranges
+
+### Changed
+- Profile keys are now stored separately from contact list
+- Receipts from normal and unidentified messages now have the same format in json output
+
+### Fixed
+- Issue where some messages were sent with an old counter index
+
+## Older
+
+Look at the [release tags](https://github.com/AsamK/signal-cli/releases) for information about older releases.
diff --git a/build.gradle b/build.gradle
index ef9ed26f..1fbf5948 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_11
mainClassName = 'org.asamk.signal.Main'
-version = '0.6.11'
+version = '0.7.1'
compileJava.options.encoding = 'UTF-8'
@@ -17,11 +17,11 @@ repositories {
}
dependencies {
- implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_14'
- implementation 'org.bouncycastle:bcprov-jdk15on:1.66'
+ implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_15'
+ implementation 'org.bouncycastle:bcprov-jdk15on:1.67'
implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1'
- implementation 'com.github.hypfvieh:dbus-java:3.2.3'
- implementation 'org.slf4j:slf4j-nop:1.7.30'
+ implementation 'com.github.hypfvieh:dbus-java:3.2.4'
+ implementation 'org.slf4j:slf4j-simple:1.7.30'
}
jar {
diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc
index 98a5da2a..b5c22167 100644
--- a/man/signal-cli.1.adoc
+++ b/man/signal-cli.1.adoc
@@ -54,6 +54,12 @@ Use the verify command to complete the verification.
*-v*, *--voice*::
The verification should be done over voice, not SMS.
+*--captcha*::
+The captcha token, required if registration failed with a captcha required error.
+To get the token, go to https://signalcaptchas.org/registration/generate.html
+Check the developer tools for a redirect starting with signalcaptcha://
+Everything after signalcaptcha:// is the captcha token.
+
=== verify
Verify the number using the code received via SMS or voice.
@@ -172,9 +178,17 @@ 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.
+If the user is a pending member, this command will accept the group invitation.
*-g* GROUP, *--group* GROUP::
Specify the recipient group ID in base64 encoding.
@@ -192,6 +206,7 @@ Specify one or more members to add to the group.
=== quitGroup
Send a quit group message to all group members and remove self from member list.
+If the user is a pending member, this command will decline the group invitation.
*-g* GROUP, *--group* GROUP::
Specify the recipient group ID in base64 encoding.
@@ -229,7 +244,7 @@ Specify the safety number of the key, only use this option if you have verified
Update the name and avatar image visible by message recipients for the current users.
The profile is stored encrypted on the Signal servers.
-The decryption key is sent with every outgoing messages (excluding group messages).
+The decryption key is sent with every outgoing messages to contacts.
*--name*::
New name visible by message recipients.
diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java
index a93d6d86..317c70f2 100644
--- a/src/main/java/org/asamk/Signal.java
+++ b/src/main/java/org/asamk/Signal.java
@@ -13,13 +13,19 @@ import java.util.List;
*/
public interface Signal extends DBusInterface {
- long sendMessage(String message, List attachments, String recipient) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
+ long sendMessage(
+ String message, List attachments, String recipient
+ ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
- long sendMessage(String message, List attachments, List recipients) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
+ long sendMessage(
+ String message, List attachments, List recipients
+ ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
void sendEndSessionMessage(List recipients) throws Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
- long sendGroupMessage(String message, List attachments, byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid, Error.UnregisteredUser, Error.UntrustedIdentity;
+ long sendGroupMessage(
+ String message, List attachments, byte[] groupId
+ ) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid, Error.UnregisteredUser, Error.UntrustedIdentity;
String getContactName(String number) throws Error.InvalidNumber;
@@ -35,7 +41,9 @@ public interface Signal extends DBusInterface {
List getGroupMembers(byte[] groupId);
- byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.UnregisteredUser, Error.UntrustedIdentity;
+ byte[] updateGroup(
+ byte[] groupId, String name, List members, String avatar
+ ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.UnregisteredUser, Error.UntrustedIdentity;
boolean isRegistered();
@@ -47,7 +55,14 @@ public interface Signal extends DBusInterface {
private final String message;
private final List attachments;
- public MessageReceived(String objectpath, long timestamp, String sender, byte[] groupId, String message, List attachments) throws DBusException {
+ public MessageReceived(
+ String objectpath,
+ long timestamp,
+ String sender,
+ byte[] groupId,
+ String message,
+ List attachments
+ ) throws DBusException {
super(objectpath, timestamp, sender, groupId, message, attachments);
this.timestamp = timestamp;
this.sender = sender;
@@ -106,7 +121,15 @@ public interface Signal extends DBusInterface {
private final String message;
private final List attachments;
- public SyncMessageReceived(String objectpath, long timestamp, String source, String destination, byte[] groupId, String message, List attachments) throws DBusException {
+ public SyncMessageReceived(
+ String objectpath,
+ long timestamp,
+ String source,
+ String destination,
+ byte[] groupId,
+ String message,
+ List attachments
+ ) throws DBusException {
super(objectpath, timestamp, source, destination, groupId, message, attachments);
this.timestamp = timestamp;
this.source = source;
diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java
index 5973d019..50eb9f9b 100644
--- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java
+++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java
@@ -1,6 +1,7 @@
package org.asamk.signal;
import org.asamk.Signal;
+import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.manager.Manager;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
@@ -29,30 +30,33 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
this.objectPath = objectPath;
}
- static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, final String objectPath, Manager m) {
+ static void sendReceivedMessageToDbus(
+ SignalServiceEnvelope envelope,
+ SignalServiceContent content,
+ DBusConnection conn,
+ final String objectPath,
+ Manager m
+ ) {
if (envelope.isReceipt()) {
try {
- conn.sendMessage(new Signal.ReceiptReceived(
- objectPath,
- envelope.getTimestamp(),
+ conn.sendMessage(new Signal.ReceiptReceived(objectPath, envelope.getTimestamp(),
// A receipt envelope always has a source address
- envelope.getSourceAddress().getLegacyIdentifier()
- ));
+ envelope.getSourceAddress().getLegacyIdentifier()));
} catch (DBusException e) {
e.printStackTrace();
}
} else if (content != null) {
- final SignalServiceAddress sender = !envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceAddress() : content.getSender();
+ final SignalServiceAddress sender = !envelope.isUnidentifiedSender() && envelope.hasSource()
+ ? envelope.getSourceAddress()
+ : content.getSender();
if (content.getReceiptMessage().isPresent()) {
final SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get();
if (receiptMessage.isDeliveryReceipt()) {
for (long timestamp : receiptMessage.getTimestamps()) {
try {
- conn.sendMessage(new Signal.ReceiptReceived(
- objectPath,
+ conn.sendMessage(new Signal.ReceiptReceived(objectPath,
timestamp,
- sender.getLegacyIdentifier()
- ));
+ sender.getLegacyIdentifier()));
} catch (DBusException e) {
e.printStackTrace();
}
@@ -61,16 +65,17 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
- if (!message.isEndSession() &&
- !(message.getGroupContext().isPresent() &&
- message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) {
+ byte[] groupId = getGroupId(message);
+ if (!message.isEndSession() && (
+ groupId == null
+ || message.getGroupContext().get().getGroupV1Type() == null
+ || message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.DELIVER
+ )) {
try {
- conn.sendMessage(new Signal.MessageReceived(
- objectPath,
+ conn.sendMessage(new Signal.MessageReceived(objectPath,
message.getTimestamp(),
sender.getLegacyIdentifier(),
- message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()
- ? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0],
+ groupId != null ? groupId : new byte[0],
message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m)));
} catch (DBusException e) {
@@ -82,17 +87,20 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
if (sync_message.getSent().isPresent()) {
SentTranscriptMessage transcript = sync_message.getSent().get();
- if (transcript.getDestination().isPresent() || transcript.getMessage().getGroupContext().isPresent()) {
+ if (transcript.getDestination().isPresent() || transcript.getMessage()
+ .getGroupContext()
+ .isPresent()) {
SignalServiceDataMessage message = transcript.getMessage();
+ byte[] groupId = getGroupId(message);
try {
- conn.sendMessage(new Signal.SyncMessageReceived(
- objectPath,
+ conn.sendMessage(new Signal.SyncMessageReceived(objectPath,
transcript.getTimestamp(),
sender.getLegacyIdentifier(),
- transcript.getDestination().isPresent() ? transcript.getDestination().get().getLegacyIdentifier() : "",
- message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()
- ? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0],
+ transcript.getDestination().isPresent() ? transcript.getDestination()
+ .get()
+ .getLegacyIdentifier() : "",
+ groupId != null ? groupId : new byte[0],
message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m)));
} catch (DBusException e) {
@@ -104,6 +112,11 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
}
}
+ 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) {
List attachments = new ArrayList<>();
if (message.getAttachments().isPresent()) {
diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java
index dfe51fe7..363fc304 100644
--- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java
+++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java
@@ -35,7 +35,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 3a11ee24..6204778d 100644
--- a/src/main/java/org/asamk/signal/Main.java
+++ b/src/main/java/org/asamk/signal/Main.java
@@ -41,6 +41,8 @@ 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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
@@ -50,10 +52,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 +64,7 @@ public class Main {
System.exit(1);
}
- int res = handleCommands(ns);
+ int res = init(ns);
System.exit(res);
}
@@ -72,71 +74,81 @@ public class Main {
Security.addProvider(new BouncyCastleProvider());
}
- private static int handleCommands(Namespace ns) {
+ public static int init(Namespace ns) {
+ if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
+ return initDbusClient(ns, ns.getBoolean("dbus_system"));
+ }
+
final String username = ns.getString("username");
- 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 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();
- return 3;
- }
+ final File dataPath;
+ String config = ns.getString("config");
+ if (config != null) {
+ dataPath = new File(config);
} else {
- String dataPath = ns.getString("config");
- if (isEmpty(dataPath)) {
- dataPath = getDefaultDataPath();
- }
+ dataPath = getDefaultDataPath();
+ }
- final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT);
+ final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(
+ BaseConfig.USER_AGENT);
- if (username == null) {
- ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
- return handleCommands(ns, pm);
- }
+ if (!ServiceConfig.getCapabilities().isGv2()) {
+ logger.warn("WARNING: Support for new group V2 is disabled,"
+ + " because the required native library dependency is missing: libzkgroup");
+ }
- Manager manager;
+ if (username == null) {
+ ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
+ return handleCommands(ns, pm);
+ }
+
+ Manager manager;
+ try {
+ manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
+ } catch (Throwable e) {
+ logger.error("Error loading state file: {}", e.getMessage());
+ return 2;
+ }
+
+ 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 (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) {
+ 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) {
+ logger.error("Cleanup failed", e);
+ return 3;
+ }
+ }
- return handleCommands(ns, m);
- } catch (IOException e) {
- e.printStackTrace();
- return 3;
+ private static int initDbusClient(final Namespace ns, final boolean systemBus) {
+ try {
+ DBusConnection.DBusBusType busType;
+ if (systemBus) {
+ 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 handleCommands(ns, ts, dBusConn);
+ }
+ } catch (DBusException | IOException e) {
+ logger.error("Dbus client failed", e);
+ return 3;
}
}
@@ -199,19 +211,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;
}
@@ -219,39 +233,7 @@ public class Main {
}
private static Namespace parseArgs(String[] args) {
- ArgumentParser parser = ArgumentParsers.newFor("signal-cli")
- .build()
- .defaultHelp(true)
- .description("Commandline interface for Signal, patched to support sending messages from stdin and outputing reactions.")
- .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).");
-
- 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 {
@@ -284,4 +266,34 @@ 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).");
+
+ 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 e417acbd..99010e13 100644
--- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
+++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
@@ -1,5 +1,7 @@
package org.asamk.signal;
+import org.asamk.signal.manager.GroupId;
+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;
@@ -11,6 +13,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
@@ -22,6 +26,8 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
@@ -48,7 +54,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
SignalServiceAddress source = envelope.getSourceAddress();
ContactInfo sourceContact = m.getContact(source.getLegacyIdentifier());
- System.out.println(String.format("Envelope from: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + source.getLegacyIdentifier(), envelope.getSourceDevice()));
+ System.out.println(String.format("Envelope from: %s (device: %d)",
+ (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + source.getLegacyIdentifier(),
+ envelope.getSourceDevice()));
if (source.getRelay().isPresent()) {
System.out.println("Relayed by: " + source.getRelay().get());
}
@@ -66,18 +74,35 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (exception != null) {
if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
org.whispersystems.libsignal.UntrustedIdentityException e = (org.whispersystems.libsignal.UntrustedIdentityException) exception;
- System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
- System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getName() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getName() + "' to mark it as trusted");
- System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getName() + "' to trust it without verification");
+ System.out.println(
+ "The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
+ System.out.println("Use 'signal-cli -u "
+ + m.getUsername()
+ + " listIdentities -n "
+ + e.getName()
+ + "', verify the key and run 'signal-cli -u "
+ + m.getUsername()
+ + " trust -v \"FINGER_PRINT\" "
+ + e.getName()
+ + "' to mark it as trusted");
+ System.out.println("If you don't care about security, use 'signal-cli -u "
+ + m.getUsername()
+ + " trust -a "
+ + e.getName()
+ + "' to trust it without verification");
} else {
- System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")");
+ System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass()
+ .getSimpleName() + ")");
}
}
if (content == null) {
System.out.println("Failed to decrypt message.");
} else {
ContactInfo sourceContact = m.getContact(content.getSender().getLegacyIdentifier());
- System.out.println(String.format("Sender: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + content.getSender().getLegacyIdentifier(), content.getSenderDevice()));
+ System.out.println(String.format("Sender: %s (device: %d)",
+ (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + content.getSender()
+ .getLegacyIdentifier(),
+ content.getSenderDevice()));
if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
handleSignalServiceDataMessage(message);
@@ -103,7 +128,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Received sync read messages list");
for (ReadMessage rm : syncMessage.getRead().get()) {
ContactInfo fromContact = m.getContact(rm.getSender().getLegacyIdentifier());
- System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender().getLegacyIdentifier() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp()));
+ System.out.println("From: "
+ + (fromContact == null ? "" : "“" + fromContact.name + "” ")
+ + rm.getSender().getLegacyIdentifier()
+ + " Message timestamp: "
+ + DateUtils.formatTimestamp(rm.getTimestamp()));
}
}
if (syncMessage.getRequest().isPresent()) {
@@ -136,15 +165,19 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
StringBuilder toBuilder = new StringBuilder();
for (SignalServiceAddress dest : sentTranscriptMessage.getRecipients()) {
ContactInfo destContact = m.getContact(dest.getLegacyIdentifier());
- toBuilder.append(destContact == null ? "" : "“" + destContact.name + "” ").append(dest.getLegacyIdentifier()).append(" ");
+ toBuilder.append(destContact == null ? "" : "“" + destContact.name + "” ")
+ .append(dest.getLegacyIdentifier())
+ .append(" ");
}
to = toBuilder.toString();
} else {
to = "Unknown";
}
- System.out.println("To: " + to + " , Message timestamp: " + DateUtils.formatTimestamp(sentTranscriptMessage.getTimestamp()));
+ System.out.println("To: " + to + " , Message timestamp: " + DateUtils.formatTimestamp(
+ sentTranscriptMessage.getTimestamp()));
if (sentTranscriptMessage.getExpirationStartTimestamp() > 0) {
- System.out.println("Expiration started at: " + DateUtils.formatTimestamp(sentTranscriptMessage.getExpirationStartTimestamp()));
+ System.out.println("Expiration started at: " + DateUtils.formatTimestamp(
+ sentTranscriptMessage.getExpirationStartTimestamp()));
}
SignalServiceDataMessage message = sentTranscriptMessage.getMessage();
handleSignalServiceDataMessage(message);
@@ -160,15 +193,38 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (syncMessage.getVerified().isPresent()) {
System.out.println("Received sync message with verified identities:");
final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
- System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified());
- String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey()));
+ System.out.println(" - "
+ + verifiedMessage.getDestination()
+ + ": "
+ + verifiedMessage.getVerified());
+ String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(),
+ verifiedMessage.getIdentityKey()));
System.out.println(" " + safetyNumber);
}
if (syncMessage.getConfiguration().isPresent()) {
System.out.println("Received sync message with configuration:");
final ConfigurationMessage configurationMessage = syncMessage.getConfiguration().get();
if (configurationMessage.getReadReceipts().isPresent()) {
- System.out.println(" - Read receipts: " + (configurationMessage.getReadReceipts().get() ? "enabled" : "disabled"));
+ System.out.println(" - Read receipts: " + (
+ configurationMessage.getReadReceipts().get() ? "enabled" : "disabled"
+ ));
+ }
+ if (configurationMessage.getLinkPreviews().isPresent()) {
+ System.out.println(" - Link previews: " + (
+ configurationMessage.getLinkPreviews().get() ? "enabled" : "disabled"
+ ));
+ }
+ if (configurationMessage.getTypingIndicators().isPresent()) {
+ System.out.println(" - Typing indicators: " + (
+ configurationMessage.getTypingIndicators().get() ? "enabled" : "disabled"
+ ));
+ }
+ if (configurationMessage.getUnidentifiedDeliveryIndicators().isPresent()) {
+ System.out.println(" - Unidentified Delivery Indicators: " + (
+ configurationMessage.getUnidentifiedDeliveryIndicators().get()
+ ? "enabled"
+ : "disabled"
+ ));
}
}
if (syncMessage.getFetchType().isPresent()) {
@@ -182,7 +238,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println(" - Timestamp:" + viewOnceOpenMessage.getTimestamp());
}
if (syncMessage.getStickerPackOperations().isPresent()) {
- final List stickerPackOperationMessages = syncMessage.getStickerPackOperations().get();
+ final List stickerPackOperationMessages = syncMessage.getStickerPackOperations()
+ .get();
System.out.println("Received sync message with sticker pack operations:");
for (StickerPackOperationMessage m : stickerPackOperationMessages) {
System.out.println(" - " + m.getType().toString());
@@ -194,6 +251,32 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
}
}
+ if (syncMessage.getMessageRequestResponse().isPresent()) {
+ final MessageRequestResponseMessage requestResponseMessage = syncMessage.getMessageRequestResponse()
+ .get();
+ System.out.println("Received message request response:");
+ System.out.println(" Type: " + requestResponseMessage.getType());
+ if (requestResponseMessage.getGroupId().isPresent()) {
+ System.out.println(" Group id: " + Base64.encodeBytes(requestResponseMessage.getGroupId()
+ .get()));
+ }
+ if (requestResponseMessage.getPerson().isPresent()) {
+ System.out.println(" Person: " + requestResponseMessage.getPerson()
+ .get()
+ .getLegacyIdentifier());
+ }
+ }
+ if (syncMessage.getKeys().isPresent()) {
+ final KeysMessage keysMessage = syncMessage.getKeys().get();
+ System.out.println("Received sync message with keys:");
+ if (keysMessage.getStorageService().isPresent()) {
+ System.out.println(" With storage key length: " + keysMessage.getStorageService()
+ .get()
+ .serialize().length);
+ } else {
+ System.out.println(" With empty storage key");
+ }
+ }
}
if (content.getCallMessage().isPresent()) {
System.out.println("Received a call message");
@@ -213,7 +296,10 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (callMessage.getIceUpdateMessages().isPresent()) {
List iceUpdateMessages = callMessage.getIceUpdateMessages().get();
for (IceUpdateMessage iceUpdateMessage : iceUpdateMessages) {
- System.out.println("Ice update message: " + iceUpdateMessage.getId() + ", sdp: " + iceUpdateMessage.getSdp());
+ System.out.println("Ice update message: "
+ + iceUpdateMessage.getId()
+ + ", sdp: "
+ + iceUpdateMessage.getSdp());
}
}
if (callMessage.getOfferMessage().isPresent()) {
@@ -242,11 +328,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println(" - Action: " + typingMessage.getAction());
System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp()));
if (typingMessage.getGroupId().isPresent()) {
- GroupInfo group = m.getGroup(typingMessage.getGroupId().get());
+ System.out.println(" - Group Info:");
+ 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.name);
+ System.out.println(" Name: " + group.getTitle());
} else {
- System.out.println(" Name: ");
+ System.out.println(" Name: ");
}
}
}
@@ -259,38 +348,57 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
private void handleSignalServiceDataMessage(SignalServiceDataMessage message) {
System.out.println("Message timestamp: " + DateUtils.formatTimestamp(message.getTimestamp()));
+ if (message.isViewOnce()) {
+ System.out.println("=VIEW ONCE=");
+ }
if (message.getBody().isPresent()) {
System.out.println("Body: " + message.getBody().get());
}
- if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
- SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
+ if (message.getGroupContext().isPresent()) {
System.out.println("Group info:");
- System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
- if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
- System.out.println(" Name: " + groupInfo.getName().get());
- } else {
- GroupInfo group = m.getGroup(groupInfo.getGroupId());
+ 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: " + groupId.toBase64());
+ if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
+ System.out.println(" Name: " + groupInfo.getName().get());
+ } else {
+ GroupInfo group = m.getGroup(groupId);
+ if (group != null) {
+ System.out.println(" Name: " + group.getTitle());
+ } else {
+ System.out.println(" Name: ");
+ }
+ }
+ System.out.println(" Type: " + groupInfo.getType());
+ if (groupInfo.getMembers().isPresent()) {
+ for (SignalServiceAddress member : groupInfo.getMembers().get()) {
+ System.out.println(" Member: " + member.getLegacyIdentifier());
+ }
+ }
+ if (groupInfo.getAvatar().isPresent()) {
+ System.out.println(" Avatar:");
+ printAttachment(groupInfo.getAvatar().get());
+ }
+ } else if (groupContext.getGroupV2().isPresent()) {
+ final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get();
+ System.out.println(" Id: " + groupId.toBase64());
+ GroupInfo group = m.getGroup(groupId);
if (group != null) {
- System.out.println(" Name: " + group.name);
+ System.out.println(" Name: " + group.getTitle());
} else {
System.out.println(" Name: ");
}
- }
- System.out.println(" Type: " + groupInfo.getType());
- if (groupInfo.getMembers().isPresent()) {
- for (SignalServiceAddress member : groupInfo.getMembers().get()) {
- System.out.println(" Member: " + member.getLegacyIdentifier());
- }
- }
- if (groupInfo.getAvatar().isPresent()) {
- System.out.println(" Avatar:");
- printAttachment(groupInfo.getAvatar().get());
+ System.out.println(" Revision: " + groupInfo.getRevision());
+ System.out.println(" Master key length: " + groupInfo.getMasterKey().serialize().length);
+ System.out.println(" Has signed group change: " + groupInfo.hasSignedGroupChange());
}
}
if (message.getPreviews().isPresent()) {
final List previews = message.getPreviews().get();
- System.out.println("Previes:");
+ System.out.println("Previews:");
for (SignalServiceDataMessage.Preview preview : previews) {
System.out.println(" - Title: " + preview.getTitle());
System.out.println(" - Url: " + preview.getUrl());
@@ -332,7 +440,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
final SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
System.out.println("Reaction:");
System.out.println(" - Emoji: " + reaction.getEmoji());
- System.out.println(" - Target author: " + reaction.getTargetAuthor().getLegacyIdentifier()); // todo resolve
+ System.out.println(" - Target author: " + m.resolveSignalServiceAddress(reaction.getTargetAuthor())
+ .getLegacyIdentifier());
System.out.println(" - Target timestamp: " + reaction.getTargetSentTimestamp());
System.out.println(" - Is remove: " + reaction.isRemove());
}
@@ -340,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());
}
@@ -355,6 +470,17 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
}
+ if (message.getRemoteDelete().isPresent()) {
+ final SignalServiceDataMessage.RemoteDelete remoteDelete = message.getRemoteDelete().get();
+ System.out.println("Remote delete message: timestamp = " + remoteDelete.getTargetSentTimestamp());
+ }
+ if (message.getMentions().isPresent()) {
+ System.out.println("Mentions: ");
+ for (SignalServiceDataMessage.Mention mention : message.getMentions().get()) {
+ printMention(mention, m);
+ }
+ }
+
if (message.getAttachments().isPresent()) {
System.out.println("Attachments: ");
for (SignalServiceAttachment attachment : message.getAttachments().get()) {
@@ -363,13 +489,28 @@ 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" : "") + ")");
+ System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (
+ attachment.isStream() ? "Stream" : ""
+ ) + ")");
if (attachment.isPointer()) {
final SignalServiceAttachmentPointer pointer = attachment.asPointer();
System.out.println(" Id: " + pointer.getRemoteId() + " Key length: " + pointer.getKey().length);
- System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-"));
- System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
+ System.out.println(" Filename: " + (
+ pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-"
+ ));
+ System.out.println(" Size: " + (
+ pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : ""
+ ) + (
+ pointer.getPreview().isPresent() ? " (Preview is available: "
+ + pointer.getPreview().get().length
+ + " bytes)" : ""
+ ));
System.out.println(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no"));
System.out.println(" Dimensions: " + pointer.getWidth() + "x" + pointer.getHeight());
File file = m.getAttachmentFile(pointer.getRemoteId());
diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java
index 05f5c9ce..2a9bc4e9 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.GroupId;
+import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
-import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
@@ -13,12 +14,8 @@ public class BlockCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("contact")
- .help("Contact number")
- .nargs("*");
- subparser.addArgument("-g", "--group")
- .help("Group ID")
- .nargs("*");
+ subparser.addArgument("contact").help("Contact number").nargs("*");
+ subparser.addArgument("-g", "--group").help("Group ID").nargs("*");
subparser.help("Block the given contacts or groups (no messages will be received)");
}
@@ -40,7 +37,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 6e5a003a..6b7dc123 100644
--- a/src/main/java/org/asamk/signal/commands/Commands.java
+++ b/src/main/java/org/asamk/signal/commands/Commands.java
@@ -17,6 +17,7 @@ public class Commands {
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 2b983851..3caaaa37 100644
--- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java
+++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java
@@ -60,7 +60,13 @@ public class DaemonCommand implements LocalCommand {
}
boolean ignoreAttachments = ns.getBoolean("ignore_attachments");
try {
- m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, ns.getBoolean("json") ? new JsonDbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH) : new DbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH));
+ m.receiveMessages(1,
+ TimeUnit.HOURS,
+ false,
+ ignoreAttachments,
+ ns.getBoolean("json")
+ ? new JsonDbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH)
+ : new DbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH));
return 0;
} catch (IOException e) {
System.err.println("Error while receiving messages: " + e.getMessage());
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..8438e1fa
--- /dev/null
+++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java
@@ -0,0 +1,84 @@
+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.GroupId;
+import org.asamk.signal.manager.GroupInviteLinkUrl;
+import org.asamk.signal.manager.Manager;
+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) {
+ if (!m.isRegistered()) {
+ System.err.println("User is not registered.");
+ return 1;
+ }
+
+ 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/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java
index 45f59082..7cc9daf5 100644
--- a/src/main/java/org/asamk/signal/commands/LinkCommand.java
+++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java
@@ -16,8 +16,7 @@ public class LinkCommand implements ProvisioningCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("-n", "--name")
- .help("Specify a name to describe this new device.");
+ subparser.addArgument("-n", "--name").help("Specify a name to describe this new device.");
}
@Override
@@ -43,7 +42,11 @@ public class LinkCommand implements ProvisioningCommand {
e.printStackTrace();
return 2;
} catch (UserAlreadyExists e) {
- System.err.println("The user " + e.getUsername() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again.");
+ System.err.println("The user "
+ + e.getUsername()
+ + " already exists\nDelete \""
+ + e.getFileName()
+ + "\" before trying again.");
return 1;
}
return 0;
diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java
index d1590d7c..4b9dac5c 100644
--- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java
@@ -25,7 +25,10 @@ public class ListDevicesCommand implements LocalCommand {
try {
List devices = m.getLinkedDevices();
for (DeviceInfo d : devices) {
- System.out.println("Device " + d.getId() + (d.getId() == m.getDeviceId() ? " (this device)" : "") + ":");
+ System.out.println("Device "
+ + d.getId()
+ + (d.getId() == m.getDeviceId() ? " (this device)" : "")
+ + ":");
System.out.println(" Name: " + d.getName());
System.out.println(" Created: " + DateUtils.formatTimestamp(d.getCreated()));
System.out.println(" Last seen: " + DateUtils.formatTimestamp(d.getLastSeen()));
diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
index 0baa8744..4d1032a2 100644
--- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
@@ -4,29 +4,61 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
+import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.groups.GroupInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import org.whispersystems.util.Base64;
import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
public class ListGroupsCommand implements LocalCommand {
- private static void printGroup(GroupInfo group, boolean detailed, SignalServiceAddress address) {
+ private static void printGroup(Manager m, GroupInfo group, boolean detailed) {
if (detailed) {
- System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s",
- Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked, group.getMembersE164()));
+ Set members = group.getMembers()
+ .stream()
+ .map(m::resolveSignalServiceAddress)
+ .map(SignalServiceAddress::getLegacyIdentifier)
+ .collect(Collectors.toSet());
+
+ Set pendingMembers = group.getPendingMembers()
+ .stream()
+ .map(m::resolveSignalServiceAddress)
+ .map(SignalServiceAddress::getLegacyIdentifier)
+ .collect(Collectors.toSet());
+
+ Set requestingMembers = group.getRequestingMembers()
+ .stream()
+ .map(m::resolveSignalServiceAddress)
+ .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 Link: %s",
+ group.getGroupId().toBase64(),
+ group.getTitle(),
+ group.isMember(m.getSelfAddress()),
+ group.isBlocked(),
+ members,
+ pendingMembers,
+ requestingMembers,
+ groupInviteLink == null ? '-' : groupInviteLink.getUrl()));
} else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
- Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked));
+ group.getGroupId().toBase64(),
+ group.getTitle(),
+ group.isMember(m.getSelfAddress()),
+ group.isBlocked()));
}
}
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("-d", "--detailed").action(Arguments.storeTrue())
- .help("List members of each group");
+ subparser.addArgument("-d", "--detailed").action(Arguments.storeTrue()).help("List members of each group");
subparser.help("List group name and ids");
}
@@ -41,7 +73,7 @@ public class ListGroupsCommand implements LocalCommand {
boolean detailed = ns.getBoolean("detailed");
for (GroupInfo group : groups) {
- printGroup(group, detailed, m.getSelfAddress());
+ printGroup(m, group, detailed);
}
return 0;
}
diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java
index edb67c76..a75e4328 100644
--- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java
@@ -15,14 +15,17 @@ public class ListIdentitiesCommand implements LocalCommand {
private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity 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(),
- theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toString(theirId.getFingerprint()), digits));
+ System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s",
+ theirId.getAddress().getNumber().orNull(),
+ theirId.getTrustLevel(),
+ theirId.getDateAdded(),
+ Hex.toString(theirId.getFingerprint()),
+ digits));
}
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("-n", "--number")
- .help("Only show identity keys for the given phone number.");
+ subparser.addArgument("-n", "--number").help("Only show identity keys for the given phone number.");
}
@Override
diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
index 6db230f5..efc63f8f 100644
--- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
+++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
@@ -3,29 +3,30 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
+import org.asamk.signal.manager.GroupId;
+import org.asamk.signal.manager.GroupIdFormatException;
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.util.Util;
-import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
+import org.whispersystems.libsignal.util.Pair;
+import org.whispersystems.signalservice.api.messages.SendMessageResult;
import java.io.IOException;
+import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
-import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
+import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
public class QuitGroupCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("-g", "--group")
- .required(true)
- .help("Specify the recipient group ID.");
+ subparser.addArgument("-g", "--group").required(true).help("Specify the recipient group ID.");
}
@Override
@@ -36,14 +37,12 @@ public class QuitGroupCommand implements LocalCommand {
}
try {
- m.sendQuitGroupMessage(Util.decodeGroupId(ns.getString("group")));
- return 0;
+ final GroupId groupId = Util.decodeGroupId(ns.getString("group"));
+ final Pair> results = m.sendQuitGroupMessage(groupId);
+ return handleTimestampAndSendMessageResults(results.first(), results.second());
} catch (IOException e) {
handleIOException(e);
return 3;
- } catch (EncapsulatedExceptions e) {
- handleEncapsulatedExceptions(e);
- return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java
index bc3acbde..bc68565a 100644
--- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java
@@ -63,7 +63,9 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
}
} else {
System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n",
- messageReceived.getSender(), DateUtils.formatTimestamp(messageReceived.getTimestamp()), messageReceived.getMessage()));
+ messageReceived.getSender(),
+ DateUtils.formatTimestamp(messageReceived.getTimestamp()),
+ messageReceived.getMessage()));
if (messageReceived.getGroupId().length > 0) {
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(messageReceived.getGroupId()));
@@ -78,23 +80,23 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
}
});
- dbusconnection.addSigHandler(Signal.ReceiptReceived.class,
- receiptReceived -> {
- if (jsonProcessor != null) {
- JsonMessageEnvelope envelope = new JsonMessageEnvelope(receiptReceived);
- ObjectNode result = jsonProcessor.createObjectNode();
- result.putPOJO("envelope", envelope);
- try {
- jsonProcessor.writeValue(System.out, result);
- System.out.println();
- } catch (IOException e) {
- e.printStackTrace();
- }
- } else {
- System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n",
- receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp())));
- }
- });
+ dbusconnection.addSigHandler(Signal.ReceiptReceived.class, receiptReceived -> {
+ if (jsonProcessor != null) {
+ JsonMessageEnvelope envelope = new JsonMessageEnvelope(receiptReceived);
+ ObjectNode result = jsonProcessor.createObjectNode();
+ result.putPOJO("envelope", envelope);
+ try {
+ jsonProcessor.writeValue(System.out, result);
+ System.out.println();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n",
+ receiptReceived.getSender(),
+ DateUtils.formatTimestamp(receiptReceived.getTimestamp())));
+ }
+ });
dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> {
if (jsonProcessor != null) {
@@ -109,7 +111,10 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
}
} else {
System.out.print(String.format("Sync Envelope from: %s to: %s\nTimestamp: %s\nBody: %s\n",
- syncReceived.getSource(), syncReceived.getDestination(), DateUtils.formatTimestamp(syncReceived.getTimestamp()), syncReceived.getMessage()));
+ syncReceived.getSource(),
+ syncReceived.getDestination(),
+ DateUtils.formatTimestamp(syncReceived.getTimestamp()),
+ syncReceived.getMessage()));
if (syncReceived.getGroupId().length > 0) {
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(syncReceived.getGroupId()));
@@ -156,8 +161,14 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
}
boolean ignoreAttachments = ns.getBoolean("ignore_attachments");
try {
- final Manager.ReceiveMessageHandler handler = ns.getBoolean("json") ? new JsonReceiveMessageHandler(m) : new ReceiveMessageHandler(m);
- m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, ignoreAttachments, handler);
+ final Manager.ReceiveMessageHandler handler = ns.getBoolean("json")
+ ? new JsonReceiveMessageHandler(m)
+ : new ReceiveMessageHandler(m);
+ m.receiveMessages((long) (timeout * 1000),
+ TimeUnit.MILLISECONDS,
+ returnOnTimeout,
+ ignoreAttachments,
+ handler);
return 0;
} catch (IOException e) {
System.err.println("Error while receiving messages: " + e.getMessage());
diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java
index e95487bf..f69e0844 100644
--- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java
+++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java
@@ -16,15 +16,19 @@ public class RegisterCommand implements LocalCommand {
subparser.addArgument("-v", "--voice")
.help("The verification should be done over voice, not sms.")
.action(Arguments.storeTrue());
+ subparser.addArgument("--captcha")
+ .help("The captcha token, required if registration failed with a captcha required error.");
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
try {
- m.register(ns.getBoolean("voice"));
+ final boolean voiceVerification = ns.getBoolean("voice");
+ final String captcha = ns.getString("captcha");
+ m.register(voiceVerification, captcha);
return 0;
} catch (CaptchaRequiredException e) {
- System.err.println("Captcha required for verification (" + e.getMessage() + ")");
+ System.err.println("Captcha invalid or required for verification (" + e.getMessage() + ")");
return 1;
} catch (IOException e) {
System.err.println("Request verify error: " + e.getMessage());
diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java
index e82a1fee..896d21d6 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.GroupIdFormatException;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
@@ -22,16 +22,10 @@ public class SendCommand implements DbusCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("-g", "--group")
- .help("Specify the recipient group ID.");
- subparser.addArgument("recipient")
- .help("Specify the recipients' phone number.")
- .nargs("*");
- subparser.addArgument("-m", "--message")
- .help("Specify the message, if missing standard input is used.");
- subparser.addArgument("-a", "--attachment")
- .nargs("*")
- .help("Add file as attachment");
+ subparser.addArgument("-g", "--group").help("Specify the recipient group ID.");
+ subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*");
+ subparser.addArgument("-m", "--message").help("Specify the message, if missing standard input is used.");
+ subparser.addArgument("-a", "--attachment").nargs("*").help("Add file as attachment");
subparser.addArgument("-e", "--endsession")
.help("Clear session state and send end session message.")
.action(Arguments.storeTrue());
@@ -44,7 +38,9 @@ public class SendCommand implements DbusCommand {
return 1;
}
- if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && (ns.getBoolean("endsession") || ns.getString("group") == null)) {
+ if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && (
+ ns.getBoolean("endsession") || ns.getString("group") == null
+ )) {
System.err.println("No recipients given");
System.err.println("Aborting sending.");
return 1;
@@ -83,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/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
index 7e748866..345c9180 100644
--- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
@@ -4,34 +4,34 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
+import org.asamk.signal.manager.GroupId;
+import org.asamk.signal.manager.GroupIdFormatException;
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.util.Util;
-import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
+import org.whispersystems.libsignal.util.Pair;
+import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
+import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
-import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
+import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
public class SendReactionCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.help("Send reaction to a previously received or sent message.");
- subparser.addArgument("-g", "--group")
- .help("Specify the recipient group ID.");
- subparser.addArgument("recipient")
- .help("Specify the recipients' phone number.")
- .nargs("*");
+ subparser.addArgument("-g", "--group").help("Specify the recipient group ID.");
+ subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*");
subparser.addArgument("-e", "--emoji")
.required(true)
.help("Specify the emoji, should be a single unicode grapheme cluster.");
@@ -42,9 +42,7 @@ public class SendReactionCommand implements LocalCommand {
.required(true)
.type(long.class)
.help("Specify the timestamp of the message to which to react.");
- subparser.addArgument("-r", "--remove")
- .help("Remove a reaction.")
- .action(Arguments.storeTrue());
+ subparser.addArgument("-r", "--remove").help("Remove a reaction.").action(Arguments.storeTrue());
}
@Override
@@ -66,19 +64,21 @@ public class SendReactionCommand implements LocalCommand {
long targetTimestamp = ns.getLong("target_timestamp");
try {
+ final Pair> results;
if (ns.getString("group") != null) {
- byte[] groupId = Util.decodeGroupId(ns.getString("group"));
- m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
+ GroupId groupId = Util.decodeGroupId(ns.getString("group"));
+ results = m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
} else {
- m.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, ns.getList("recipient"));
+ results = m.sendMessageReaction(emoji,
+ isRemove,
+ targetAuthor,
+ targetTimestamp,
+ ns.getList("recipient"));
}
- return 0;
+ return handleTimestampAndSendMessageResults(results.first(), results.second());
} catch (IOException e) {
handleIOException(e);
return 3;
- } catch (EncapsulatedExceptions e) {
- handleEncapsulatedExceptions(e);
- return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java
index 2780dc46..076a86db 100644
--- a/src/main/java/org/asamk/signal/commands/TrustCommand.java
+++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java
@@ -16,9 +16,7 @@ public class TrustCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("number")
- .help("Specify the phone number, for which to set the trust.")
- .required(true);
+ subparser.addArgument("number").help("Specify the phone number, for which to set the trust.").required(true);
MutuallyExclusiveGroup mutTrust = subparser.addMutuallyExclusiveGroup();
mutTrust.addArgument("-a", "--trust-all-known-keys")
.help("Trust all known keys of this user, only use this for testing.")
@@ -49,7 +47,8 @@ public class TrustCommand implements LocalCommand {
try {
fingerprintBytes = Hex.toByteArray(safetyNumber.toLowerCase(Locale.ROOT));
} catch (Exception e) {
- System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters.");
+ System.err.println(
+ "Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters.");
return 1;
}
boolean res;
@@ -60,7 +59,8 @@ public class TrustCommand implements LocalCommand {
return 1;
}
if (!res) {
- System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct.");
+ System.err.println(
+ "Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct.");
return 1;
}
} else if (safetyNumber.length() == 60) {
@@ -72,15 +72,18 @@ public class TrustCommand implements LocalCommand {
return 1;
}
if (!res) {
- System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct.");
+ System.err.println(
+ "Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct.");
return 1;
}
} else {
- System.err.println("Safety number has invalid format, either specify the old hex fingerprint or the new safety number");
+ System.err.println(
+ "Safety number has invalid format, either specify the old hex fingerprint or the new safety number");
return 1;
}
} else {
- System.err.println("You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER");
+ System.err.println(
+ "You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER");
return 1;
}
}
diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java
index a95aa328..73e578ac 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.GroupId;
+import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
-import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
@@ -13,12 +14,8 @@ public class UnblockCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("contact")
- .help("Contact number")
- .nargs("*");
- subparser.addArgument("-g", "--group")
- .help("Group ID")
- .nargs("*");
+ subparser.addArgument("contact").help("Contact number").nargs("*");
+ subparser.addArgument("-g", "--group").help("Group ID").nargs("*");
subparser.help("Unblock the given contacts or groups (messages will be received again)");
}
@@ -40,7 +37,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/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java
index d7fa3893..da090209 100644
--- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java
@@ -12,11 +12,8 @@ public class UpdateContactCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("number")
- .help("Contact number");
- subparser.addArgument("-n", "--name")
- .required(true)
- .help("New contact name");
+ subparser.addArgument("number").help("Contact number");
+ subparser.addArgument("-n", "--name").required(true).help("New contact name");
subparser.addArgument("-e", "--expiration")
.required(false)
.type(int.class)
diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
index 925b8c90..dae06b86 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.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.util.Base64;
@@ -19,15 +19,10 @@ public class UpdateGroupCommand implements DbusCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("-g", "--group")
- .help("Specify the recipient group ID.");
- subparser.addArgument("-n", "--name")
- .help("Specify the new group name.");
- subparser.addArgument("-a", "--avatar")
- .help("Specify a new group avatar image file");
- subparser.addArgument("-m", "--member")
- .nargs("*")
- .help("Specify one or more members to add to the group");
+ subparser.addArgument("-g", "--group").help("Specify the recipient group ID.");
+ subparser.addArgument("-n", "--name").help("Specify the new group name.");
+ subparser.addArgument("-a", "--avatar").help("Specify a new group avatar image file");
+ subparser.addArgument("-m", "--member").nargs("*").help("Specify one or more members to add to the group");
}
@Override
@@ -40,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 218c8b77..1e332fb4 100644
--- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java
@@ -14,16 +14,11 @@ public class UpdateProfileCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup()
- .required(true);
- avatarOptions.addArgument("--avatar")
- .help("Path to new profile avatar");
- avatarOptions.addArgument("--remove-avatar")
- .action(Arguments.storeTrue());
+ final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup().required(true);
+ avatarOptions.addArgument("--avatar").help("Path to new profile avatar");
+ avatarOptions.addArgument("--remove-avatar").action(Arguments.storeTrue());
- subparser.addArgument("--name")
- .required(true)
- .help("New profile name");
+ subparser.addArgument("--name").required(true).help("New profile name");
subparser.help("Set a name and avatar image for the user profile");
}
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 0f336325..b6ad100b 100644
--- a/src/main/java/org/asamk/signal/commands/VerifyCommand.java
+++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java
@@ -12,10 +12,8 @@ public class VerifyCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.addArgument("verificationCode")
- .help("The verification code you received via sms or voice call.");
- subparser.addArgument("-p", "--pin")
- .help("The registration lock PIN, that was set by the user (Optional)");
+ subparser.addArgument("verificationCode").help("The verification code you received via sms or voice call.");
+ subparser.addArgument("-p", "--pin").help("The registration lock PIN, that was set by the user (Optional)");
}
@Override
@@ -30,7 +28,8 @@ public class VerifyCommand implements LocalCommand {
m.verifyAccount(verificationCode, pin);
return 0;
} catch (LockedException e) {
- System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: " + (e.getTimeRemaining() / 1000 / 60 / 60));
+ 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 3;
} catch (IOException e) {
diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
index 17cc2caa..cbb72835 100644
--- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
+++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
@@ -2,21 +2,23 @@ package org.asamk.signal.dbus;
import org.asamk.Signal;
import org.asamk.signal.manager.AttachmentInvalidException;
+import org.asamk.signal.manager.GroupId;
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.util.ErrorUtils;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
-import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
-import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
-import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
-import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
+import org.whispersystems.libsignal.util.Pair;
+import org.whispersystems.signalservice.api.messages.SendMessageResult;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
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;
public class DbusSignalImpl implements Signal {
@@ -43,41 +45,30 @@ public class DbusSignalImpl implements Signal {
return sendMessage(message, attachments, recipients);
}
- private static DBusExecutionException convertEncapsulatedExceptions(EncapsulatedExceptions e) {
- if (e.getNetworkExceptions().size() + e.getUnregisteredUserExceptions().size() + e.getUntrustedIdentityExceptions().size() == 1) {
- if (e.getNetworkExceptions().size() == 1) {
- NetworkFailureException n = e.getNetworkExceptions().get(0);
- return new Error.Failure("Network failure for \"" + n.getE164number() + "\": " + n.getMessage());
- } else if (e.getUnregisteredUserExceptions().size() == 1) {
- UnregisteredUserException n = e.getUnregisteredUserExceptions().get(0);
- return new Error.UnregisteredUser("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage());
- } else if (e.getUntrustedIdentityExceptions().size() == 1) {
- UntrustedIdentityException n = e.getUntrustedIdentityExceptions().get(0);
- return new Error.UntrustedIdentity("Untrusted Identity for \"" + n.getIdentifier() + "\": " + n.getMessage());
- }
+ private static void checkSendMessageResults(
+ long timestamp, List results
+ ) throws DBusExecutionException {
+ List errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
+ if (errors.size() == 0) {
+ return;
}
StringBuilder message = new StringBuilder();
- message.append("Failed to send (some) messages:").append('\n');
- for (NetworkFailureException n : e.getNetworkExceptions()) {
- message.append("Network failure for \"").append(n.getE164number()).append("\": ").append(n.getMessage()).append('\n');
- }
- for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) {
- message.append("Unregistered user \"").append(n.getE164Number()).append("\": ").append(n.getMessage()).append('\n');
- }
- for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) {
- message.append("Untrusted Identity for \"").append(n.getIdentifier()).append("\": ").append(n.getMessage()).append('\n');
+ message.append(timestamp).append('\n');
+ message.append("Failed to send (some) messages:\n");
+ for (String error : errors) {
+ message.append(error).append('\n');
}
- return new Error.Failure(message.toString());
+ throw new Error.Failure(message.toString());
}
@Override
public long sendMessage(final String message, final List attachments, final List recipients) {
try {
- return m.sendMessage(message, attachments, recipients);
- } catch (EncapsulatedExceptions e) {
- throw convertEncapsulatedExceptions(e);
+ final Pair> results = m.sendMessage(message, attachments, recipients);
+ checkSendMessageResults(results.first(), results.second());
+ return results.first();
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
} catch (AttachmentInvalidException e) {
@@ -90,11 +81,10 @@ public class DbusSignalImpl implements Signal {
@Override
public void sendEndSessionMessage(final List recipients) {
try {
- m.sendEndSessionMessage(recipients);
+ final Pair> results = m.sendEndSessionMessage(recipients);
+ checkSendMessageResults(results.first(), results.second());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
- } catch (EncapsulatedExceptions e) {
- throw convertEncapsulatedExceptions(e);
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
}
@@ -103,11 +93,13 @@ public class DbusSignalImpl implements Signal {
@Override
public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) {
try {
- return 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) {
throw new Error.Failure(e.getMessage());
- } catch (EncapsulatedExceptions e) {
- throw convertEncapsulatedExceptions(e);
} catch (GroupNotFoundException | NotAGroupMemberException e) {
throw new Error.GroupNotFound(e.getMessage());
} catch (AttachmentInvalidException e) {
@@ -145,7 +137,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());
}
@@ -156,39 +148,57 @@ 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 {
- return group.name;
+ return group.getTitle();
}
}
@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();
} else {
- return new ArrayList<>(group.getMembersE164());
+ return group.getMembers()
+ .stream()
+ .map(m::resolveSignalServiceAddress)
+ .map(SignalServiceAddress::getLegacyIdentifier)
+ .collect(Collectors.toList());
}
}
@Override
- public byte[] updateGroup(final byte[] groupId, final String name, final List members, final String avatar) {
+ public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) {
try {
- return m.updateGroup(groupId, name, members, avatar);
+ if (groupId.length == 0) {
+ groupId = null;
+ }
+ if (name.isEmpty()) {
+ name = null;
+ }
+ if (members.isEmpty()) {
+ members = null;
+ }
+ if (avatar.isEmpty()) {
+ avatar = null;
+ }
+ final Pair> results = m.updateGroup(groupId == null
+ ? null
+ : GroupId.unknownVersion(groupId), name, members, avatar);
+ checkSendMessageResults(0, results.second());
+ return results.first().serialize();
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
- } catch (EncapsulatedExceptions e) {
- throw convertEncapsulatedExceptions(e);
} catch (GroupNotFoundException | NotAGroupMemberException e) {
throw new Error.GroupNotFound(e.getMessage());
} catch (InvalidNumberException e) {
diff --git a/src/main/java/org/asamk/signal/json/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java
index 5364b9a5..9a975864 100644
--- a/src/main/java/org/asamk/signal/json/JsonDataMessage.java
+++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java
@@ -1,19 +1,17 @@
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.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
-// i think this is what you have to do to get another dict in json
-// but i'm not sure
class JsonReaction {
- String emoji; // unicode??
+ String emoji; // unicode?
String targetAuthor;
long targetTimestamp;
boolean isRemove;
@@ -32,50 +30,72 @@ class JsonDataMessage {
long timestamp;
String message;
int expiresInSeconds;
+
+ JsonReaction reaction;
+ JsonQuote quote;
+ List mentions;
List attachments;
JsonGroupInfo groupInfo;
JsonReaction reaction;
SignalServiceDataMessage.Quote quote;
- JsonDataMessage(SignalServiceDataMessage dataMessage) {
+ JsonDataMessage(SignalServiceDataMessage dataMessage, Manager m) {
this.timestamp = dataMessage.getTimestamp();
- if (dataMessage.getGroupContext().isPresent() && dataMessage.getGroupContext().get().getGroupV1().isPresent()) {
- SignalServiceGroup groupInfo = dataMessage.getGroupContext().get().getGroupV1().get();
- this.groupInfo = new JsonGroupInfo(groupInfo);
+ if (dataMessage.getGroupContext().isPresent()) {
+ if (dataMessage.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = dataMessage.getGroupContext().get().getGroupV1().get();
+ this.groupInfo = new JsonGroupInfo(groupInfo);
+ } else if (dataMessage.getGroupContext().get().getGroupV2().isPresent()) {
+ SignalServiceGroupV2 groupInfo = dataMessage.getGroupContext().get().getGroupV2().get();
+ this.groupInfo = new JsonGroupInfo(groupInfo);
+ }
}
if (dataMessage.getBody().isPresent()) {
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();
}
if (dataMessage.getReaction().isPresent()) {
final SignalServiceDataMessage.Reaction reaction = dataMessage.getReaction().get();
this.reaction = new JsonReaction(reaction);
-/* this.emoji = reaction.getEmoji();
- // comment on this line from ReceiveMessageHandler: todo resolve
+/* this.emoji = reaction.getEmoji();
this.targetAuthor = reaction.getTargetAuthor().getLegacyIdentifier();
this.targetTimestamp = reaction.getTargetSentTimestamp();
*/ } /*else {
this.reaction = null;
-/*
this.emoji = "";
this.targetAuthor = "";
this.targetTimestamp = 0;
-*/ // }
-/*
+ }
if (message.getQuote().isPresent()) {
SignalServiceDataMessage.Quote quote = message.getQuote().get();
System.out.println("Quote: (" + quote.getId() + ")");
- // there doesn't seem to be any fucking way to find a message's id?
+ // there doesn't seem to be any way to find a message's id?
System.out.println(" Author: " + quote.getAuthor().getLegacyIdentifier());
System.out.println(" Text: " + quote.getText());
}
@@ -84,27 +104,24 @@ class JsonDataMessage {
}
*/
}
- // very confusingly MessageReceived seems to be only made in JsonDbusReceiveMessageHandler
- // and only when *sending* to dbus, so to my current understanding this never gets called
- // which would suggest i'm not understanding something
public JsonDataMessage(Signal.MessageReceived messageReceived) {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
- attachments = messageReceived.getAttachments()
- .stream()
- .map(JsonAttachment::new)
- .collect(Collectors.toList());
+ reaction = null; // TODO Replace these 3 with the proper commands
+ quote = null;
+ mentions = null;
+ attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
}
- // i don't understand what SyncMessages are so i'm gonna ignore them
+ // i don't understand what SyncMessages are so i'm going to ignore them
// i think it only matters if you have multiple devices on your end
public JsonDataMessage(Signal.SyncMessageReceived messageReceived) {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
- attachments = messageReceived.getAttachments()
- .stream()
- .map(JsonAttachment::new)
- .collect(Collectors.toList());
+ 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 08bc19a9..9709be20 100644
--- a/src/main/java/org/asamk/signal/json/JsonGroupInfo.java
+++ b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java
@@ -1,6 +1,8 @@
package org.asamk.signal.json;
+import org.asamk.signal.manager.GroupUtils;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
@@ -28,6 +30,11 @@ class JsonGroupInfo {
this.type = groupInfo.getType().toString();
}
+ JsonGroupInfo(SignalServiceGroupV2 groupInfo) {
+ this.groupId = GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64();
+ this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER";
+ }
+
JsonGroupInfo(byte[] groupId) {
this.groupId = Base64.encodeBytes(groupId);
}
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 7ed75da9..9c796cf0 100644
--- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java
+++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java
@@ -2,24 +2,25 @@ package org.asamk.signal.json;
import org.asamk.Signal;
//import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
+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;
+import java.util.List;
+
public class JsonMessageEnvelope {
- // gotta do something so that it actually emits valid json instead of null
- // or just fix it on the python side i guess
String source;
int sourceDevice;
String relay;
long timestamp;
- boolean isReceipt;
JsonDataMessage dataMessage;
JsonSyncMessage syncMessage;
JsonCallMessage callMessage;
JsonReceiptMessage receiptMessage;
- // String typingAction;
- public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content) {
+ // String typingAction;
+
+ public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content, Manager m) {
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
SignalServiceAddress source = envelope.getSourceAddress();
this.source = source.getLegacyIdentifier();
@@ -27,17 +28,19 @@ public class JsonMessageEnvelope {
}
this.sourceDevice = envelope.getSourceDevice();
this.timestamp = envelope.getTimestamp();
- this.isReceipt = envelope.isReceipt();
+ if (envelope.isReceipt()) {
+ this.receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp));
+ }
if (content != null) {
if (envelope.isUnidentifiedSender()) {
this.source = content.getSender().getLegacyIdentifier();
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());
@@ -61,7 +64,7 @@ public class JsonMessageEnvelope {
public JsonMessageEnvelope(Signal.ReceiptReceived receiptReceived) {
source = receiptReceived.getSender();
timestamp = receiptReceived.getTimestamp();
- isReceipt = true;
+ receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp));
}
public JsonMessageEnvelope(Signal.SyncMessageReceived messageReceived) {
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/JsonReceiptMessage.java b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java
index 1b896053..ccd5960b 100644
--- a/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java
+++ b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java
@@ -22,4 +22,17 @@ class JsonReceiptMessage {
}
this.timestamps = receiptMessage.getTimestamps();
}
+
+ private JsonReceiptMessage(
+ final long when, final boolean isDelivery, final boolean isRead, final List timestamps
+ ) {
+ this.when = when;
+ this.isDelivery = isDelivery;
+ this.isRead = isRead;
+ this.timestamps = timestamps;
+ }
+
+ static JsonReceiptMessage deliveryReceipt(final long when, final List timestamps) {
+ return new JsonReceiptMessage(when, true, false, timestamps);
+ }
}
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/GroupId.java b/src/main/java/org/asamk/signal/manager/GroupId.java
new file mode 100644
index 00000000..34e18e8e
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/GroupId.java
@@ -0,0 +1,63 @@
+package org.asamk.signal.manager;
+
+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/GroupIdFormatException.java b/src/main/java/org/asamk/signal/manager/GroupIdFormatException.java
new file mode 100644
index 00000000..83afd15b
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/GroupIdFormatException.java
@@ -0,0 +1,8 @@
+package org.asamk.signal.manager;
+
+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/GroupIdV1.java b/src/main/java/org/asamk/signal/manager/GroupIdV1.java
new file mode 100644
index 00000000..40862f07
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/GroupIdV1.java
@@ -0,0 +1,14 @@
+package org.asamk.signal.manager;
+
+import static org.asamk.signal.manager.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/GroupIdV2.java b/src/main/java/org/asamk/signal/manager/GroupIdV2.java
new file mode 100644
index 00000000..b329be1d
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/GroupIdV2.java
@@ -0,0 +1,14 @@
+package org.asamk.signal.manager;
+
+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/GroupInviteLinkUrl.java b/src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java
new file mode 100644
index 00000000..67ce7892
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java
@@ -0,0 +1,140 @@
+package org.asamk.signal.manager;
+
+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/GroupLinkPassword.java b/src/main/java/org/asamk/signal/manager/GroupLinkPassword.java
new file mode 100644
index 00000000..38e2aaf4
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/GroupLinkPassword.java
@@ -0,0 +1,40 @@
+package org.asamk.signal.manager;
+
+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/GroupNotFoundException.java b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java
index 0c0d6d2d..d7efa923 100644
--- a/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java
+++ b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java
@@ -1,10 +1,8 @@
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));
+ 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/GroupUtils.java
new file mode 100644
index 00000000..d86dfbe9
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/GroupUtils.java
@@ -0,0 +1,68 @@
+package org.asamk.signal.manager;
+
+import org.asamk.signal.storage.groups.GroupInfo;
+import org.asamk.signal.storage.groups.GroupInfoV1;
+import org.asamk.signal.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 {
+
+ public static void setGroupContext(
+ final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo
+ ) {
+ if (groupInfo instanceof GroupInfoV1) {
+ SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
+ .withId(groupInfo.getGroupId().serialize())
+ .build();
+ messageBuilder.asGroupMessage(group);
+ } else {
+ final GroupInfoV2 groupInfoV2 = (GroupInfoV2) groupInfo;
+ SignalServiceGroupV2 group = SignalServiceGroupV2.newBuilder(groupInfoV2.getMasterKey())
+ .withRevision(groupInfoV2.getGroup() == null ? 0 : groupInfoV2.getGroup().getRevision())
+ .build();
+ messageBuilder.asGroupMessage(group);
+ }
+ }
+
+ 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 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(groupIdV1.serialize(),
+ "GV2 Migration".getBytes(),
+ GroupMasterKey.SIZE));
+ } catch (InvalidInputException e) {
+ throw new AssertionError(e);
+ }
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/HandleAction.java b/src/main/java/org/asamk/signal/manager/HandleAction.java
index 2ef99062..aa25d8c5 100644
--- a/src/main/java/org/asamk/signal/manager/HandleAction.java
+++ b/src/main/java/org/asamk/signal/manager/HandleAction.java
@@ -2,7 +2,6 @@ package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import java.util.Arrays;
import java.util.Objects;
interface HandleAction {
@@ -30,8 +29,7 @@ class SendReceiptAction implements HandleAction {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendReceiptAction that = (SendReceiptAction) o;
- return timestamp == that.timestamp &&
- address.equals(that.address);
+ return timestamp == that.timestamp && address.equals(that.address);
}
@Override
@@ -94,9 +92,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;
}
@@ -110,15 +108,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;
}
}
@@ -126,9 +126,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;
}
@@ -142,15 +142,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/IasTrustStore.java b/src/main/java/org/asamk/signal/manager/IasTrustStore.java
new file mode 100644
index 00000000..f9bbb0b3
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/IasTrustStore.java
@@ -0,0 +1,18 @@
+package org.asamk.signal.manager;
+
+import org.whispersystems.signalservice.api.push.TrustStore;
+
+import java.io.InputStream;
+
+class IasTrustStore implements TrustStore {
+
+ @Override
+ public InputStream getKeyStoreInputStream() {
+ return IasTrustStore.class.getResourceAsStream("ias.store");
+ }
+
+ @Override
+ public String getKeyStorePassword() {
+ return "whisper";
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java
index fff8179c..21f6037f 100644
--- a/src/main/java/org/asamk/signal/manager/KeyUtils.java
+++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java
@@ -26,14 +26,6 @@ class KeyUtils {
return getSecret(18);
}
- static byte[] createGroupId() {
- return getSecretBytes(16);
- }
-
- static byte[] createUnrestrictedUnidentifiedAccess() {
- return getSecretBytes(16);
- }
-
static byte[] createStickerUploadKey() {
return getSecretBytes(32);
}
@@ -43,7 +35,7 @@ class KeyUtils {
return Base64.encodeBytes(secret);
}
- private static byte[] getSecretBytes(int size) {
+ 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 855f534e..be0789c5 100644
--- a/src/main/java/org/asamk/signal/manager/Manager.java
+++ b/src/main/java/org/asamk/signal/manager/Manager.java
@@ -18,13 +18,18 @@ package org.asamk.signal.manager;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.asamk.signal.manager.helper.GroupHelper;
+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.JsonGroupStore;
+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.signal.libsignal.metadata.InvalidMetadataMessageException;
@@ -38,10 +43,20 @@ 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.InvalidCertificateException;
+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;
+import org.signal.zkgroup.auth.AuthCredentialResponse;
+import org.signal.zkgroup.groups.GroupMasterKey;
+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;
@@ -63,10 +78,12 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
-import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
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.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
@@ -77,6 +94,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload;
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload.StickerInfo;
@@ -92,20 +110,22 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
+import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
-import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
-import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.StreamDetails;
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.contacts.crypto.Quote;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
+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;
@@ -126,22 +146,24 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
+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.LinkedList;
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.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -149,30 +171,67 @@ import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
+import static org.asamk.signal.manager.ServiceConfig.CDS_MRENCLAVE;
import static org.asamk.signal.manager.ServiceConfig.capabilities;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
+import static org.asamk.signal.manager.ServiceConfig.getIasKeyStore;
public class Manager implements Closeable {
+ final static Logger logger = LoggerFactory.getLogger(Manager.class);
+
private final SleepTimer timer = new UptimeSleepTimer();
+
private final SignalServiceConfiguration serviceConfiguration;
private final String userAgent;
+ private final boolean discoverableByPhoneNumber = true;
+ private final boolean unrestrictedUnidentifiedAccess = false;
private final SignalAccount account;
private final PathConfig pathConfig;
private SignalServiceAccountManager accountManager;
+ private GroupsV2Api groupsV2Api;
+ private final GroupsV2Operations groupsV2Operations;
+
+ private SignalServiceMessageReceiver messageReceiver = null;
private SignalServiceMessagePipe messagePipe = null;
private SignalServiceMessagePipe unidentifiedMessagePipe = null;
- private boolean discoverableByPhoneNumber = true;
- public Manager(SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, String userAgent) {
+ private final UnidentifiedAccessHelper unidentifiedAccessHelper;
+ private final ProfileHelper profileHelper;
+ private final GroupHelper groupHelper;
+
+ public Manager(
+ SignalAccount account,
+ PathConfig pathConfig,
+ SignalServiceConfiguration serviceConfiguration,
+ String userAgent
+ ) {
this.account = account;
this.pathConfig = pathConfig;
this.serviceConfiguration = serviceConfiguration;
this.userAgent = userAgent;
+ this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create(
+ serviceConfiguration)) : null;
this.accountManager = createSignalServiceAccountManager();
+ this.groupsV2Api = accountManager.getGroupsV2Api();
this.account.setResolver(this::resolveSignalServiceAddress);
+
+ this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
+ account.getProfileStore()::getProfileKey,
+ this::getRecipientProfile,
+ this::getSenderCertificate);
+ this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
+ unidentifiedAccessHelper::getAccessFor,
+ unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
+ this::getOrCreateMessageReceiver);
+ this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
+ this::getRecipientProfile,
+ account::getSelfAddress,
+ groupsV2Operations,
+ groupsV2Api,
+ this::getGroupAuthForToday);
}
public String getUsername() {
@@ -184,14 +243,12 @@ public class Manager implements Closeable {
}
private SignalServiceAccountManager createSignalServiceAccountManager() {
- GroupsV2Operations groupsV2Operations;
- try {
- groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration));
- } catch (Throwable ignored) {
- groupsV2Operations = null;
- }
return new SignalServiceAccountManager(serviceConfiguration,
- new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), account.getPassword(), null, account.getDeviceId()),
+ new DynamicCredentialsProvider(account.getUuid(),
+ account.getUsername(),
+ account.getPassword(),
+ null,
+ account.getDeviceId()),
userAgent,
groupsV2Operations,
timer);
@@ -205,25 +262,27 @@ public class Manager implements Closeable {
return account.getDeviceId();
}
- private String getMessageCachePath() {
- return pathConfig.getDataPath() + "/" + account.getUsername() + ".d/msg-cache";
+ private File getMessageCachePath() {
+ return SignalAccount.getMessageCachePath(pathConfig.getDataPath(), account.getUsername());
}
- private String getMessageCachePath(String sender) {
+ private File getMessageCachePath(String sender) {
if (sender == null || sender.isEmpty()) {
return getMessageCachePath();
}
- return getMessageCachePath() + "/" + sender.replace("/", "_");
+ return new File(getMessageCachePath(), sender.replace("/", "_"));
}
private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException {
- String cachePath = getMessageCachePath(sender);
+ File cachePath = getMessageCachePath(sender);
IOUtils.createPrivateDirectories(cachePath);
- return new File(cachePath + "/" + now + "_" + timestamp);
+ return new File(cachePath, now + "_" + timestamp);
}
- public static Manager init(String username, String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) throws IOException {
+ public static Manager init(
+ String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent
+ ) throws IOException {
PathConfig pathConfig = PathConfig.createDefault(settingsPath);
if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
@@ -231,7 +290,11 @@ public class Manager implements Closeable {
int registrationId = KeyHelper.generateRegistrationId(false);
ProfileKey profileKey = KeyUtils.createProfileKey();
- SignalAccount account = SignalAccount.create(pathConfig.getDataPath(), username, identityKey, registrationId, profileKey);
+ SignalAccount account = SignalAccount.create(pathConfig.getDataPath(),
+ username,
+ identityKey,
+ registrationId,
+ profileKey);
account.save();
return new Manager(account, pathConfig, serviceConfiguration, userAgent);
@@ -247,29 +310,28 @@ public class Manager implements Closeable {
}
private void migrateLegacyConfigs() {
- // Copy group avatars that were previously stored in the attachments folder
- // to the new avatar folder
- if (JsonGroupStore.groupsWithLegacyAvatarId.size() > 0) {
- for (GroupInfo g : JsonGroupStore.groupsWithLegacyAvatarId) {
- File avatarFile = getGroupAvatarFile(g.groupId);
- File attachmentFile = getAttachmentFile(new SignalServiceAttachmentRemoteId(g.getAvatarId()));
- if (!avatarFile.exists() && attachmentFile.exists()) {
- try {
- IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
- Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
- } catch (Exception e) {
- // Ignore
- }
- }
- }
- JsonGroupStore.groupsWithLegacyAvatarId.clear();
- account.save();
- }
- if (account.getProfileKey() == null) {
+ if (account.getProfileKey() == null && isRegistered()) {
// Old config file, creating new profile key
account.setProfileKey(KeyUtils.createProfileKey());
account.save();
}
+ // 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());
}
public void checkAccountState() throws IOException {
@@ -282,6 +344,7 @@ public class Manager implements Closeable {
account.setUuid(accountManager.getOwnUuid());
account.save();
}
+ updateAccountAttributes();
}
}
@@ -289,17 +352,20 @@ public class Manager implements Closeable {
return account.isRegistered();
}
- public void register(boolean voiceVerification) throws IOException {
+ public void register(boolean voiceVerification, String captcha) throws IOException {
account.setPassword(KeyUtils.createPassword());
// Resetting UUID, because registering doesn't work otherwise
account.setUuid(null);
accountManager = createSignalServiceAccountManager();
+ this.groupsV2Api = accountManager.getGroupsV2Api();
if (voiceVerification) {
- accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent(), Optional.absent());
+ accountManager.requestVoiceVerificationCode(Locale.getDefault(),
+ Optional.fromNullable(captcha),
+ Optional.absent());
} else {
- accountManager.requestSmsVerificationCode(false, Optional.absent(), Optional.absent());
+ accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent());
}
account.setRegistered(false);
@@ -307,7 +373,15 @@ public class Manager implements Closeable {
}
public void updateAccountAttributes() throws IOException {
- accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber);
+ accountManager.setAccountAttributes(account.getSignalingKey(),
+ account.getSignalProtocolStore().getLocalRegistrationId(),
+ true,
+ account.getRegistrationLockPin(),
+ account.getRegistrationLock(),
+ unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
+ unrestrictedUnidentifiedAccess,
+ capabilities,
+ discoverableByPhoneNumber);
}
public void setProfile(String name, File avatar) throws IOException {
@@ -350,7 +424,11 @@ public class Manager implements Closeable {
IdentityKeyPair identityKeyPair = getIdentityKeyPair();
String verificationCode = accountManager.getNewDeviceVerificationCode();
- accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey().serialize()), verificationCode);
+ accountManager.addDevice(deviceIdentifier,
+ deviceKey,
+ identityKeyPair,
+ Optional.of(account.getProfileKey().serialize()),
+ verificationCode);
account.setMultiDevice(true);
account.save();
}
@@ -376,8 +454,12 @@ public class Manager implements Closeable {
private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
try {
ECKeyPair keyPair = Curve.generateKeyPair();
- byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
- SignedPreKeyRecord record = new SignedPreKeyRecord(account.getNextSignedPreKeyId(), System.currentTimeMillis(), keyPair, signature);
+ byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(),
+ keyPair.getPublicKey().serialize());
+ SignedPreKeyRecord record = new SignedPreKeyRecord(account.getNextSignedPreKeyId(),
+ System.currentTimeMillis(),
+ keyPair,
+ signature);
account.addSignedPreKey(record);
account.save();
@@ -392,7 +474,16 @@ public class Manager implements Closeable {
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, getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber);
+ 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()
@@ -400,7 +491,10 @@ public class Manager implements Closeable {
account.setRegistered(true);
account.setUuid(uuid);
account.setRegistrationLockPin(pin);
- account.getSignalProtocolStore().saveIdentity(account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), TrustLevel.TRUSTED_VERIFIED);
+ account.getSignalProtocolStore()
+ .saveIdentity(account.getSelfAddress(),
+ getIdentityKeyPair().getPublicKey(),
+ TrustLevel.TRUSTED_VERIFIED);
refreshPreKeys();
account.save();
@@ -425,69 +519,166 @@ public class Manager implements Closeable {
accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys);
}
- private SignalServiceMessageReceiver getMessageReceiver() {
- // TODO implement ZkGroup support
- final ClientZkProfileOperations clientZkProfileOperations = null;
- return new SignalServiceMessageReceiver(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), userAgent, null, timer, clientZkProfileOperations);
+ 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 SignalServiceMessageSender getMessageSender() {
- // TODO implement ZkGroup support
- final ClientZkProfileOperations clientZkProfileOperations = null;
- final boolean attachmentsV3 = false;
+ private SignalServiceMessageReceiver getOrCreateMessageReceiver() {
+ if (messageReceiver == null) {
+ messageReceiver = createMessageReceiver();
+ }
+ return messageReceiver;
+ }
+
+ private SignalServiceMessagePipe getOrCreateMessagePipe() {
+ if (messagePipe == null) {
+ messagePipe = getOrCreateMessageReceiver().createMessagePipe();
+ }
+ return messagePipe;
+ }
+
+ private SignalServiceMessagePipe getOrCreateUnidentifiedMessagePipe() {
+ if (unidentifiedMessagePipe == null) {
+ unidentifiedMessagePipe = getOrCreateMessageReceiver().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(), account.getUsername(), account.getPassword(),
- account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations, executor);
+ return new SignalServiceMessageSender(serviceConfiguration,
+ account.getUuid(),
+ account.getUsername(),
+ account.getPassword(),
+ account.getDeviceId(),
+ account.getSignalProtocolStore(),
+ userAgent,
+ account.isMultiDevice(),
+ Optional.fromNullable(messagePipe),
+ Optional.fromNullable(unidentifiedMessagePipe),
+ Optional.absent(),
+ clientZkProfileOperations,
+ executor,
+ ServiceConfig.MAX_ENVELOPE_SIZE);
}
- private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess) throws IOException {
- SignalServiceMessagePipe pipe = unidentifiedMessagePipe != null && unidentifiedAccess.isPresent() ? unidentifiedMessagePipe
- : messagePipe;
-
- if (pipe != null) {
- try {
- return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile();
- } catch (IOException | InterruptedException | ExecutionException | TimeoutException ignored) {
- }
- }
-
- SignalServiceMessageReceiver receiver = getMessageReceiver();
- try {
- return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile();
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- throw new IOException("Failed to retrieve profile", e);
- }
+ private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address) throws IOException {
+ return profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE).getProfile();
}
- private SignalProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException {
- SignalProfileEntry profileEntry = account.getProfileStore().getProfile(address);
+ private SignalProfile getRecipientProfile(
+ SignalServiceAddress address
+ ) {
+ SignalProfileEntry profileEntry = account.getProfileStore().getProfileEntry(address);
+ if (profileEntry == null) {
+ return null;
+ }
long now = new Date().getTime();
// Profiles are cache for 24h before retrieving them again
- if (profileEntry == null || profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) {
- SignalProfile profile = retrieveRecipientProfile(address, unidentifiedAccess, profileKey);
- account.getProfileStore().updateProfile(address, profileKey, now, profile);
+ if (!profileEntry.isRequestPending() && (
+ profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000
+ )) {
+ ProfileKey profileKey = profileEntry.getProfileKey();
+ profileEntry.setRequestPending(true);
+ SignalProfile profile;
+ try {
+ profile = retrieveRecipientProfile(address, profileKey);
+ } catch (IOException e) {
+ logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
+ profileEntry.setRequestPending(false);
+ return null;
+ }
+ profileEntry.setRequestPending(false);
+ account.getProfileStore()
+ .updateProfile(address, profileKey, now, profile, profileEntry.getProfileKeyCredential());
return profile;
}
return profileEntry.getProfile();
}
- private SignalProfile retrieveRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException {
- final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address, unidentifiedAccess);
+ private ProfileKeyCredential getRecipientProfileKeyCredential(SignalServiceAddress address) {
+ SignalProfileEntry profileEntry = account.getProfileStore().getProfileEntry(address);
+ if (profileEntry == null) {
+ return null;
+ }
+ if (profileEntry.getProfileKeyCredential() == null) {
+ ProfileAndCredential profileAndCredential;
+ try {
+ profileAndCredential = profileHelper.retrieveProfileSync(address,
+ SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
+ } catch (IOException e) {
+ logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
+ return null;
+ }
+ long now = new Date().getTime();
+ final ProfileKeyCredential profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
+ final SignalProfile profile = decryptProfile(address,
+ profileEntry.getProfileKey(),
+ profileAndCredential.getProfile());
+ account.getProfileStore()
+ .updateProfile(address, profileEntry.getProfileKey(), now, profile, profileKeyCredential);
+ return profileKeyCredential;
+ }
+ return profileEntry.getProfileKeyCredential();
+ }
+
+ private SignalProfile retrieveRecipientProfile(
+ SignalServiceAddress address, ProfileKey profileKey
+ ) throws IOException {
+ final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address);
+
+ return decryptProfile(address, profileKey, encryptedProfile);
+ }
+
+ private SignalProfile decryptProfile(
+ final SignalServiceAddress address, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
+ ) {
File avatarFile = null;
try {
- avatarFile = encryptedProfile.getAvatar() == null ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
+ avatarFile = encryptedProfile.getAvatar() == null
+ ? 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);
try {
- return new SignalProfile(
- encryptedProfile.getIdentityKey(),
- encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))),
+ String name;
+ try {
+ name = encryptedProfile.getName() == null
+ ? null
+ : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName())));
+ } catch (IOException e) {
+ name = null;
+ }
+ String unidentifiedAccess;
+ try {
+ unidentifiedAccess = encryptedProfile.getUnidentifiedAccess() == null
+ || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess()))
+ ? null
+ : encryptedProfile.getUnidentifiedAccess();
+ } catch (IOException e) {
+ unidentifiedAccess = null;
+ }
+ return new SignalProfile(encryptedProfile.getIdentityKey(),
+ name,
avatarFile,
- encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(),
+ unidentifiedAccess,
encryptedProfile.isUnrestrictedUnidentifiedAccess(),
encryptedProfile.getCapabilities());
} catch (InvalidCiphertextException e) {
@@ -495,7 +686,7 @@ 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();
@@ -513,13 +704,24 @@ public class Manager implements Closeable {
return Optional.of(Utils.createAttachment(file));
}
- private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException {
+ private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
GroupInfo g = account.getGroupStore().getGroup(groupId);
if (g == null) {
throw new GroupNotFoundException(groupId);
}
if (!g.isMember(account.getSelfAddress())) {
- throw new NotAGroupMemberException(groupId, g.name);
+ throw new NotAGroupMemberException(groupId, g.getTitle());
+ }
+ return g;
+ }
+
+ private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
+ GroupInfo g = account.getGroupStore().getGroup(groupId);
+ if (g == null) {
+ throw new GroupNotFoundException(groupId);
+ }
+ if (!g.isMember(account.getSelfAddress()) && !g.isPendingMember(account.getSelfAddress())) {
+ throw new NotAGroupMemberException(groupId, g.getTitle());
}
return g;
}
@@ -528,68 +730,180 @@ public class Manager implements Closeable {
return account.getGroupStore().getGroups();
}
- public long sendGroupMessage(String messageText, List attachments,
- byte[] groupId)
- throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
- final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
+ public Pair> sendGroupMessage(
+ SignalServiceDataMessage.Builder messageBuilder, GroupId groupId
+ ) throws IOException, GroupNotFoundException, NotAGroupMemberException {
+ final GroupInfo g = getGroupForSending(groupId);
+
+ GroupUtils.setGroupContext(messageBuilder, g);
+ messageBuilder.withExpiration(g.getMessageExpirationTime());
+
+ return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
+ }
+
+ public Pair> sendGroupMessage(
+ 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));
}
- if (groupId != null) {
- SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
- .withId(groupId)
- .build();
- messageBuilder.asGroupMessage(group);
- }
- final GroupInfo g = getGroupForSending(groupId);
-
- messageBuilder.withExpiration(g.messageExpirationTime);
-
- return sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
+ return sendGroupMessage(messageBuilder, groupId);
}
- public void sendGroupMessageReaction(String emoji, boolean remove, String targetAuthor,
- long targetSentTimestamp, byte[] groupId)
- throws IOException, EncapsulatedExceptions, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException {
- SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp);
+ public Pair> sendGroupMessageReaction(
+ String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId
+ ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException {
+ SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji,
+ remove,
+ canonicalizeAndResolveSignalServiceAddress(targetAuthor),
+ targetSentTimestamp);
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withReaction(reaction);
- if (groupId != null) {
- SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
- .withId(groupId)
+
+ return sendGroupMessage(messageBuilder, groupId);
+ }
+
+ public Pair> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
+
+ SignalServiceDataMessage.Builder messageBuilder;
+
+ final GroupInfo g = getGroupForUpdating(groupId);
+ if (g instanceof GroupInfoV1) {
+ GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
+ SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
+ .withId(groupId.serialize())
.build();
- messageBuilder.asGroupMessage(group);
+ messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
+ groupInfoV1.removeMember(account.getSelfAddress());
+ account.getGroupStore().updateGroup(groupInfoV1);
+ } else {
+ final GroupInfoV2 groupInfoV2 = (GroupInfoV2) g;
+ final Pair groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2);
+ groupInfoV2.setGroup(groupGroupChangePair.first());
+ messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
+ account.getGroupStore().updateGroup(groupInfoV2);
}
- final GroupInfo g = getGroupForSending(groupId);
- sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
+
+ return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
- public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, NotAGroupMemberException {
- SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
- .withId(groupId)
- .build();
-
- SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
- .asGroupMessage(group);
-
- final GroupInfo g = getGroupForSending(groupId);
- g.removeMember(account.getSelfAddress());
- account.getGroupStore().updateGroup(g);
-
- sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
- }
-
- private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
+ private Pair> sendUpdateGroupMessage(
+ GroupId groupId, String name, Collection members, String avatarFile
+ ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
GroupInfo g;
+ SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) {
// Create new group
- g = new GroupInfo(KeyUtils.createGroupId());
- g.addMembers(Collections.singleton(account.getSelfAddress()));
+ GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile);
+ if (gv2 == null) {
+ GroupInfoV1 gv1 = new GroupInfoV1(GroupIdV1.createRandom());
+ gv1.addMembers(Collections.singleton(account.getSelfAddress()));
+ updateGroupV1(gv1, name, members, avatarFile);
+ messageBuilder = getGroupUpdateMessageBuilder(gv1);
+ g = gv1;
+ } else {
+ messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
+ g = gv2;
+ }
} else {
- g = getGroupForSending(groupId);
+ GroupInfo group = getGroupForUpdating(groupId);
+ if (group instanceof GroupInfoV2) {
+ final GroupInfoV2 groupInfoV2 = (GroupInfoV2) group;
+
+ Pair> result = null;
+ if (groupInfoV2.isPendingMember(getSelfAddress())) {
+ Pair groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
+ result = sendUpdateGroupMessage(groupInfoV2,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
+ }
+
+ if (members != null) {
+ final Set newMembers = new HashSet<>(members);
+ newMembers.removeAll(group.getMembers()
+ .stream()
+ .map(this::resolveSignalServiceAddress)
+ .collect(Collectors.toSet()));
+ if (newMembers.size() > 0) {
+ Pair groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2,
+ newMembers);
+ result = sendUpdateGroupMessage(groupInfoV2,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
+ }
+ }
+ if (result == null || name != null || avatarFile != null) {
+ Pair groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2,
+ name,
+ avatarFile);
+ result = sendUpdateGroupMessage(groupInfoV2,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
+ }
+
+ return new Pair<>(group.getGroupId(), result.second());
+ } else {
+ GroupInfoV1 gv1 = (GroupInfoV1) group;
+ updateGroupV1(gv1, name, members, avatarFile);
+ messageBuilder = getGroupUpdateMessageBuilder(gv1);
+ g = gv1;
+ }
}
+ account.getGroupStore().updateGroup(g);
+
+ final Pair> result = sendMessage(messageBuilder,
+ g.getMembersIncludingPendingWithout(account.getSelfAddress()));
+ 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(
+ GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
+ ) throws IOException {
+ group.setGroup(newDecryptedGroup);
+ final SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(group,
+ groupChange.toByteArray());
+ account.getGroupStore().updateGroup(group);
+ return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress()));
+ }
+
+ private void updateGroupV1(
+ final GroupInfoV1 g,
+ final String name,
+ final Collection members,
+ final String avatarFile
+ ) throws IOException {
if (name != null) {
g.name = name;
}
@@ -609,7 +923,9 @@ public class Manager implements Closeable {
for (ContactTokenDetails contact : contacts) {
newE164Members.remove(contact.getNumber());
}
- throw new IOException("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal");
+ throw new IOException("Failed to add members "
+ + Util.join(", ", newE164Members)
+ + " to group: Not registered on Signal");
}
g.addMembers(members);
@@ -617,41 +933,38 @@ 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);
}
-
- account.getGroupStore().updateGroup(g);
-
- SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
-
- sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
- return g.groupId;
}
- void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
- if (groupId == null) {
- return;
+ Pair> sendUpdateGroupMessage(
+ GroupIdV1 groupId, SignalServiceAddress recipient
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
+ GroupInfoV1 g;
+ GroupInfo group = getGroupForSending(groupId);
+ if (!(group instanceof GroupInfoV1)) {
+ throw new RuntimeException("Received an invalid group request for a v2 group!");
}
- GroupInfo g = getGroupForSending(groupId);
+ g = (GroupInfoV1) group;
if (!g.isMember(recipient)) {
- return;
+ throw new NotAGroupMemberException(groupId, g.name);
}
SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
// Send group message only to the recipient who requested it
- sendMessageLegacy(messageBuilder, Collections.singleton(recipient));
+ return sendMessage(messageBuilder, Collections.singleton(recipient));
}
- private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) throws AttachmentInvalidException {
+ 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));
@@ -662,41 +975,53 @@ public class Manager implements Closeable {
return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build())
- .withExpiration(g.messageExpirationTime);
+ .withExpiration(g.getMessageExpirationTime());
}
- void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions {
- if (groupId == null) {
- return;
- }
+ private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
+ SignalServiceGroupV2.Builder group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
+ .withRevision(g.getGroup().getRevision())
+ .withSignedGroupChange(signedGroupChange);
+ return SignalServiceDataMessage.newBuilder()
+ .asGroupMessage(group.build())
+ .withExpiration(g.getMessageExpirationTime());
+ }
+ Pair> sendGroupInfoRequest(
+ 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
- sendMessageLegacy(messageBuilder, Collections.singleton(recipient));
+ return sendMessage(messageBuilder, Collections.singleton(recipient));
}
- void sendReceipt(SignalServiceAddress remoteAddress, long messageId) throws IOException, UntrustedIdentityException {
+ void sendReceipt(
+ SignalServiceAddress remoteAddress, long messageId
+ ) throws IOException, UntrustedIdentityException {
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
Collections.singletonList(messageId),
System.currentTimeMillis());
- getMessageSender().sendReceipt(remoteAddress, getAccessFor(remoteAddress), receiptMessage);
+ createMessageSender().sendReceipt(remoteAddress,
+ unidentifiedAccessHelper.getAccessFor(remoteAddress),
+ receiptMessage);
}
- public long sendMessage(String messageText, List attachments,
- List recipients)
- throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException {
- final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
+ public Pair> sendMessage(
+ String messageText, List attachments, List recipients
+ ) throws IOException, AttachmentInvalidException, InvalidNumberException {
+ final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
+ .withBody(messageText);
if (attachments != null) {
List attachmentStreams = Utils.getSignalServiceAttachments(attachments);
// Upload attachments here, so we only upload once even for multiple recipients
- SignalServiceMessageSender messageSender = getMessageSender();
+ SignalServiceMessageSender messageSender = createMessageSender();
List attachmentPointers = new ArrayList<>(attachmentStreams.size());
for (SignalServiceAttachment attachment : attachmentStreams) {
if (attachment.isStream()) {
@@ -708,25 +1033,27 @@ public class Manager implements Closeable {
messageBuilder.withAttachments(attachmentPointers);
}
- return sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
+ return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
}
- public void sendMessageReaction(String emoji, boolean remove, String targetAuthor,
- long targetSentTimestamp, List recipients)
- throws IOException, EncapsulatedExceptions, InvalidNumberException {
- SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp);
+ public Pair> sendMessageReaction(
+ String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients
+ ) throws IOException, InvalidNumberException {
+ SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji,
+ remove,
+ canonicalizeAndResolveSignalServiceAddress(targetAuthor),
+ targetSentTimestamp);
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withReaction(reaction);
- sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients));
+ return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
}
- public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException {
- SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
- .asEndSessionMessage();
+ public Pair> sendEndSessionMessage(List recipients) throws IOException, InvalidNumberException {
+ SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
final Collection signalServiceAddresses = getSignalServiceAddresses(recipients);
try {
- sendMessageLegacy(messageBuilder, signalServiceAddresses);
+ return sendMessage(messageBuilder, signalServiceAddresses);
} catch (Exception e) {
for (SignalServiceAddress address : signalServiceAddresses) {
handleEndSession(address);
@@ -770,31 +1097,24 @@ 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);
}
- group.blocked = blocked;
+ group.setBlocked(blocked);
account.getGroupStore().updateGroup(group);
account.save();
}
- public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
- if (groupId.length == 0) {
- groupId = null;
- }
- if (name.isEmpty()) {
- name = null;
- }
- if (members.isEmpty()) {
- members = null;
- }
- if (avatar.isEmpty()) {
- avatar = null;
- }
- return sendUpdateGroupMessage(groupId, name, members == null ? null : getSignalServiceAddresses(members), avatar);
+ public Pair> updateGroup(
+ GroupId groupId, String name, List members, String avatar
+ ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
+ return sendUpdateGroupMessage(groupId,
+ name,
+ members == null ? null : getSignalServiceAddresses(members),
+ avatar);
}
/**
@@ -817,7 +1137,9 @@ public class Manager implements Closeable {
/**
* Change the expiration timer for a contact
*/
- public void setExpirationTimer(String number, int messageExpirationTimer) throws IOException, InvalidNumberException {
+ public void setExpirationTimer(
+ String number, int messageExpirationTimer
+ ) throws IOException, InvalidNumberException {
SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number);
setExpirationTimer(address, messageExpirationTimer);
}
@@ -825,10 +1147,15 @@ public class Manager implements Closeable {
/**
* Change the expiration timer for a group
*/
- public void setExpirationTimer(byte[] groupId, int messageExpirationTimer) {
+ public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) {
GroupInfo g = account.getGroupStore().getGroup(groupId);
- g.messageExpirationTime = messageExpirationTimer;
- account.getGroupStore().updateGroup(g);
+ if (g instanceof GroupInfoV1) {
+ GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
+ groupInfoV1.messageExpirationTime = messageExpirationTimer;
+ account.getGroupStore().updateGroup(groupInfoV1);
+ } else {
+ throw new RuntimeException("TODO Not implemented!");
+ }
}
/**
@@ -837,27 +1164,36 @@ 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 = getMessageSender();
+ SignalServiceMessageSender messageSender = createMessageSender();
byte[] packKey = KeyUtils.createStickerUploadKey();
String packId = messageSender.uploadStickerManifest(manifest, packKey);
+ Sticker sticker = new Sticker(Hex.fromStringCondensed(packId), packKey);
+ account.getStickerStore().updateSticker(sticker);
+ account.save();
+
try {
- return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder.encode(packId, "utf-8") + "&pack_key=" + URLEncoder.encode(Hex.toStringCondensed(packKey), "utf-8"))
- .toString();
+ return new URI("https",
+ "signal.art",
+ "/addstickers/",
+ "pack_id=" + URLEncoder.encode(packId, StandardCharsets.UTF_8) + "&pack_key=" + URLEncoder.encode(
+ Hex.toStringCondensed(packKey),
+ StandardCharsets.UTF_8)).toString();
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
- private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(final String path) throws IOException, StickerPackInvalidException {
+ private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(
+ 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")) {
@@ -890,7 +1226,10 @@ public class Manager implements Closeable {
}
String contentType = Utils.getFileMimeType(new File(sticker.file), null);
- StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""), contentType);
+ StickerInfo stickerInfo = new StickerInfo(data.first(),
+ data.second(),
+ Optional.fromNullable(sticker.emoji).or(""),
+ contentType);
stickers.add(stickerInfo);
}
@@ -908,14 +1247,13 @@ public class Manager implements Closeable {
}
String contentType = Utils.getFileMimeType(new File(pack.cover.file), null);
- cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""), contentType);
+ cover = new StickerInfo(data.first(),
+ data.second(),
+ Optional.fromNullable(pack.cover.emoji).or(""),
+ contentType);
}
- return new SignalServiceStickerManifestUpload(
- pack.title,
- pack.author,
- cover,
- stickers);
+ return new SignalServiceStickerManifestUpload(pack.title, pack.author, cover, stickers);
}
private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException {
@@ -928,7 +1266,9 @@ public class Manager implements Closeable {
return new ObjectMapper().readValue(inputStream, JsonStickerPack.class);
}
- private static Pair getInputStreamAndLength(final String rootPath, final ZipFile zip, final String subfile) throws IOException {
+ private static Pair getInputStreamAndLength(
+ final String rootPath, final ZipFile zip, final String subfile
+ ) throws IOException {
if (zip != null) {
final ZipEntry entry = zip.getEntry(subfile);
return new Pair<>(zip.getInputStream(entry), entry.getSize());
@@ -939,7 +1279,9 @@ public class Manager implements Closeable {
}
void requestSyncGroups() throws IOException {
- SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
+ SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder()
+ .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS)
+ .build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
sendSyncMessage(message);
@@ -949,7 +1291,9 @@ public class Manager implements Closeable {
}
void requestSyncContacts() throws IOException {
- SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
+ SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder()
+ .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS)
+ .build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
sendSyncMessage(message);
@@ -959,7 +1303,9 @@ public class Manager implements Closeable {
}
void requestSyncBlocked() throws IOException {
- SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED).build();
+ SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder()
+ .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED)
+ .build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
sendSyncMessage(message);
@@ -969,7 +1315,9 @@ public class Manager implements Closeable {
}
void requestSyncConfiguration() throws IOException {
- SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION).build();
+ SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder()
+ .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION)
+ .build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
sendSyncMessage(message);
@@ -985,175 +1333,98 @@ 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
return certificate;
}
- private byte[] getSelfUnidentifiedAccessKey() {
- return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
- }
-
- private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
- ContactInfo contact = account.getContactStore().getContact(recipient);
- if (contact == null || contact.profileKey == null) {
- return null;
- }
- ProfileKey theirProfileKey;
+ private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException {
+ SignalServiceMessageSender messageSender = createMessageSender();
try {
- theirProfileKey = new ProfileKey(Base64.decode(contact.profileKey));
- } catch (InvalidInputException | IOException e) {
- throw new AssertionError(e);
- }
- SignalProfile targetProfile;
- try {
- targetProfile = getRecipientProfile(recipient, Optional.absent(), theirProfileKey);
- } catch (IOException e) {
- System.err.println("Failed to get recipient profile: " + e);
- return null;
- }
-
- if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) {
- return null;
- }
-
- if (targetProfile.isUnrestrictedUnidentifiedAccess()) {
- return KeyUtils.createUnrestrictedUnidentifiedAccess();
- }
-
- return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
- }
-
- private Optional getAccessForSync() {
- byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
- byte[] selfUnidentifiedAccessCertificate = getSenderCertificate();
-
- if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
- return Optional.absent();
- }
-
- try {
- return Optional.of(new UnidentifiedAccessPair(
- new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
- new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
- ));
- } catch (InvalidCertificateException e) {
- return Optional.absent();
- }
- }
-
- private List> getAccessFor(Collection recipients) {
- List> result = new ArrayList<>(recipients.size());
- for (SignalServiceAddress recipient : recipients) {
- result.add(getAccessFor(recipient));
- }
- return result;
- }
-
- private Optional getAccessFor(SignalServiceAddress recipient) {
- byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
- byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
- byte[] selfUnidentifiedAccessCertificate = getSenderCertificate();
-
- if (recipientUnidentifiedAccessKey == null || selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
- return Optional.absent();
- }
-
- try {
- return Optional.of(new UnidentifiedAccessPair(
- new UnidentifiedAccess(recipientUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
- new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
- ));
- } catch (InvalidCertificateException e) {
- return Optional.absent();
- }
- }
-
- private Optional getUnidentifiedAccess(SignalServiceAddress recipient) {
- Optional unidentifiedAccess = getAccessFor(recipient);
-
- if (unidentifiedAccess.isPresent()) {
- return unidentifiedAccess.get().getTargetUnidentifiedAccess();
- }
-
- return Optional.absent();
- }
-
- private void sendSyncMessage(SignalServiceSyncMessage message)
- throws IOException, UntrustedIdentityException {
- SignalServiceMessageSender messageSender = getMessageSender();
- try {
- messageSender.sendMessage(message, getAccessForSync());
+ messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
+ e.getIdentityKey(),
+ TrustLevel.UNTRUSTED);
throw e;
}
}
- /**
- * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
- */
- private long sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients)
- throws EncapsulatedExceptions, IOException {
- final long timestamp = System.currentTimeMillis();
- messageBuilder.withTimestamp(timestamp);
- List results = sendMessage(messageBuilder, recipients);
-
- List untrustedIdentities = new LinkedList<>();
- List unregisteredUsers = new LinkedList<>();
- List networkExceptions = new LinkedList<>();
-
- for (SendMessageResult result : results) {
- if (result.isUnregisteredFailure()) {
- unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getLegacyIdentifier(), null));
- } else if (result.isNetworkFailure()) {
- networkExceptions.add(new NetworkFailureException(result.getAddress().getLegacyIdentifier(), null));
- } else if (result.getIdentityFailure() != null) {
- untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getLegacyIdentifier(), result.getIdentityFailure().getIdentityKey()));
- }
- }
- if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
- throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
- }
- return timestamp;
- }
-
private Collection getSignalServiceAddresses(Collection numbers) throws InvalidNumberException {
final Set signalServiceAddresses = new HashSet<>(numbers.size());
+ final Set missingUuids = new HashSet<>();
for (String number : numbers) {
- signalServiceAddresses.add(canonicalizeAndResolveSignalServiceAddress(number));
+ final SignalServiceAddress resolvedAddress = canonicalizeAndResolveSignalServiceAddress(number);
+ if (resolvedAddress.getUuid().isPresent()) {
+ signalServiceAddresses.add(resolvedAddress);
+ } else {
+ missingUuids.add(resolvedAddress);
+ }
}
+
+ Map registeredUsers;
+ try {
+ registeredUsers = accountManager.getRegisteredUsers(getIasKeyStore(),
+ missingUuids.stream().map(a -> a.getNumber().get()).collect(Collectors.toSet()),
+ CDS_MRENCLAVE);
+ } catch (IOException | Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException e) {
+ logger.warn("Failed to resolve uuids from server, ignoring: {}", e.getMessage());
+ registeredUsers = new HashMap<>();
+ }
+
+ for (SignalServiceAddress address : missingUuids) {
+ final String number = address.getNumber().get();
+ if (registeredUsers.containsKey(number)) {
+ final SignalServiceAddress newAddress = resolveSignalServiceAddress(new SignalServiceAddress(
+ registeredUsers.get(number),
+ number));
+ signalServiceAddresses.add(newAddress);
+ } else {
+ signalServiceAddresses.add(address);
+ }
+ }
+
return signalServiceAddresses;
}
- private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients)
- throws IOException {
- if (messagePipe == null) {
- messagePipe = getMessageReceiver().createMessagePipe();
- }
- if (unidentifiedMessagePipe == null) {
- unidentifiedMessagePipe = getMessageReceiver().createUnidentifiedMessagePipe();
- }
+ private Pair> sendMessage(
+ SignalServiceDataMessage.Builder messageBuilder, Collection recipients
+ ) throws IOException {
+ recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet());
+ final long timestamp = System.currentTimeMillis();
+ messageBuilder.withTimestamp(timestamp);
+ getOrCreateMessagePipe();
+ getOrCreateUnidentifiedMessagePipe();
SignalServiceDataMessage message = null;
try {
message = messageBuilder.build();
if (message.getGroupContext().isPresent()) {
try {
- SignalServiceMessageSender messageSender = getMessageSender();
+ SignalServiceMessageSender messageSender = createMessageSender();
final boolean isRecipientUpdate = false;
- List result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message);
+ List result = messageSender.sendMessage(new ArrayList<>(recipients),
+ unidentifiedAccessHelper.getAccessFor(recipients),
+ isRecipientUpdate,
+ message);
for (SendMessageResult r : result) {
if (r.getIdentityFailure() != null) {
- account.getSignalProtocolStore().saveIdentity(r.getAddress(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore()
+ .saveIdentity(r.getAddress(),
+ r.getIdentityFailure().getIdentityKey(),
+ TrustLevel.UNTRUSTED);
}
}
- return result;
+ return new Pair<>(timestamp, result);
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
- return Collections.emptyList();
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
+ e.getIdentityKey(),
+ TrustLevel.UNTRUSTED);
+ return new Pair<>(timestamp, Collections.emptyList());
}
} else {
// Send to all individually, so sync messages are sent correctly
@@ -1174,7 +1445,7 @@ public class Manager implements Closeable {
results.add(sendMessage(address, message));
}
}
- return results;
+ return new Pair<>(timestamp, results);
}
} finally {
if (message != null && message.isEndSession()) {
@@ -1187,11 +1458,11 @@ public class Manager implements Closeable {
}
private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException {
- SignalServiceMessageSender messageSender = getMessageSender();
+ SignalServiceMessageSender messageSender = createMessageSender();
SignalServiceAddress recipient = account.getSelfAddress();
- final Optional unidentifiedAccess = getAccessFor(recipient);
+ final Optional unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipient);
SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient),
message.getTimestamp(),
message,
@@ -1201,33 +1472,51 @@ public class Manager implements Closeable {
SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
try {
+ long startTime = System.currentTimeMillis();
messageSender.sendMessage(syncMessage, unidentifiedAccess);
- return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), false);
+ return SendMessageResult.success(recipient,
+ unidentifiedAccess.isPresent(),
+ false,
+ System.currentTimeMillis() - startTime);
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
+ e.getIdentityKey(),
+ TrustLevel.UNTRUSTED);
return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
}
}
- private SendMessageResult sendMessage(SignalServiceAddress address, SignalServiceDataMessage message) throws IOException {
- SignalServiceMessageSender messageSender = getMessageSender();
+ private SendMessageResult sendMessage(
+ SignalServiceAddress address, SignalServiceDataMessage message
+ ) throws IOException {
+ SignalServiceMessageSender messageSender = createMessageSender();
try {
- return messageSender.sendMessage(address, getAccessFor(address), message);
+ return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message);
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
+ e.getIdentityKey(),
+ TrustLevel.UNTRUSTED);
return SendMessageResult.identityFailure(address, e.getIdentityKey());
}
}
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());
+ SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(),
+ account.getSignalProtocolStore(),
+ Utils.getCertificateValidator());
try {
return cipher.decrypt(envelope);
} catch (ProtocolUntrustedIdentityException e) {
if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
- org.whispersystems.libsignal.UntrustedIdentityException identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException.getName()), identityException.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
+ org.whispersystems.libsignal.UntrustedIdentityException identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e
+ .getCause();
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(identityException.getName()),
+ identityException.getUntrustedIdentity(),
+ TrustLevel.UNTRUSTED);
throw identityException;
}
throw new AssertionError(e);
@@ -1238,75 +1527,128 @@ public class Manager implements Closeable {
account.getSignalProtocolStore().deleteAllSessions(source);
}
- private List handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) {
+ private static int currentTimeDays() {
+ return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
+ }
+
+ private GroupsV2AuthorizationString getGroupAuthForToday(
+ final GroupSecretParams groupSecretParams
+ ) throws IOException {
+ final int today = currentTimeDays();
+ // Returns credentials for the next 7 days
+ final HashMap credentials = groupsV2Api.getCredentials(today);
+ // TODO cache credentials until they expire
+ AuthCredentialResponse authCredentialResponse = credentials.get(today);
+ try {
+ return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
+ today,
+ groupSecretParams,
+ authCredentialResponse);
+ } catch (VerificationFailedException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private List handleSignalServiceDataMessage(
+ SignalServiceDataMessage message,
+ boolean isSync,
+ SignalServiceAddress source,
+ SignalServiceAddress destination,
+ boolean ignoreAttachments
+ ) {
List actions = new ArrayList<>();
- if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
- SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
- GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
- switch (groupInfo.getType()) {
- case UPDATE:
- if (group == null) {
- group = new GroupInfo(groupInfo.getGroupId());
- }
-
- if (groupInfo.getAvatar().isPresent()) {
- SignalServiceAttachment avatar = groupInfo.getAvatar().get();
- if (avatar.isPointer()) {
- try {
- retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
- } catch (IOException | InvalidMessageException | MissingConfigurationException e) {
- System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getRemoteId() + "): " + e.getMessage());
+ if (message.getGroupContext().isPresent()) {
+ if (message.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
+ GroupIdV1 groupId = GroupId.v1(groupInfo.getGroupId());
+ GroupInfo group = account.getGroupStore().getGroup(groupId);
+ if (group == null || group instanceof GroupInfoV1) {
+ GroupInfoV1 groupV1 = (GroupInfoV1) group;
+ switch (groupInfo.getType()) {
+ case UPDATE: {
+ if (groupV1 == null) {
+ groupV1 = new GroupInfoV1(groupId);
}
+
+ if (groupInfo.getAvatar().isPresent()) {
+ SignalServiceAttachment avatar = groupInfo.getAvatar().get();
+ if (avatar.isPointer()) {
+ try {
+ retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.getGroupId());
+ } catch (IOException | InvalidMessageException | MissingConfigurationException e) {
+ logger.warn("Failed to retrieve avatar for group {}, ignoring: {}",
+ groupId.toBase64(),
+ e.getMessage());
+ }
+ }
+ }
+
+ if (groupInfo.getName().isPresent()) {
+ groupV1.name = groupInfo.getName().get();
+ }
+
+ if (groupInfo.getMembers().isPresent()) {
+ groupV1.addMembers(groupInfo.getMembers()
+ .get()
+ .stream()
+ .map(this::resolveSignalServiceAddress)
+ .collect(Collectors.toSet()));
+ }
+
+ account.getGroupStore().updateGroup(groupV1);
+ break;
}
+ case DELIVER:
+ if (groupV1 == null && !isSync) {
+ actions.add(new SendGroupInfoRequestAction(source, groupId));
+ }
+ break;
+ case QUIT: {
+ if (groupV1 != null) {
+ groupV1.removeMember(source);
+ account.getGroupStore().updateGroup(groupV1);
+ }
+ break;
+ }
+ case REQUEST_INFO:
+ if (groupV1 != null && !isSync) {
+ actions.add(new SendGroupUpdateAction(source, groupV1.getGroupId()));
+ }
+ break;
}
+ } else {
+ // Received a group v1 message for a v2 group
+ }
+ }
+ if (message.getGroupContext().get().getGroupV2().isPresent()) {
+ final SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get();
+ final GroupMasterKey groupMasterKey = groupContext.getMasterKey();
- if (groupInfo.getName().isPresent()) {
- group.name = groupInfo.getName().get();
- }
-
- if (groupInfo.getMembers().isPresent()) {
- group.addMembers(groupInfo.getMembers().get()
- .stream()
- .map(this::resolveSignalServiceAddress)
- .collect(Collectors.toSet()));
- }
-
- account.getGroupStore().updateGroup(group);
- break;
- case DELIVER:
- if (group == null && !isSync) {
- actions.add(new SendGroupInfoRequestAction(source, groupInfo.getGroupId()));
- }
- break;
- case QUIT:
- if (group != null) {
- group.removeMember(source);
- account.getGroupStore().updateGroup(group);
- }
- break;
- case REQUEST_INFO:
- if (group != null && !isSync) {
- actions.add(new SendGroupUpdateAction(source, group.groupId));
- }
- break;
+ 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() && message.getGroupContext().get().getGroupV1().isPresent()) {
- SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
- GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
- if (group == null) {
- group = new GroupInfo(groupInfo.getGroupId());
+ if (message.getGroupContext().isPresent()) {
+ if (message.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
+ GroupInfoV1 group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId()));
+ if (group != null) {
+ if (group.messageExpirationTime != message.getExpiresInSeconds()) {
+ group.messageExpirationTime = message.getExpiresInSeconds();
+ account.getGroupStore().updateGroup(group);
+ }
+ }
+ } else if (message.getGroupContext().get().getGroupV2().isPresent()) {
+ // disappearing message timer already stored in the DecryptedGroup
}
- if (group.messageExpirationTime != message.getExpiresInSeconds()) {
- group.messageExpirationTime = message.getExpiresInSeconds();
- account.getGroupStore().updateGroup(group);
- }
- } else {
+ } else if (conversationPartnerAddress != null) {
ContactInfo contact = account.getContactStore().getContact(conversationPartnerAddress);
if (contact == null) {
contact = new ContactInfo(conversationPartnerAddress);
@@ -1323,30 +1665,24 @@ 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());
}
}
}
}
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
- if (source.matches(account.getSelfAddress())) {
- try {
- this.account.setProfileKey(new ProfileKey(message.getProfileKey().get()));
- } catch (InvalidInputException ignored) {
- }
- ContactInfo contact = account.getContactStore().getContact(source);
- if (contact != null) {
- contact.profileKey = Base64.encodeBytes(message.getProfileKey().get());
- account.getContactStore().updateContact(contact);
- }
- } else {
- ContactInfo contact = account.getContactStore().getContact(source);
- if (contact == null) {
- contact = new ContactInfo(source);
- }
- contact.profileKey = Base64.encodeBytes(message.getProfileKey().get());
- account.getContactStore().updateContact(contact);
+ final ProfileKey profileKey;
+ try {
+ profileKey = new ProfileKey(message.getProfileKey().get());
+ } catch (InvalidInputException e) {
+ throw new AssertionError(e);
}
+ if (source.matches(account.getSelfAddress())) {
+ this.account.setProfileKey(profileKey);
+ }
+ this.account.getProfileStore().storeProfileKey(source, profileKey);
}
if (message.getPreviews().isPresent()) {
final List previews = message.getPreviews().get();
@@ -1356,16 +1692,105 @@ 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());
+ }
+ }
+ }
+ }
+ if (message.getSticker().isPresent()) {
+ final SignalServiceDataMessage.Sticker messageSticker = message.getSticker().get();
+ Sticker sticker = account.getStickerStore().getSticker(messageSticker.getPackId());
+ if (sticker == null) {
+ sticker = new Sticker(messageSticker.getPackId(), messageSticker.getPackKey());
+ account.getStickerStore().updateSticker(sticker);
+ }
+ }
return actions;
}
- private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
- final File cachePath = new File(getMessageCachePath());
+ 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 = account.getGroupStore().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) {
+ for (DecryptedMember member : group.getMembersList()) {
+ final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(
+ member.getUuid().toByteArray()), null));
+ try {
+ account.getProfileStore()
+ .storeProfileKey(address, new ProfileKey(member.getProfileKey().toByteArray()));
+ } catch (InvalidInputException ignored) {
+ }
+ }
+ }
+
+ private void retryFailedReceivedMessages(
+ ReceiveMessageHandler handler, boolean ignoreAttachments
+ ) {
+ final File cachePath = getMessageCachePath();
if (!cachePath.exists()) {
return;
}
@@ -1386,7 +1811,9 @@ public class Manager implements Closeable {
}
}
- private void retryFailedReceivedMessage(final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry) {
+ private void retryFailedReceivedMessage(
+ final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry
+ ) {
SignalServiceEnvelope envelope;
try {
envelope = Utils.loadEnvelope(fileEntry);
@@ -1401,7 +1828,15 @@ public class Manager implements Closeable {
if (!envelope.isReceipt()) {
try {
content = decryptMessage(envelope);
- } catch (Exception e) {
+ } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
+ return;
+ } catch (Exception er) {
+ // All other errors are not recoverable, so delete the cached message
+ try {
+ Files.delete(fileEntry.toPath());
+ } catch (IOException e) {
+ logger.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry, e.getMessage());
+ }
return;
}
List actions = handleMessage(envelope, content, ignoreAttachments);
@@ -1418,7 +1853,7 @@ public class Manager implements Closeable {
try {
Files.delete(fileEntry.toPath());
} catch (IOException e) {
- System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
+ logger.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry, e.getMessage());
}
}
@@ -1524,15 +1959,18 @@ public class Manager implements Closeable {
}
}
}
- public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException {
+ public void receiveMessages(
+ long timeout,
+ TimeUnit unit,
+ boolean returnOnTimeout,
+ boolean ignoreAttachments,
+ ReceiveMessageHandler handler
+ ) throws IOException {
retryFailedReceivedMessages(handler, ignoreAttachments);
- final SignalServiceMessageReceiver messageReceiver = getMessageReceiver();
Set queuedActions = null;
- if (messagePipe == null) {
- messagePipe = messageReceiver.createMessagePipe();
- }
+ getOrCreateMessagePipe();
boolean hasCaughtUpWithOldMessages = false;
@@ -1549,7 +1987,7 @@ public class Manager implements Closeable {
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());
+ logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage());
}
});
if (result.isPresent()) {
@@ -1566,6 +2004,7 @@ public class Manager implements Closeable {
e.printStackTrace();
}
}
+ account.save();
queuedActions.clear();
queuedActions = null;
}
@@ -1574,13 +2013,13 @@ public class Manager implements Closeable {
continue;
}
} catch (TimeoutException e) {
- if (returnOnTimeout)
- return;
+ if (returnOnTimeout) return;
continue;
} catch (InvalidVersionException e) {
- System.err.println("Ignoring error: " + e.getMessage());
+ logger.warn("Error while receiving messages, ignoring: {}", e.getMessage());
continue;
}
+
if (envelope.hasSource()) {
// Store uuid if we don't have it already
SignalServiceAddress source = envelope.getSourceAddress();
@@ -1619,15 +2058,17 @@ public class Manager implements Closeable {
cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp());
Files.delete(cacheFile.toPath());
// Try to delete directory if empty
- new File(getMessageCachePath()).delete();
+ getMessageCachePath().delete();
} catch (IOException e) {
- System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
+ logger.warn("Failed to delete cached message file “{}”, ignoring: {}", cacheFile, e.getMessage());
}
}
}
}
- private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) {
+ private boolean isMessageBlocked(
+ SignalServiceEnvelope envelope, SignalServiceContent content
+ ) {
SignalServiceAddress source;
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
source = envelope.getSourceAddress();
@@ -1643,10 +2084,16 @@ public class Manager implements Closeable {
if (content != null && content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
- if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
- SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
- GroupInfo group = getGroup(groupInfo.getGroupId());
- if (groupInfo.getType() == SignalServiceGroup.Type.DELIVER && group != null && group.blocked) {
+ if (message.getGroupContext().isPresent()) {
+ if (message.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
+ if (groupInfo.getType() != SignalServiceGroup.Type.DELIVER) {
+ return false;
+ }
+ }
+ GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get());
+ GroupInfo group = account.getGroupStore().getGroup(groupId);
+ if (group != null && group.isBlocked()) {
return true;
}
}
@@ -1654,10 +2101,12 @@ public class Manager implements Closeable {
return false;
}
- private List handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
+ private List handleMessage(
+ SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments
+ ) {
List actions = new ArrayList<>();
if (content != null) {
- SignalServiceAddress sender;
+ final SignalServiceAddress sender;
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
sender = envelope.getSourceAddress();
} else {
@@ -1673,14 +2122,23 @@ public class Manager implements Closeable {
actions.add(new SendReceiptAction(sender, message.getTimestamp()));
}
- actions.addAll(handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments));
+ actions.addAll(handleSignalServiceDataMessage(message,
+ false,
+ sender,
+ account.getSelfAddress(),
+ ignoreAttachments));
}
if (content.getSyncMessage().isPresent()) {
account.setMultiDevice(true);
SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
if (syncMessage.getSent().isPresent()) {
SentTranscriptMessage message = syncMessage.getSent().get();
- actions.addAll(handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments));
+ final SignalServiceAddress destination = message.getDestination().orNull();
+ actions.addAll(handleSignalServiceDataMessage(message.getMessage(),
+ true,
+ sender,
+ destination,
+ ignoreAttachments));
}
if (syncMessage.getRequest().isPresent()) {
RequestMessage rm = syncMessage.getRequest().get();
@@ -1693,54 +2151,61 @@ public class Manager implements Closeable {
if (rm.isBlockedListRequest()) {
actions.add(SendSyncBlockedListAction.create());
}
- // TODO Handle rm.isConfigurationRequest();
+ // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest();
}
if (syncMessage.getGroups().isPresent()) {
File tmpFile = null;
try {
tmpFile = IOUtils.createTempFile();
- try (InputStream attachmentAsStream = retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile)) {
+ try (InputStream attachmentAsStream = retrieveAttachmentAsStream(syncMessage.getGroups()
+ .get()
+ .asPointer(), tmpFile)) {
DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream);
DeviceGroup g;
while ((g = s.read()) != null) {
- GroupInfo syncGroup = account.getGroupStore().getGroup(g.getId());
- if (syncGroup == null) {
- syncGroup = new GroupInfo(g.getId());
- }
- if (g.getName().isPresent()) {
- syncGroup.name = g.getName().get();
- }
- syncGroup.addMembers(g.getMembers()
- .stream()
- .map(this::resolveSignalServiceAddress)
- .collect(Collectors.toSet()));
- if (!g.isActive()) {
- syncGroup.removeMember(account.getSelfAddress());
- } else {
- // Add ourself to the member set as it's marked as active
- syncGroup.addMembers(Collections.singleton(account.getSelfAddress()));
- }
- syncGroup.blocked = g.isBlocked();
- if (g.getColor().isPresent()) {
- syncGroup.color = g.getColor().get();
- }
+ GroupInfoV1 syncGroup = account.getGroupStore()
+ .getOrCreateGroupV1(GroupId.v1(g.getId()));
+ if (syncGroup != null) {
+ if (g.getName().isPresent()) {
+ syncGroup.name = g.getName().get();
+ }
+ syncGroup.addMembers(g.getMembers()
+ .stream()
+ .map(this::resolveSignalServiceAddress)
+ .collect(Collectors.toSet()));
+ if (!g.isActive()) {
+ syncGroup.removeMember(account.getSelfAddress());
+ } else {
+ // Add ourself to the member set as it's marked as active
+ syncGroup.addMembers(Collections.singleton(account.getSelfAddress()));
+ }
+ syncGroup.blocked = g.isBlocked();
+ if (g.getColor().isPresent()) {
+ syncGroup.color = g.getColor().get();
+ }
- if (g.getAvatar().isPresent()) {
- retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
+ if (g.getAvatar().isPresent()) {
+ retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.getGroupId());
+ }
+ syncGroup.inboxPosition = g.getInboxPosition().orNull();
+ syncGroup.archived = g.isArchived();
+ account.getGroupStore().updateGroup(syncGroup);
}
- syncGroup.inboxPosition = g.getInboxPosition().orNull();
- syncGroup.archived = g.isArchived();
- account.getGroupStore().updateGroup(syncGroup);
}
}
} 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());
}
}
}
@@ -1750,11 +2215,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());
}
}
}
@@ -1763,7 +2232,8 @@ public class Manager implements Closeable {
try {
tmpFile = IOUtils.createTempFile();
final ContactsMessage contactsMessage = syncMessage.getContacts().get();
- try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)) {
+ try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream()
+ .asPointer(), tmpFile)) {
DeviceContactsInputStream s = new DeviceContactsInputStream(attachmentAsStream);
if (contactsMessage.isComplete()) {
account.getContactStore().clear();
@@ -1785,11 +2255,14 @@ public class Manager implements Closeable {
contact.color = c.getColor().get();
}
if (c.getProfileKey().isPresent()) {
- contact.profileKey = Base64.encodeBytes(c.getProfileKey().get().serialize());
+ account.getProfileStore().storeProfileKey(address, c.getProfileKey().get());
}
if (c.getVerified().isPresent()) {
final VerifiedMessage verifiedMessage = c.getVerified().get();
- account.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
+ account.getSignalProtocolStore()
+ .setIdentityTrustLevel(verifiedMessage.getDestination(),
+ verifiedMessage.getIdentityKey(),
+ TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
if (c.getExpirationTimer().isPresent()) {
contact.messageExpirationTime = c.getExpirationTimer().get();
@@ -1811,14 +2284,38 @@ 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());
}
}
}
}
if (syncMessage.getVerified().isPresent()) {
final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
- account.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
+ account.getSignalProtocolStore()
+ .setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()),
+ verifiedMessage.getIdentityKey(),
+ TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
+ }
+ if (syncMessage.getStickerPackOperations().isPresent()) {
+ final List stickerPackOperationMessages = syncMessage.getStickerPackOperations()
+ .get();
+ for (StickerPackOperationMessage m : stickerPackOperationMessages) {
+ if (!m.getPackId().isPresent()) {
+ continue;
+ }
+ Sticker sticker = account.getStickerStore().getSticker(m.getPackId().get());
+ if (sticker == null) {
+ if (!m.getPackKey().isPresent()) {
+ continue;
+ }
+ sticker = new Sticker(m.getPackId().get(), m.getPackKey().get());
+ }
+ sticker.setInstalled(!m.getType().isPresent()
+ || m.getType().get() == StickerPackOperationMessage.Type.INSTALL);
+ account.getStickerStore().updateSticker(sticker);
+ }
}
if (syncMessage.getConfiguration().isPresent()) {
// TODO
@@ -1832,7 +2329,9 @@ public class Manager implements Closeable {
return new File(pathConfig.getAvatarsPath(), "contact-" + number);
}
- private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException, MissingConfigurationException {
+ private File retrieveContactAvatarAttachment(
+ SignalServiceAttachment attachment, String number
+ ) throws IOException, InvalidMessageException, MissingConfigurationException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
@@ -1843,11 +2342,13 @@ public class Manager implements Closeable {
}
}
- 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) throws IOException, InvalidMessageException, MissingConfigurationException {
+ private File retrieveGroupAvatarAttachment(
+ SignalServiceAttachment attachment, GroupId groupId
+ ) throws IOException, InvalidMessageException, MissingConfigurationException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
@@ -1858,24 +2359,62 @@ public class Manager implements Closeable {
}
}
+ private File retrieveGroupAvatar(
+ GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey
+ ) throws IOException {
+ IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
+ SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver();
+ File outputFile = getGroupAvatarFile(groupId);
+ GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+
+ File tmpFile = IOUtils.createTempFile();
+ tmpFile.deleteOnExit();
+ try (InputStream input = receiver.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());
}
- private File retrieveProfileAvatar(SignalServiceAddress address, String avatarPath, ProfileKey profileKey) throws IOException {
+ private File retrieveProfileAvatar(
+ SignalServiceAddress address, String avatarPath, ProfileKey profileKey
+ ) throws IOException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
- SignalServiceMessageReceiver receiver = getMessageReceiver();
+ SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver();
File outputFile = getProfileAvatarFile(address);
File tmpFile = IOUtils.createTempFile();
- try (InputStream input = receiver.retrieveProfileAvatar(avatarPath, tmpFile, profileKey, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
+ try (InputStream input = receiver.retrieveProfileAvatar(avatarPath,
+ tmpFile,
+ profileKey,
+ ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
// Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
IOUtils.copyStreamToFile(input, outputFile, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
} finally {
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;
@@ -1890,7 +2429,9 @@ public class Manager implements Closeable {
return retrieveAttachment(pointer, getAttachmentFile(pointer.getRemoteId()), true);
}
- private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException, MissingConfigurationException {
+ private File retrieveAttachment(
+ SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview
+ ) throws IOException, InvalidMessageException, MissingConfigurationException {
if (storePreview && pointer.getPreview().isPresent()) {
File previewFile = new File(outputFile + ".preview");
try (OutputStream output = new FileOutputStream(previewFile)) {
@@ -1902,23 +2443,29 @@ public class Manager implements Closeable {
}
}
- final SignalServiceMessageReceiver messageReceiver = getMessageReceiver();
+ final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver();
File tmpFile = IOUtils.createTempFile();
- try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE)) {
+ try (InputStream input = messageReceiver.retrieveAttachment(pointer,
+ tmpFile,
+ ServiceConfig.MAX_ATTACHMENT_SIZE)) {
IOUtils.copyStreamToFile(input, outputFile);
} finally {
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;
}
- private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException, MissingConfigurationException {
- final SignalServiceMessageReceiver messageReceiver = getMessageReceiver();
+ private InputStream retrieveAttachmentAsStream(
+ SignalServiceAttachmentPointer pointer, File tmpFile
+ ) throws IOException, InvalidMessageException, MissingConfigurationException {
+ final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver();
return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
}
@@ -1929,10 +2476,19 @@ public class Manager implements Closeable {
try (OutputStream fos = new FileOutputStream(groupsFile)) {
DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
for (GroupInfo record : account.getGroupStore().getGroups()) {
- out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
- new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId),
- record.isMember(account.getSelfAddress()), Optional.of(record.messageExpirationTime),
- Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived));
+ if (record instanceof GroupInfoV1) {
+ GroupInfoV1 groupInfo = (GroupInfoV1) record;
+ out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
+ Optional.fromNullable(groupInfo.name),
+ new ArrayList<>(groupInfo.getMembers()),
+ createGroupAvatarAttachment(groupInfo.getGroupId()),
+ groupInfo.isMember(account.getSelfAddress()),
+ Optional.of(groupInfo.messageExpirationTime),
+ Optional.fromNullable(groupInfo.color),
+ groupInfo.blocked,
+ Optional.fromNullable(groupInfo.inboxPosition),
+ groupInfo.archived));
+ }
}
}
@@ -1951,7 +2507,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());
}
}
}
@@ -1964,30 +2520,40 @@ 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());
+ JsonIdentityKeyStore.Identity currentIdentity = account.getSignalProtocolStore()
+ .getIdentity(record.getAddress());
if (currentIdentity != null) {
- verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime());
+ verifiedMessage = new VerifiedMessage(record.getAddress(),
+ currentIdentity.getIdentityKey(),
+ currentIdentity.getTrustLevel().toVerifiedState(),
+ currentIdentity.getDateAdded().getTime());
}
- ProfileKey profileKey = null;
- try {
- profileKey = record.profileKey == null ? null : new ProfileKey(Base64.decode(record.profileKey));
- } catch (InvalidInputException ignored) {
- }
- out.write(new DeviceContact(record.getAddress(), Optional.fromNullable(record.name),
- createContactAvatarAttachment(record.number), Optional.fromNullable(record.color),
- Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), record.blocked,
+ ProfileKey profileKey = account.getProfileStore().getProfileKey(record.getAddress());
+ out.write(new DeviceContact(record.getAddress(),
+ Optional.fromNullable(record.name),
+ createContactAvatarAttachment(record.number),
+ Optional.fromNullable(record.color),
+ Optional.fromNullable(verifiedMessage),
+ Optional.fromNullable(profileKey),
+ record.blocked,
Optional.of(record.messageExpirationTime),
- Optional.fromNullable(record.inboxPosition), record.archived));
+ Optional.fromNullable(record.inboxPosition),
+ record.archived));
}
if (account.getProfileKey() != null) {
// Send our own profile key as well
out.write(new DeviceContact(account.getSelfAddress(),
- Optional.absent(), Optional.absent(),
- Optional.absent(), Optional.absent(),
+ Optional.absent(),
+ Optional.absent(),
+ Optional.absent(),
+ Optional.absent(),
Optional.of(account.getProfileKey()),
- false, Optional.absent(), Optional.absent(), false));
+ false,
+ Optional.absent(),
+ Optional.absent(),
+ false));
}
}
@@ -2006,7 +2572,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());
}
}
}
@@ -2020,15 +2586,20 @@ public class Manager implements Closeable {
}
List groupIds = new ArrayList<>();
for (GroupInfo record : account.getGroupStore().getGroups()) {
- if (record.blocked) {
- groupIds.add(record.groupId);
+ if (record.isBlocked()) {
+ groupIds.add(record.getGroupId().serialize());
}
}
sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
}
- private void sendVerifiedMessage(SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel) throws IOException, UntrustedIdentityException {
- VerifiedMessage verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState(), System.currentTimeMillis());
+ private void sendVerifiedMessage(
+ SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
+ ) throws IOException, UntrustedIdentityException {
+ VerifiedMessage verifiedMessage = new VerifiedMessage(destination,
+ identityKey,
+ trustLevel.toVerifiedState(),
+ System.currentTimeMillis());
sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
}
@@ -2040,7 +2611,7 @@ public class Manager implements Closeable {
return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number));
}
- public GroupInfo getGroup(byte[] groupId) {
+ public GroupInfo getGroup(GroupId groupId) {
return account.getGroupStore().getGroup(groupId);
}
@@ -2069,7 +2640,8 @@ public class Manager implements Closeable {
continue;
}
- account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+ account.getSignalProtocolStore()
+ .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
try {
sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
} catch (IOException | UntrustedIdentityException e) {
@@ -2098,7 +2670,8 @@ public class Manager implements Closeable {
continue;
}
- account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+ account.getSignalProtocolStore()
+ .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
try {
sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
} catch (IOException | UntrustedIdentityException e) {
@@ -2123,7 +2696,8 @@ public class Manager implements Closeable {
}
for (JsonIdentityKeyStore.Identity id : ids) {
if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
- account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
+ account.getSignalProtocolStore()
+ .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
try {
sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
} catch (IOException | UntrustedIdentityException e) {
@@ -2135,8 +2709,13 @@ public class Manager implements Closeable {
return true;
}
- public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
- return Utils.computeSafetyNumber(account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey);
+ public String computeSafetyNumber(
+ SignalServiceAddress theirAddress, IdentityKey theirIdentityKey
+ ) {
+ return Utils.computeSafetyNumber(account.getSelfAddress(),
+ getIdentityKeyPair().getPublicKey(),
+ theirAddress,
+ theirIdentityKey);
}
void saveAccount() {
@@ -2144,7 +2723,9 @@ public class Manager implements Closeable {
}
public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
- String canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : Util.canonicalizeNumber(identifier, account.getUsername());
+ String canonicalizedNumber = UuidUtil.isUuid(identifier)
+ ? identifier
+ : Util.canonicalizeNumber(identifier, account.getUsername());
return resolveSignalServiceAddress(canonicalizedNumber);
}
diff --git a/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java
index 8c0e9be0..2c9b3f33 100644
--- a/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java
+++ b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java
@@ -1,10 +1,8 @@
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) + ")");
+ 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/PathConfig.java b/src/main/java/org/asamk/signal/manager/PathConfig.java
index 2c2d938a..d96034df 100644
--- a/src/main/java/org/asamk/signal/manager/PathConfig.java
+++ b/src/main/java/org/asamk/signal/manager/PathConfig.java
@@ -1,34 +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 4f1aca18..f81cfa49 100644
--- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
+++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
@@ -31,6 +31,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,7 +46,7 @@ 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;
@@ -70,12 +71,19 @@ public class ProvisioningManager {
public String getDeviceLinkUri() throws TimeoutException, IOException {
String deviceUuid = accountManager.getNewDeviceUuid();
- return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid, identityKey.getPublicKey().getPublicKey()));
+ return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid,
+ identityKey.getPublicKey().getPublicKey()));
}
public String finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
String signalingKey = KeyUtils.createSignalingKey();
- SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(identityKey, signalingKey, false, true, registrationId, deviceName);
+ SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(
+ identityKey,
+ signalingKey,
+ false,
+ true,
+ registrationId,
+ deviceName);
String username = ret.getNumber();
// TODO do this check before actually registering
@@ -96,7 +104,15 @@ public class ProvisioningManager {
}
}
- try (SignalAccount account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(), username, ret.getUuid(), password, ret.getDeviceId(), ret.getIdentity(), registrationId, signalingKey, profileKey)) {
+ try (SignalAccount account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(),
+ username,
+ ret.getUuid(),
+ password,
+ ret.getDeviceId(),
+ ret.getIdentity(),
+ registrationId,
+ signalingKey,
+ profileKey)) {
account.save();
try (Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent)) {
diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java
index a8b0c5b6..353670ae 100644
--- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java
+++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java
@@ -1,7 +1,8 @@
package org.asamk.signal.manager;
+import org.signal.zkgroup.ServerPublicParams;
import org.whispersystems.libsignal.util.guava.Optional;
-import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
+import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
@@ -12,8 +13,11 @@ 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.HashMap;
import java.util.List;
import java.util.Map;
@@ -26,53 +30,86 @@ public class ServiceConfig {
final static int PREKEY_MINIMUM_COUNT = 20;
final static int PREKEY_BATCH_SIZE = 100;
final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
+ final static int MAX_ENVELOPE_SIZE = 0;
final static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
+ final static String CDS_MRENCLAVE = "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15";
+
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";
+ private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api.directory.signal.org";
private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org";
private final static String STORAGE_URL = "https://storage.signal.org";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
+ private final static TrustStore IAS_TRUST_STORE = new IasTrustStore();
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;
- static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false, false);
+ static final AccountAttributes.Capabilities capabilities;
- public static SignalServiceConfiguration createDefaultServiceConfiguration(String userAgent) {
- final Interceptor userAgentInterceptor = chain ->
- chain.proceed(chain.request().newBuilder()
- .header("User-Agent", userAgent)
- .build());
-
- final List interceptors = Collections.singletonList(userAgentInterceptor);
-
- final byte[] zkGroupServerPublicParams;
+ static {
try {
zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex);
} catch (IOException e) {
throw new AssertionError(e);
}
- return new SignalServiceConfiguration(
- new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
- makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}),
- new SignalContactDiscoveryUrl[0],
+ boolean zkGroupAvailable;
+ try {
+ new ServerPublicParams(zkGroupServerPublicParams);
+ zkGroupAvailable = true;
+ } catch (Throwable ignored) {
+ zkGroupAvailable = false;
+ }
+ capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable);
+ }
+
+ public static SignalServiceConfiguration createDefaultServiceConfiguration(String userAgent) {
+ final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
+ .newBuilder()
+ .header("User-Agent", userAgent)
+ .build());
+
+ final List interceptors = Collections.singletonList(userAgentInterceptor);
+
+ return new SignalServiceConfiguration(new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
+ makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
+ new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}),
+ new SignalContactDiscoveryUrl[]{new SignalContactDiscoveryUrl(SIGNAL_CONTACT_DISCOVERY_URL,
+ TRUST_STORE)},
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
interceptors,
dns,
- zkGroupServerPublicParams
- );
+ zkGroupServerPublicParams);
}
- private static Map makeSignalCdnUrlMapFor(SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls) {
- Map result = new HashMap<>();
- result.put(0, cdn0Urls);
- result.put(2, cdn2Urls);
- return Collections.unmodifiableMap(result);
+ public static AccountAttributes.Capabilities getCapabilities() {
+ return capabilities;
+ }
+
+ static KeyStore getIasKeyStore() {
+ try {
+ TrustStore contactTrustStore = IAS_TRUST_STORE;
+
+ KeyStore keyStore = KeyStore.getInstance("BKS");
+ keyStore.load(contactTrustStore.getKeyStoreInputStream(),
+ contactTrustStore.getKeyStorePassword().toCharArray());
+
+ return keyStore;
+ } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static Map makeSignalCdnUrlMapFor(
+ SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls
+ ) {
+ return Map.of(0, cdn0Urls, 2, cdn2Urls);
}
private ServiceConfig() {
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
index 05fcfb5e..0a815ea9 100644
--- a/src/main/java/org/asamk/signal/manager/Utils.java
+++ b/src/main/java/org/asamk/signal/manager/Utils.java
@@ -27,11 +27,11 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
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;
@@ -81,7 +81,21 @@ class Utils {
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);
+ 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 {
@@ -96,7 +110,8 @@ class Utils {
static CertificateValidator getCertificateValidator() {
try {
- ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
+ ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT),
+ 0);
return new CertificateValidator(unidentifiedSenderTrustRoot);
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
@@ -107,31 +122,20 @@ class Utils {
String[] params = query.split("&");
Map map = new HashMap<>();
for (String param : params) {
- String name = null;
final String[] paramParts = param.split("=");
- try {
- name = URLDecoder.decode(paramParts[0], "utf-8");
- } catch (UnsupportedEncodingException e) {
- // Impossible
- }
- String value = null;
- try {
- value = URLDecoder.decode(paramParts[1], "utf-8");
- } catch (UnsupportedEncodingException e) {
- // Impossible
- }
+ 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) {
- try {
- return "tsdevice:/?uuid=" + URLEncoder.encode(info.deviceIdentifier, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()), "utf-8");
- } catch (UnsupportedEncodingException e) {
- // Shouldn't happen
- return null;
- }
+ 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 {
@@ -195,7 +199,15 @@ class Utils {
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);
+ return new SignalServiceEnvelope(type,
+ addressOptional,
+ sourceDevice,
+ timestamp,
+ legacyMessage,
+ content,
+ serverReceivedTimestamp,
+ serverDeliveredTimestamp,
+ uuid);
}
}
@@ -245,13 +257,18 @@ class Utils {
return outputFile;
}
- static String computeSafetyNumber(SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
+ 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()) {
+ if (ServiceConfig.capabilities.isUuid() && ownAddress.getUuid().isPresent() && theirAddress.getUuid()
+ .isPresent()) {
// Version 2: UUID user
version = 2;
ownId = UuidUtil.toByteArray(ownAddress.getUuid().get());
@@ -266,7 +283,11 @@ class Utils {
theirId = theirAddress.getNumber().get().getBytes();
}
- Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, ownId, ownIdentityKey, theirId, theirIdentityKey);
+ Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version,
+ ownId,
+ ownIdentityKey,
+ theirId,
+ theirIdentityKey);
return fingerprint.getDisplayableFingerprint().getDisplayText();
}
diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupAuthorizationProvider.java b/src/main/java/org/asamk/signal/manager/helper/GroupAuthorizationProvider.java
new file mode 100644
index 00000000..d26ebb06
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/GroupAuthorizationProvider.java
@@ -0,0 +1,11 @@
+package org.asamk.signal.manager.helper;
+
+import org.signal.zkgroup.groups.GroupSecretParams;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
+
+import java.io.IOException;
+
+public interface GroupAuthorizationProvider {
+
+ GroupsV2AuthorizationString getAuthorizationForToday(GroupSecretParams groupSecretParams) throws IOException;
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
new file mode 100644
index 00000000..5a88bc66
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
@@ -0,0 +1,398 @@
+package org.asamk.signal.manager.helper;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.asamk.signal.manager.GroupIdV2;
+import org.asamk.signal.manager.GroupLinkPassword;
+import org.asamk.signal.manager.GroupUtils;
+import org.asamk.signal.storage.groups.GroupInfoV2;
+import org.asamk.signal.storage.profiles.SignalProfile;
+import org.asamk.signal.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;
+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;
+import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
+import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.util.UuidUtil;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+public class GroupHelper {
+
+ final static Logger logger = LoggerFactory.getLogger(GroupHelper.class);
+
+ private final ProfileKeyCredentialProvider profileKeyCredentialProvider;
+
+ private final ProfileProvider profileProvider;
+
+ private final SelfAddressProvider selfAddressProvider;
+
+ private final GroupsV2Operations groupsV2Operations;
+
+ private final GroupsV2Api groupsV2Api;
+
+ private final GroupAuthorizationProvider groupAuthorizationProvider;
+
+ public GroupHelper(
+ final ProfileKeyCredentialProvider profileKeyCredentialProvider,
+ final ProfileProvider profileProvider,
+ final SelfAddressProvider selfAddressProvider,
+ final GroupsV2Operations groupsV2Operations,
+ final GroupsV2Api groupsV2Api,
+ final GroupAuthorizationProvider groupAuthorizationProvider
+ ) {
+ this.profileKeyCredentialProvider = profileKeyCredentialProvider;
+ this.profileProvider = profileProvider;
+ this.selfAddressProvider = selfAddressProvider;
+ this.groupsV2Operations = groupsV2Operations;
+ this.groupsV2Api = groupsV2Api;
+ 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 {
+ final byte[] avatarBytes = readAvatarBytes(avatarFile);
+ final GroupsV2Operations.NewGroup newGroup = buildNewGroupV2(name, members, avatarBytes);
+ if (newGroup == null) {
+ return null;
+ }
+
+ final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
+
+ final GroupsV2AuthorizationString groupAuthForToday;
+ final DecryptedGroup decryptedGroup;
+ try {
+ groupAuthForToday = groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams);
+ groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
+ decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
+ } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
+ logger.warn("Failed to create V2 group: {}", e.getMessage());
+ return null;
+ }
+ if (decryptedGroup == null) {
+ logger.warn("Failed to create V2 group, unknown error!");
+ return null;
+ }
+
+ final GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams);
+ final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
+ GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
+ g.setGroup(decryptedGroup);
+
+ return g;
+ }
+
+ private byte[] readAvatarBytes(final String avatarFile) throws IOException {
+ final byte[] avatarBytes;
+ try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
+ avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
+ }
+ return avatarBytes;
+ }
+
+ private GroupsV2Operations.NewGroup buildNewGroupV2(
+ String name, Collection members, byte[] avatar
+ ) {
+ final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
+ selfAddressProvider.getSelfAddress());
+ if (profileKeyCredential == null) {
+ logger.warn("Cannot create a V2 group as self does not have a versioned profile");
+ return null;
+ }
+
+ if (!areMembersValid(members)) return null;
+
+ GroupCandidate self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(),
+ Optional.fromNullable(profileKeyCredential));
+ Set candidates = members.stream()
+ .map(member -> new GroupCandidate(member.getUuid().get(),
+ Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
+ .collect(Collectors.toSet());
+
+ final GroupSecretParams groupSecretParams = GroupSecretParams.generate();
+ return groupsV2Operations.createNewGroup(groupSecretParams,
+ name,
+ Optional.fromNullable(avatar),
+ self,
+ candidates,
+ Member.Role.DEFAULT,
+ 0);
+ }
+
+ private boolean areMembersValid(final Collection members) {
+ final Set noUuidCapability = members.stream()
+ .filter(address -> !address.getUuid().isPresent())
+ .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 Set noGv2Capability = members.stream()
+ .map(profileProvider::getProfile)
+ .filter(profile -> profile != null && !profile.getCapabilities().gv2)
+ .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;
+ }
+
+ return true;
+ }
+
+ public Pair updateGroupV2(
+ GroupInfoV2 groupInfoV2, String name, String avatarFile
+ ) throws IOException {
+ final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
+ GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+
+ GroupChange.Actions.Builder change = name != null
+ ? groupOperations.createModifyGroupTitle(name)
+ : GroupChange.Actions.newBuilder();
+
+ if (avatarFile != null) {
+ final byte[] avatarBytes = readAvatarBytes(avatarFile);
+ String avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes,
+ groupSecretParams,
+ groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
+ change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
+ }
+
+ final Optional uuid = this.selfAddressProvider.getSelfAddress().getUuid();
+ if (uuid.isPresent()) {
+ change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
+ }
+
+ return commitChange(groupInfoV2, change);
+ }
+
+ public Pair updateGroupV2(
+ GroupInfoV2 groupInfoV2, Set newMembers
+ ) throws IOException {
+ final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
+ GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+
+ if (!areMembersValid(newMembers)) {
+ throw new IOException("Failed to update group");
+ }
+
+ Set candidates = newMembers.stream()
+ .map(member -> new GroupCandidate(member.getUuid().get(),
+ Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
+ .collect(Collectors.toSet());
+
+ final GroupChange.Actions.Builder change = groupOperations.createModifyGroupMembershipChange(candidates,
+ selfAddressProvider.getSelfAddress().getUuid().get());
+
+ final Optional uuid = this.selfAddressProvider.getSelfAddress().getUuid();
+ if (uuid.isPresent()) {
+ change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
+ }
+
+ return commitChange(groupInfoV2, change);
+ }
+
+ public Pair leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
+ List pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
+ final UUID selfUuid = selfAddressProvider.getSelfAddress().getUuid().get();
+ Optional selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList,
+ selfUuid);
+
+ if (selfPendingMember.isPresent()) {
+ return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
+ } else {
+ return ejectMembers(groupInfoV2, Set.of(selfUuid));
+ }
+ }
+
+ 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);
+
+ 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");
+ }
+
+ final GroupChange.Actions.Builder change = groupOperations.createAcceptInviteChange(profileKeyCredential);
+
+ final Optional uuid = selfAddress.getUuid();
+ if (uuid.isPresent()) {
+ change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
+ }
+
+ return commitChange(groupInfoV2, change);
+ }
+
+ public Pair revokeInvites(
+ GroupInfoV2 groupInfoV2, Set pendingMembers
+ ) throws IOException {
+ final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
+ final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+ final Set uuidCipherTexts = pendingMembers.stream().map(member -> {
+ try {
+ return new UuidCiphertext(member.getUuidCipherText().toByteArray());
+ } catch (InvalidInputException e) {
+ throw new AssertionError(e);
+ }
+ }).collect(Collectors.toSet());
+ return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
+ }
+
+ public Pair ejectMembers(GroupInfoV2 groupInfoV2, Set uuids) throws IOException {
+ final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
+ final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+ return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
+ }
+
+ private Pair commitChange(
+ GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
+ ) throws IOException {
+ final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
+ final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+ final DecryptedGroup previousGroupState = groupInfoV2.getGroup();
+ final int nextRevision = previousGroupState.getRevision() + 1;
+ final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
+ final DecryptedGroupChange decryptedChange;
+ final DecryptedGroup decryptedGroupState;
+
+ try {
+ decryptedChange = groupOperations.decryptChange(changeActions,
+ selfAddressProvider.getSelfAddress().getUuid().get());
+ decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
+ } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
+ throw new IOException(e);
+ }
+
+ 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
+ ) {
+ try {
+ final DecryptedGroupChange decryptedGroupChange = getDecryptedGroupChange(signedGroupChange,
+ groupMasterKey);
+ if (decryptedGroupChange == null) {
+ return null;
+ }
+ return DecryptedGroupUtil.apply(group, decryptedGroupChange);
+ } catch (NotAbleToApplyGroupV2ChangeException e) {
+ return null;
+ }
+ }
+
+ private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
+ if (signedGroupChange != null) {
+ GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(
+ groupMasterKey));
+
+ try {
+ return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
+ } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
+ return null;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/MessagePipeProvider.java b/src/main/java/org/asamk/signal/manager/helper/MessagePipeProvider.java
new file mode 100644
index 00000000..7739928c
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/MessagePipeProvider.java
@@ -0,0 +1,8 @@
+package org.asamk.signal.manager.helper;
+
+import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
+
+public interface MessagePipeProvider {
+
+ SignalServiceMessagePipe getMessagePipe(boolean unidentified);
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/MessageReceiverProvider.java b/src/main/java/org/asamk/signal/manager/helper/MessageReceiverProvider.java
new file mode 100644
index 00000000..9a18a5e4
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/MessageReceiverProvider.java
@@ -0,0 +1,8 @@
+package org.asamk.signal.manager.helper;
+
+import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
+
+public interface MessageReceiverProvider {
+
+ SignalServiceMessageReceiver getMessageReceiver();
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
new file mode 100644
index 00000000..c81e2ff7
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
@@ -0,0 +1,123 @@
+package org.asamk.signal.manager.helper;
+
+import org.signal.zkgroup.profiles.ProfileKey;
+import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
+import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
+import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
+import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
+import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
+import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
+import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
+import org.whispersystems.signalservice.internal.util.concurrent.CascadingFuture;
+import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public final class ProfileHelper {
+
+ private final ProfileKeyProvider profileKeyProvider;
+
+ private final UnidentifiedAccessProvider unidentifiedAccessProvider;
+
+ private final MessagePipeProvider messagePipeProvider;
+
+ private final MessageReceiverProvider messageReceiverProvider;
+
+ public ProfileHelper(
+ final ProfileKeyProvider profileKeyProvider,
+ final UnidentifiedAccessProvider unidentifiedAccessProvider,
+ final MessagePipeProvider messagePipeProvider,
+ final MessageReceiverProvider messageReceiverProvider
+ ) {
+ this.profileKeyProvider = profileKeyProvider;
+ this.unidentifiedAccessProvider = unidentifiedAccessProvider;
+ this.messagePipeProvider = messagePipeProvider;
+ this.messageReceiverProvider = messageReceiverProvider;
+ }
+
+ public ProfileAndCredential retrieveProfileSync(
+ SignalServiceAddress recipient, SignalServiceProfile.RequestType requestType
+ ) throws IOException {
+ try {
+ return retrieveProfile(recipient, requestType).get(10, TimeUnit.SECONDS);
+ } catch (ExecutionException e) {
+ if (e.getCause() instanceof PushNetworkException) {
+ throw (PushNetworkException) e.getCause();
+ } else if (e.getCause() instanceof NotFoundException) {
+ throw (NotFoundException) e.getCause();
+ } else {
+ throw new IOException(e);
+ }
+ } catch (InterruptedException | TimeoutException e) {
+ throw new PushNetworkException(e);
+ }
+ }
+
+ public ListenableFuture retrieveProfile(
+ SignalServiceAddress address, SignalServiceProfile.RequestType requestType
+ ) {
+ Optional unidentifiedAccess = getUnidentifiedAccess(address);
+ Optional profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(address));
+
+ if (unidentifiedAccess.isPresent()) {
+ return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address,
+ profileKey,
+ unidentifiedAccess,
+ requestType),
+ () -> getSocketRetrievalFuture(address, profileKey, unidentifiedAccess, requestType),
+ () -> getPipeRetrievalFuture(address, profileKey, Optional.absent(), requestType),
+ () -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)),
+ e -> !(e instanceof NotFoundException));
+ } else {
+ return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address,
+ profileKey,
+ Optional.absent(),
+ requestType), () -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)),
+ e -> !(e instanceof NotFoundException));
+ }
+ }
+
+ private ListenableFuture getPipeRetrievalFuture(
+ SignalServiceAddress address,
+ Optional profileKey,
+ Optional unidentifiedAccess,
+ SignalServiceProfile.RequestType requestType
+ ) throws IOException {
+ SignalServiceMessagePipe unidentifiedPipe = messagePipeProvider.getMessagePipe(true);
+ SignalServiceMessagePipe pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent()
+ ? unidentifiedPipe
+ : messagePipeProvider.getMessagePipe(false);
+ if (pipe != null) {
+ return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
+ }
+
+ throw new IOException("No pipe available!");
+ }
+
+ private ListenableFuture getSocketRetrievalFuture(
+ SignalServiceAddress address,
+ Optional profileKey,
+ Optional unidentifiedAccess,
+ SignalServiceProfile.RequestType requestType
+ ) {
+ SignalServiceMessageReceiver receiver = messageReceiverProvider.getMessageReceiver();
+ return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
+ }
+
+ private Optional getUnidentifiedAccess(SignalServiceAddress recipient) {
+ Optional unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipient);
+
+ if (unidentifiedAccess.isPresent()) {
+ return unidentifiedAccess.get().getTargetUnidentifiedAccess();
+ }
+
+ return Optional.absent();
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/ProfileKeyCredentialProvider.java b/src/main/java/org/asamk/signal/manager/helper/ProfileKeyCredentialProvider.java
new file mode 100644
index 00000000..ebb728c1
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/ProfileKeyCredentialProvider.java
@@ -0,0 +1,9 @@
+package org.asamk.signal.manager.helper;
+
+import org.signal.zkgroup.profiles.ProfileKeyCredential;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+public interface ProfileKeyCredentialProvider {
+
+ ProfileKeyCredential getProfileKeyCredential(SignalServiceAddress address);
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java b/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java
new file mode 100644
index 00000000..9172710e
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java
@@ -0,0 +1,9 @@
+package org.asamk.signal.manager.helper;
+
+import org.signal.zkgroup.profiles.ProfileKey;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+public interface ProfileKeyProvider {
+
+ ProfileKey getProfileKey(SignalServiceAddress address);
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java b/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java
new file mode 100644
index 00000000..1ff4cb05
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java
@@ -0,0 +1,9 @@
+package org.asamk.signal.manager.helper;
+
+import org.asamk.signal.storage.profiles.SignalProfile;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+public interface ProfileProvider {
+
+ SignalProfile getProfile(SignalServiceAddress address);
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/SelfAddressProvider.java b/src/main/java/org/asamk/signal/manager/helper/SelfAddressProvider.java
new file mode 100644
index 00000000..3591064f
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/SelfAddressProvider.java
@@ -0,0 +1,8 @@
+package org.asamk.signal.manager.helper;
+
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+public interface SelfAddressProvider {
+
+ SignalServiceAddress getSelfAddress();
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/SelfProfileKeyProvider.java b/src/main/java/org/asamk/signal/manager/helper/SelfProfileKeyProvider.java
new file mode 100644
index 00000000..8fa51835
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/SelfProfileKeyProvider.java
@@ -0,0 +1,8 @@
+package org.asamk.signal.manager.helper;
+
+import org.signal.zkgroup.profiles.ProfileKey;
+
+public interface SelfProfileKeyProvider {
+
+ ProfileKey getProfileKey();
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java
new file mode 100644
index 00000000..97331cf3
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java
@@ -0,0 +1,105 @@
+package org.asamk.signal.manager.helper;
+
+import org.asamk.signal.storage.profiles.SignalProfile;
+import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
+import org.signal.zkgroup.profiles.ProfileKey;
+import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
+import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.whispersystems.signalservice.internal.util.Util.getSecretBytes;
+
+public class UnidentifiedAccessHelper {
+
+ private final SelfProfileKeyProvider selfProfileKeyProvider;
+
+ private final ProfileKeyProvider profileKeyProvider;
+
+ private final ProfileProvider profileProvider;
+
+ private final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider;
+
+ public UnidentifiedAccessHelper(
+ final SelfProfileKeyProvider selfProfileKeyProvider,
+ final ProfileKeyProvider profileKeyProvider,
+ final ProfileProvider profileProvider,
+ final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider
+ ) {
+ this.selfProfileKeyProvider = selfProfileKeyProvider;
+ this.profileKeyProvider = profileKeyProvider;
+ this.profileProvider = profileProvider;
+ this.senderCertificateProvider = senderCertificateProvider;
+ }
+
+ public byte[] getSelfUnidentifiedAccessKey() {
+ return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey());
+ }
+
+ public byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
+ ProfileKey theirProfileKey = profileKeyProvider.getProfileKey(recipient);
+ if (theirProfileKey == null) {
+ return null;
+ }
+
+ SignalProfile targetProfile = profileProvider.getProfile(recipient);
+ if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) {
+ return null;
+ }
+
+ if (targetProfile.isUnrestrictedUnidentifiedAccess()) {
+ return createUnrestrictedUnidentifiedAccess();
+ }
+
+ return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
+ }
+
+ public Optional getAccessForSync() {
+ byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
+ byte[] selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
+
+ if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
+ return Optional.absent();
+ }
+
+ try {
+ return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(selfUnidentifiedAccessKey,
+ selfUnidentifiedAccessCertificate),
+ new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)));
+ } catch (InvalidCertificateException e) {
+ return Optional.absent();
+ }
+ }
+
+ public List> getAccessFor(Collection recipients) {
+ return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
+ }
+
+ public Optional getAccessFor(SignalServiceAddress recipient) {
+ byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
+ byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
+ byte[] selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
+
+ if (recipientUnidentifiedAccessKey == null
+ || selfUnidentifiedAccessKey == null
+ || selfUnidentifiedAccessCertificate == null) {
+ return Optional.absent();
+ }
+
+ try {
+ return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(recipientUnidentifiedAccessKey,
+ selfUnidentifiedAccessCertificate),
+ new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)));
+ } catch (InvalidCertificateException e) {
+ return Optional.absent();
+ }
+ }
+
+ private static byte[] createUnrestrictedUnidentifiedAccess() {
+ return getSecretBytes(16);
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessProvider.java b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessProvider.java
new file mode 100644
index 00000000..a4b65a6f
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessProvider.java
@@ -0,0 +1,10 @@
+package org.asamk.signal.manager.helper;
+
+import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+public interface UnidentifiedAccessProvider {
+
+ Optional getAccessFor(SignalServiceAddress address);
+}
diff --git a/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java
new file mode 100644
index 00000000..b0597346
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java
@@ -0,0 +1,6 @@
+package org.asamk.signal.manager.helper;
+
+public interface UnidentifiedAccessSenderCertificateProvider {
+
+ byte[] getSenderCertificate();
+}
diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java
index 6043d803..3af52708 100644
--- a/src/main/java/org/asamk/signal/storage/SignalAccount.java
+++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java
@@ -10,9 +10,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.asamk.signal.manager.GroupId;
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;
@@ -20,12 +22,15 @@ 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.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;
@@ -50,6 +55,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;
@@ -71,6 +78,7 @@ public class SignalAccount implements Closeable {
private JsonContactsStore contactStore;
private RecipientStore recipientStore;
private ProfileStore profileStore;
+ private StickerStore stickerStore;
private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
this.fileChannel = fileChannel;
@@ -82,12 +90,12 @@ 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();
+ account.load(dataPath);
return account;
} catch (Throwable e) {
pair.second().close();
@@ -96,10 +104,12 @@ public class SignalAccount implements Closeable {
}
}
- public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey) throws IOException {
+ public static SignalAccount create(
+ 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);
}
@@ -109,19 +119,30 @@ public class SignalAccount implements Closeable {
account.username = username;
account.profileKey = profileKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
- account.groupStore = new JsonGroupStore();
+ account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.contactStore = new JsonContactsStore();
account.recipientStore = new RecipientStore();
account.profileStore = new ProfileStore();
+ account.stickerStore = new StickerStore();
account.registered = false;
return account;
}
- public static SignalAccount createLinkedAccount(String dataPath, String username, UUID uuid, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException {
+ public static SignalAccount createLinkedAccount(
+ File dataPath,
+ String username,
+ UUID uuid,
+ String password,
+ int deviceId,
+ IdentityKeyPair identityKey,
+ int registrationId,
+ String signalingKey,
+ 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);
}
@@ -135,29 +156,42 @@ public class SignalAccount implements Closeable {
account.deviceId = deviceId;
account.signalingKey = signalingKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
- account.groupStore = new JsonGroupStore();
+ account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.contactStore = new JsonContactsStore();
account.recipientStore = new RecipientStore();
account.profileStore = new ProfileStore();
+ account.stickerStore = new StickerStore();
account.registered = true;
account.isMultiDevice = true;
return account;
}
- public static String getFileName(String dataPath, String username) {
- return dataPath + "/" + username;
+ 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() throws IOException {
+ private void load(File dataPath) throws IOException {
JsonNode rootNode;
synchronized (fileChannel) {
fileChannel.position(0);
@@ -200,18 +234,22 @@ public class SignalAccount implements Closeable {
try {
profileKey = new ProfileKey(Base64.decode(Util.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", e);
+ throw new IOException(
+ "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
+ e);
}
}
- signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
+ signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"),
+ JsonSignalProtocolStore.class);
registered = Util.getNotNullNode(rootNode, "registered").asBoolean();
JsonNode groupStoreNode = rootNode.get("groupStore");
if (groupStoreNode != null) {
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
+ groupStore.groupCachePath = getGroupCachePath(dataPath, username);
}
if (groupStore == null) {
- groupStore = new JsonGroupStore();
+ groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
}
JsonNode contactStoreNode = rootNode.get("contactStore");
@@ -236,9 +274,12 @@ public class SignalAccount implements Closeable {
}
for (GroupInfo group : groupStore.getGroups()) {
- group.members = group.members.stream()
- .map(m -> recipientStore.resolveServiceAddress(m))
- .collect(Collectors.toSet());
+ if (group instanceof GroupInfoV1) {
+ GroupInfoV1 groupInfoV1 = (GroupInfoV1) group;
+ groupInfoV1.members = groupInfoV1.members.stream()
+ .map(m -> recipientStore.resolveServiceAddress(m))
+ .collect(Collectors.toSet());
+ }
}
for (SessionInfo session : signalProtocolStore.getSessions()) {
@@ -258,9 +299,18 @@ public class SignalAccount implements Closeable {
profileStore = new ProfileStore();
}
+ JsonNode stickerStoreNode = rootNode.get("stickerStore");
+ if (stickerStoreNode != null) {
+ stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class);
+ }
+ if (stickerStore == null) {
+ stickerStore = new StickerStore();
+ }
+
JsonNode threadStoreNode = rootNode.get("threadStore");
if (threadStoreNode != null) {
- LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
+ LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode,
+ LegacyJsonThreadStore.class);
// Migrate thread info to group and contact store
for (ThreadInfo thread : threadStore.getThreads()) {
if (thread.id == null || thread.id.isEmpty()) {
@@ -272,9 +322,9 @@ public class SignalAccount implements Closeable {
contactInfo.messageExpirationTime = thread.messageExpirationTime;
contactStore.updateContact(contactInfo);
} else {
- GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id));
- if (groupInfo != null) {
- groupInfo.messageExpirationTime = thread.messageExpirationTime;
+ GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
+ if (groupInfo instanceof GroupInfoV1) {
+ ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
groupStore.updateGroup(groupInfo);
}
}
@@ -305,7 +355,7 @@ public class SignalAccount implements Closeable {
.putPOJO("contactStore", contactStore)
.putPOJO("recipientStore", recipientStore)
.putPOJO("profileStore", profileStore)
- ;
+ .putPOJO("stickerStore", stickerStore);
try {
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
// Write to memory first to prevent corrupting the file in case of serialization errors
@@ -319,17 +369,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);
}
@@ -370,6 +420,10 @@ public class SignalAccount implements Closeable {
return profileStore;
}
+ public StickerStore getStickerStore() {
+ return stickerStore;
+ }
+
public String getUsername() {
return username;
}
diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
index 4d3a5e95..3b155210 100644
--- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
+++ b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
@@ -7,6 +7,8 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.UUID;
+import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY;
+
public class ContactInfo {
@JsonProperty
@@ -24,7 +26,7 @@ public class ContactInfo {
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
- @JsonProperty
+ @JsonProperty(access = WRITE_ONLY)
public String profileKey;
@JsonProperty(defaultValue = "false")
diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java
index 4b0adcd0..40b8c884 100644
--- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java
+++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java
@@ -1,123 +1,63 @@
package org.asamk.signal.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.JsonSerializer;
-import com.fasterxml.jackson.databind.ObjectMapper;
-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.GroupId;
+import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.HashSet;
import java.util.Set;
-import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
-public class GroupInfo {
+public abstract class GroupInfo {
- private static final ObjectMapper jsonProcessor = new ObjectMapper();
-
- @JsonProperty
- public final byte[] groupId;
-
- @JsonProperty
- public String name;
-
- @JsonProperty
- @JsonDeserialize(using = MembersDeserializer.class)
- @JsonSerialize(using = MembersSerializer.class)
- public Set members = new HashSet<>();
- @JsonProperty
- public String color;
- @JsonProperty(defaultValue = "0")
- public int messageExpirationTime;
- @JsonProperty(defaultValue = "false")
- public boolean blocked;
- @JsonProperty
- public Integer inboxPosition;
- @JsonProperty(defaultValue = "false")
- public boolean archived;
-
- private long avatarId;
-
- @JsonProperty
@JsonIgnore
- private boolean active;
+ public abstract GroupId getGroupId();
- public GroupInfo(byte[] groupId) {
- this.groupId = groupId;
- }
+ @JsonIgnore
+ public abstract String getTitle();
- public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived, @JsonProperty("messageExpirationTime") int messageExpirationTime) {
- this.groupId = groupId;
- this.name = name;
- this.members.addAll(members);
- this.avatarId = avatarId;
- this.color = color;
- this.blocked = blocked;
- this.inboxPosition = inboxPosition;
- this.archived = archived;
- this.messageExpirationTime = messageExpirationTime;
+ @JsonIgnore
+ public abstract GroupInviteLinkUrl getGroupInviteLink();
+
+ @JsonIgnore
+ public abstract Set getMembers();
+
+ @JsonIgnore
+ public Set getPendingMembers() {
+ return Set.of();
}
@JsonIgnore
- public long getAvatarId() {
- return avatarId;
+ public Set getRequestingMembers() {
+ return Set.of();
}
@JsonIgnore
- public Set getMembers() {
- return members;
- }
+ public abstract boolean isBlocked();
@JsonIgnore
- public Set getMembersE164() {
- Set membersE164 = new HashSet<>();
- for (SignalServiceAddress member : members) {
- if (!member.getNumber().isPresent()) {
- continue;
- }
- membersE164.add(member.getNumber().get());
- }
- return membersE164;
- }
+ public abstract void setBlocked(boolean blocked);
+
+ @JsonIgnore
+ public abstract int getMessageExpirationTime();
@JsonIgnore
public Set getMembersWithout(SignalServiceAddress address) {
- Set members = new HashSet<>(this.members.size());
- for (SignalServiceAddress member : this.members) {
- if (!member.matches(address)) {
- members.add(member);
- }
- }
- return members;
+ return getMembers().stream().filter(member -> !member.matches(address)).collect(Collectors.toSet());
}
- public void addMembers(Collection addresses) {
- for (SignalServiceAddress address : addresses) {
- if (this.members.contains(address)) {
- continue;
- }
- removeMember(address);
- this.members.add(address);
- }
- }
-
- public void removeMember(SignalServiceAddress address) {
- this.members.removeIf(member -> member.matches(address));
+ @JsonIgnore
+ public Set getMembersIncludingPendingWithout(SignalServiceAddress address) {
+ return Stream.concat(getMembers().stream(), getPendingMembers().stream())
+ .filter(member -> !member.matches(address))
+ .collect(Collectors.toSet());
}
@JsonIgnore
public boolean isMember(SignalServiceAddress address) {
- for (SignalServiceAddress member : this.members) {
+ for (SignalServiceAddress member : getMembers()) {
if (member.matches(address)) {
return true;
}
@@ -125,61 +65,13 @@ public class GroupInfo {
return false;
}
- private static final class JsonSignalServiceAddress {
-
- @JsonProperty
- private UUID uuid;
-
- @JsonProperty
- private String number;
-
- JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) {
- this.uuid = uuid;
- this.number = number;
- }
-
- JsonSignalServiceAddress(SignalServiceAddress address) {
- this.uuid = address.getUuid().orNull();
- this.number = address.getNumber().orNull();
- }
-
- SignalServiceAddress toSignalServiceAddress() {
- return new SignalServiceAddress(uuid, number);
- }
- }
-
- private static class MembersSerializer extends JsonSerializer> {
-
- @Override
- public void serialize(final Set value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
- jgen.writeStartArray(value.size());
- for (SignalServiceAddress address : value) {
- if (address.getUuid().isPresent()) {
- jgen.writeObject(new JsonSignalServiceAddress(address));
- } else {
- jgen.writeString(address.getNumber().get());
- }
+ @JsonIgnore
+ public boolean isPendingMember(SignalServiceAddress address) {
+ for (SignalServiceAddress member : getPendingMembers()) {
+ if (member.matches(address)) {
+ return true;
}
- jgen.writeEndArray();
- }
- }
-
- private static class MembersDeserializer extends JsonDeserializer> {
-
- @Override
- public Set deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
- Set addresses = new HashSet<>();
- JsonNode node = jsonParser.getCodec().readTree(jsonParser);
- for (JsonNode n : node) {
- if (n.isTextual()) {
- addresses.add(new SignalServiceAddress(null, n.textValue()));
- } else {
- JsonSignalServiceAddress address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class);
- addresses.add(address.toSignalServiceAddress());
- }
- }
-
- return addresses;
}
+ return false;
}
}
diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java
new file mode 100644
index 00000000..90b26b81
--- /dev/null
+++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java
@@ -0,0 +1,212 @@
+package org.asamk.signal.storage.groups;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.GroupId;
+import org.asamk.signal.manager.GroupIdV1;
+import org.asamk.signal.manager.GroupIdV2;
+import org.asamk.signal.manager.GroupInviteLinkUrl;
+import org.asamk.signal.manager.GroupUtils;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+public class GroupInfoV1 extends GroupInfo {
+
+ private static final ObjectMapper jsonProcessor = new ObjectMapper();
+
+ private final GroupIdV1 groupId;
+
+ private GroupIdV2 expectedV2Id;
+
+ @JsonProperty
+ public String name;
+
+ @JsonProperty
+ @JsonDeserialize(using = MembersDeserializer.class)
+ @JsonSerialize(using = MembersSerializer.class)
+ public Set members = new HashSet<>();
+ @JsonProperty
+ public String color;
+ @JsonProperty(defaultValue = "0")
+ public int messageExpirationTime;
+ @JsonProperty(defaultValue = "false")
+ public boolean blocked;
+ @JsonProperty
+ public Integer inboxPosition;
+ @JsonProperty(defaultValue = "false")
+ public boolean archived;
+
+ public GroupInfoV1(GroupIdV1 groupId) {
+ this.groupId = groupId;
+ }
+
+ public GroupInfoV1(
+ @JsonProperty("groupId") byte[] groupId,
+ @JsonProperty("expectedV2Id") byte[] expectedV2Id,
+ @JsonProperty("name") String name,
+ @JsonProperty("members") Collection members,
+ @JsonProperty("avatarId") long _ignored_avatarId,
+ @JsonProperty("color") String color,
+ @JsonProperty("blocked") boolean blocked,
+ @JsonProperty("inboxPosition") Integer inboxPosition,
+ @JsonProperty("archived") boolean archived,
+ @JsonProperty("messageExpirationTime") int messageExpirationTime,
+ @JsonProperty("active") boolean _ignored_active
+ ) {
+ this.groupId = GroupId.v1(groupId);
+ this.expectedV2Id = GroupId.v2(expectedV2Id);
+ this.name = name;
+ this.members.addAll(members);
+ this.color = color;
+ this.blocked = blocked;
+ this.inboxPosition = inboxPosition;
+ this.archived = archived;
+ 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 expectedV2Id.serialize();
+ }
+
+ @Override
+ public String getTitle() {
+ return name;
+ }
+
+ @Override
+ public GroupInviteLinkUrl getGroupInviteLink() {
+ return null;
+ }
+
+ @JsonIgnore
+ public Set getMembers() {
+ return members;
+ }
+
+ @Override
+ public boolean isBlocked() {
+ return blocked;
+ }
+
+ @Override
+ public void setBlocked(final boolean blocked) {
+ this.blocked = blocked;
+ }
+
+ @Override
+ public int getMessageExpirationTime() {
+ return messageExpirationTime;
+ }
+
+ public void addMembers(Collection addresses) {
+ for (SignalServiceAddress address : addresses) {
+ if (this.members.contains(address)) {
+ continue;
+ }
+ removeMember(address);
+ this.members.add(address);
+ }
+ }
+
+ public void removeMember(SignalServiceAddress address) {
+ this.members.removeIf(member -> member.matches(address));
+ }
+
+ private static final class JsonSignalServiceAddress {
+
+ @JsonProperty
+ private UUID uuid;
+
+ @JsonProperty
+ private String number;
+
+ JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) {
+ this.uuid = uuid;
+ this.number = number;
+ }
+
+ JsonSignalServiceAddress(SignalServiceAddress address) {
+ this.uuid = address.getUuid().orNull();
+ this.number = address.getNumber().orNull();
+ }
+
+ SignalServiceAddress toSignalServiceAddress() {
+ return new SignalServiceAddress(uuid, number);
+ }
+ }
+
+ private static class MembersSerializer extends JsonSerializer> {
+
+ @Override
+ public void serialize(
+ final Set value, final JsonGenerator jgen, final SerializerProvider provider
+ ) throws IOException {
+ jgen.writeStartArray(value.size());
+ for (SignalServiceAddress address : value) {
+ if (address.getUuid().isPresent()) {
+ jgen.writeObject(new JsonSignalServiceAddress(address));
+ } else {
+ jgen.writeString(address.getNumber().get());
+ }
+ }
+ jgen.writeEndArray();
+ }
+ }
+
+ private static class MembersDeserializer extends JsonDeserializer> {
+
+ @Override
+ public Set deserialize(
+ JsonParser jsonParser, DeserializationContext deserializationContext
+ ) throws IOException {
+ Set addresses = new HashSet<>();
+ JsonNode node = jsonParser.getCodec().readTree(jsonParser);
+ for (JsonNode n : node) {
+ if (n.isTextual()) {
+ addresses.add(new SignalServiceAddress(null, n.textValue()));
+ } else {
+ JsonSignalServiceAddress address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class);
+ addresses.add(address.toSignalServiceAddress());
+ }
+ }
+
+ return addresses;
+ }
+ }
+}
diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java
new file mode 100644
index 00000000..1b00caaa
--- /dev/null
+++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java
@@ -0,0 +1,115 @@
+package org.asamk.signal.storage.groups;
+
+import org.asamk.signal.manager.GroupIdV2;
+import org.asamk.signal.manager.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 GroupIdV2 groupId, final GroupMasterKey masterKey) {
+ this.groupId = groupId;
+ this.masterKey = masterKey;
+ }
+
+ @Override
+ public GroupIdV2 getGroupId() {
+ return groupId;
+ }
+
+ public GroupMasterKey getMasterKey() {
+ return masterKey;
+ }
+
+ public void setGroup(final DecryptedGroup group) {
+ this.group = group;
+ }
+
+ public DecryptedGroup getGroup() {
+ return group;
+ }
+
+ @Override
+ public String getTitle() {
+ if (this.group == null) {
+ return null;
+ }
+ 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 group.getMembersList()
+ .stream()
+ .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public Set getPendingMembers() {
+ if (this.group == null) {
+ return Collections.emptySet();
+ }
+ return group.getPendingMembersList()
+ .stream()
+ .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public Set getRequestingMembers() {
+ if (this.group == null) {
+ return Collections.emptySet();
+ }
+ return group.getRequestingMembersList()
+ .stream()
+ .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public boolean isBlocked() {
+ return blocked;
+ }
+
+ @Override
+ public void setBlocked(final boolean blocked) {
+ this.blocked = blocked;
+ }
+
+ @Override
+ public int getMessageExpirationTime() {
+ return this.group != null && this.group.hasDisappearingMessagesTimer()
+ ? this.group.getDisappearingMessagesTimer().getDuration()
+ : 0;
+ }
+}
diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java
index b8186b8b..18bf5ed0 100644
--- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java
+++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java
@@ -12,58 +12,191 @@ 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.GroupId;
+import org.asamk.signal.manager.GroupIdV1;
+import org.asamk.signal.manager.GroupIdV2;
+import org.asamk.signal.manager.GroupUtils;
+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;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonGroupStore {
- private static final ObjectMapper jsonProcessor = new ObjectMapper();
+ final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
- public static List groupsWithLegacyAvatarId = new ArrayList<>();
+ private static final ObjectMapper jsonProcessor = new ObjectMapper();
+ public File groupCachePath;
@JsonProperty("groups")
- @JsonSerialize(using = JsonGroupStore.MapToListSerializer.class)
- @JsonDeserialize(using = JsonGroupStore.GroupsDeserializer.class)
- private Map groups = new HashMap<>();
+ @JsonSerialize(using = GroupsSerializer.class)
+ @JsonDeserialize(using = GroupsDeserializer.class)
+ private final Map groups = new HashMap<>();
+
+ private JsonGroupStore() {
+ }
+
+ public JsonGroupStore(final File groupCachePath) {
+ this.groupCachePath = groupCachePath;
+ }
public void updateGroup(GroupInfo group) {
- groups.put(Base64.encodeBytes(group.groupId), group);
- }
-
- public GroupInfo getGroup(byte[] groupId) {
- return groups.get(Base64.encodeBytes(groupId));
- }
-
- public List getGroups() {
- return new ArrayList<>(groups.values());
- }
-
- private static class MapToListSerializer extends JsonSerializer