diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index a9284fc3..d72f15eb 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -50,10 +50,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5fc25c27..4972cbea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,26 @@
# Changelog
## [Unreleased]
+### Added
+- Accept group invitation with `updateGroup -g GROUP_ID`
+- Decline group invitation with `quitGroup -g GROUP_ID`
+
+### 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
diff --git a/build.gradle b/build.gradle
index 05015987..8b097612 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_11
mainClassName = 'org.asamk.signal.Main'
-version = '0.6.12'
+version = '0.7.0'
compileJava.options.encoding = 'UTF-8'
@@ -20,7 +20,7 @@ dependencies {
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 'com.github.hypfvieh:dbus-java:3.2.4'
implementation 'org.slf4j:slf4j-nop:1.7.30'
}
diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc
index 0bef0afc..e9b52593 100644
--- a/man/signal-cli.1.adoc
+++ b/man/signal-cli.1.adoc
@@ -181,6 +181,7 @@ Output received messages in json format, one object per line.
=== 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.
@@ -198,6 +199,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.
@@ -235,7 +237,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 4e4c33cf..41b91a48 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();
}
@@ -62,13 +66,13 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
SignalServiceDataMessage message = content.getDataMessage().get();
byte[] groupId = getGroupId(m, message);
- if (!message.isEndSession() &&
- (groupId == null
+ if (!message.isEndSession() && (
+ groupId == null
|| message.getGroupContext().get().getGroupV1Type() == null
- || message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.DELIVER)) {
+ || message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.DELIVER
+ )) {
try {
- conn.sendMessage(new Signal.MessageReceived(
- objectPath,
+ conn.sendMessage(new Signal.MessageReceived(objectPath,
message.getTimestamp(),
sender.getLegacyIdentifier(),
groupId != null ? groupId : new byte[0],
@@ -83,16 +87,19 @@ 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(m, 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() : "",
+ transcript.getDestination().isPresent() ? transcript.getDestination()
+ .get()
+ .getLegacyIdentifier() : "",
groupId != null ? groupId : new byte[0],
message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m)));
@@ -111,7 +118,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
if (message.getGroupContext().get().getGroupV1().isPresent()) {
groupId = message.getGroupContext().get().getGroupV1().get().getGroupId();
} else if (message.getGroupContext().get().getGroupV2().isPresent()) {
- groupId = m.getGroupId(message.getGroupContext().get().getGroupV2().get().getMasterKey());
+ groupId = GroupUtils.getGroupId(message.getGroupContext().get().getGroupV2().get().getMasterKey());
} else {
groupId = null;
}
diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java
index 0e42e20d..6b0b3811 100644
--- a/src/main/java/org/asamk/signal/Main.java
+++ b/src/main/java/org/asamk/signal/Main.java
@@ -84,8 +84,8 @@ public class Main {
busType = DBusConnection.DBusBusType.SESSION;
}
try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
- Signal ts = dBusConn.getRemoteObject(
- DbusConfig.SIGNAL_BUSNAME, DbusConfig.SIGNAL_OBJECTPATH,
+ Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME,
+ DbusConfig.SIGNAL_OBJECTPATH,
Signal.class);
return handleCommands(ns, ts, dBusConn);
@@ -103,7 +103,8 @@ public class Main {
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);
@@ -225,23 +226,16 @@ public class Main {
.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("-v", "--version").help("Show package version.").action(Arguments.version());
parser.addArgument("--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
parser.addArgument("-n", "--busname")
.help("Name of the DBus.");
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
- mut.addArgument("-u", "--username")
- .help("Specify your phone number, that will be used for verification.");
- mut.addArgument("--dbus")
- .help("Make request via user dbus.")
- .action(Arguments.storeTrue());
- mut.addArgument("--dbus-system")
- .help("Make request via system dbus.")
- .action(Arguments.storeTrue());
+ 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")
diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
index f32303b1..5925b2b8 100644
--- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
+++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java
@@ -1,5 +1,6 @@
package org.asamk.signal;
+import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.groups.GroupInfo;
@@ -52,7 +53,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());
}
@@ -70,18 +73,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);
@@ -107,7 +127,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()) {
@@ -140,15 +164,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);
@@ -164,24 +192,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"));
+ System.out.println(" - Link previews: " + (
+ configurationMessage.getLinkPreviews().get() ? "enabled" : "disabled"
+ ));
}
if (configurationMessage.getTypingIndicators().isPresent()) {
- System.out.println(" - Typing indicators: " + (configurationMessage.getTypingIndicators().get() ? "enabled" : "disabled"));
+ 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"));
+ System.out.println(" - Unidentified Delivery Indicators: " + (
+ configurationMessage.getUnidentifiedDeliveryIndicators().get()
+ ? "enabled"
+ : "disabled"
+ ));
}
}
if (syncMessage.getFetchType().isPresent()) {
@@ -195,7 +237,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());
@@ -208,21 +251,27 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
}
if (syncMessage.getMessageRequestResponse().isPresent()) {
- final MessageRequestResponseMessage requestResponseMessage = syncMessage.getMessageRequestResponse().get();
+ 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()));
+ System.out.println(" Group id: " + Base64.encodeBytes(requestResponseMessage.getGroupId()
+ .get()));
}
if (requestResponseMessage.getPerson().isPresent()) {
- System.out.println(" Person: " + requestResponseMessage.getPerson().get().getLegacyIdentifier());
+ 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);
+ System.out.println(" With storage key length: " + keysMessage.getStorageService()
+ .get()
+ .serialize().length);
} else {
System.out.println(" With empty storage key");
}
@@ -246,7 +295,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()) {
@@ -329,7 +381,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
} else if (groupContext.getGroupV2().isPresent()) {
final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get();
- byte[] groupId = m.getGroupId(groupInfo.getMasterKey());
+ byte[] groupId = GroupUtils.getGroupId(groupInfo.getMasterKey());
System.out.println(" Id: " + Base64.encodeBytes(groupId));
GroupInfo group = m.getGroup(groupId);
if (group != null) {
@@ -386,7 +438,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: " + m.resolveSignalServiceAddress(reaction.getTargetAuthor()).getLegacyIdentifier());
+ System.out.println(" - Target author: " + m.resolveSignalServiceAddress(reaction.getTargetAuthor())
+ .getLegacyIdentifier());
System.out.println(" - Target timestamp: " + reaction.getTargetSentTimestamp());
System.out.println(" - Is remove: " + reaction.isRemove());
}
@@ -417,7 +470,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
final List mentions = message.getMentions().get();
System.out.println("Mentions: ");
for (SignalServiceDataMessage.Mention mention : mentions) {
- System.out.println("- " + mention.getUuid() + ": " + mention.getStart() + " (length: " + mention.getLength() + ")");
+ System.out.println("- "
+ + mention.getUuid()
+ + ": "
+ + mention.getStart()
+ + " (length: "
+ + mention.getLength()
+ + ")");
}
}
@@ -430,12 +489,22 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
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..95c3738c 100644
--- a/src/main/java/org/asamk/signal/commands/BlockCommand.java
+++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java
@@ -13,12 +13,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)");
}
diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java
index 5cdb8ba1..138bbe0d 100644
--- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java
+++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java
@@ -66,7 +66,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/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 9e13685e..b9f54a6b 100644
--- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
@@ -22,18 +22,40 @@ public class ListGroupsCommand implements LocalCommand {
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
- System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s",
- Base64.encodeBytes(group.groupId), group.getTitle(), group.isMember(m.getSelfAddress()), group.isBlocked(), members));
+
+ 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());
+
+ System.out.println(String.format(
+ "Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s",
+ Base64.encodeBytes(group.groupId),
+ group.getTitle(),
+ group.isMember(m.getSelfAddress()),
+ group.isBlocked(),
+ members,
+ pendingMembers,
+ requestingMembers));
} else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
- Base64.encodeBytes(group.groupId), group.getTitle(), group.isMember(m.getSelfAddress()), group.isBlocked()));
+ Base64.encodeBytes(group.groupId),
+ 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");
}
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 6d0edf87..20d06eba 100644
--- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
+++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java
@@ -25,9 +25,7 @@ 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
@@ -38,7 +36,8 @@ public class QuitGroupCommand implements LocalCommand {
}
try {
- final Pair> results = m.sendQuitGroupMessage(Util.decodeGroupId(ns.getString("group")));
+ final byte[] groupId = Util.decodeGroupId(ns.getString("group"));
+ final Pair> results = m.sendQuitGroupMessage(groupId);
return handleTimestampAndSendMessageResults(results.first(), results.second());
} catch (IOException e) {
handleIOException(e);
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/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java
index 43166b5b..551cf938 100644
--- a/src/main/java/org/asamk/signal/commands/SendCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendCommand.java
@@ -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;
diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
index 3d000a62..6e5f24bb 100644
--- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java
@@ -29,11 +29,8 @@ 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.");
@@ -44,9 +41,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
@@ -73,7 +68,11 @@ public class SendReactionCommand implements LocalCommand {
byte[] groupId = Util.decodeGroupId(ns.getString("group"));
results = m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
} else {
- results = m.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, ns.getList("recipient"));
+ results = m.sendMessageReaction(emoji,
+ isRemove,
+ targetAuthor,
+ targetTimestamp,
+ ns.getList("recipient"));
}
handleTimestampAndSendMessageResults(results.first(), results.second());
return 0;
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..b4f6cc3b 100644
--- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java
@@ -13,12 +13,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)");
}
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..4216fd9b 100644
--- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
+++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java
@@ -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
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/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 77b3bc99..396063f2 100644
--- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
+++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
@@ -44,7 +44,9 @@ public class DbusSignalImpl implements Signal {
return sendMessage(message, attachments, recipients);
}
- private static void checkSendMessageResults(long timestamp, List results) throws DBusExecutionException {
+ private static void checkSendMessageResults(
+ long timestamp, List results
+ ) throws DBusExecutionException {
List errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
if (errors.size() == 0) {
return;
@@ -164,13 +166,29 @@ public class DbusSignalImpl implements Signal {
if (group == null) {
return Collections.emptyList();
} else {
- return group.getMembers().stream().map(m::resolveSignalServiceAddress).map(SignalServiceAddress::getLegacyIdentifier).collect(Collectors.toList());
+ 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 {
+ 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, name, members, avatar);
checkSendMessageResults(0, results.second());
return results.first();
diff --git a/src/main/java/org/asamk/signal/json/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java
index fc8538aa..653a59e6 100644
--- a/src/main/java/org/asamk/signal/json/JsonDataMessage.java
+++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java
@@ -4,6 +4,7 @@ import org.asamk.Signal;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import java.util.ArrayList;
import java.util.List;
@@ -19,9 +20,14 @@ class JsonDataMessage {
JsonDataMessage(SignalServiceDataMessage dataMessage) {
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();
@@ -41,19 +47,13 @@ class JsonDataMessage {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
- attachments = messageReceived.getAttachments()
- .stream()
- .map(JsonAttachment::new)
- .collect(Collectors.toList());
+ attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
}
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());
+ 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..970cde52 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 = Base64.encodeBytes(GroupUtils.getGroupId(groupInfo.getMasterKey()));
+ this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER";
+ }
+
JsonGroupInfo(byte[] groupId) {
this.groupId = Base64.encodeBytes(groupId);
}
diff --git a/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java
index b2ab7f75..ccd5960b 100644
--- a/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java
+++ b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java
@@ -23,7 +23,9 @@ class JsonReceiptMessage {
this.timestamps = receiptMessage.getTimestamps();
}
- private JsonReceiptMessage(final long when, final boolean isDelivery, final boolean isRead, final List timestamps) {
+ private JsonReceiptMessage(
+ final long when, final boolean isDelivery, final boolean isRead, final List timestamps
+ ) {
this.when = when;
this.isDelivery = isDelivery;
this.isRead = isRead;
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..0d192002
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/GroupUtils.java
@@ -0,0 +1,47 @@
+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.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.groupId)
+ .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 byte[] getGroupId(GroupMasterKey groupMasterKey) {
+ final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
+ return groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
+ }
+
+ public static GroupMasterKey deriveV2MigrationMasterKey(byte[] groupId) {
+ try {
+ return new GroupMasterKey(new HKDFv3().deriveSecrets(groupId,
+ "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..9bdd3885 100644
--- a/src/main/java/org/asamk/signal/manager/HandleAction.java
+++ b/src/main/java/org/asamk/signal/manager/HandleAction.java
@@ -30,8 +30,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
@@ -111,8 +110,7 @@ class SendGroupInfoRequestAction implements HandleAction {
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);
+ return address.equals(that.address) && Arrays.equals(groupId, that.groupId);
}
@Override
@@ -143,8 +141,7 @@ class SendGroupUpdateAction implements HandleAction {
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);
+ return address.equals(that.address) && Arrays.equals(groupId, that.groupId);
}
@Override
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..1f12193c 100644
--- a/src/main/java/org/asamk/signal/manager/KeyUtils.java
+++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java
@@ -30,10 +30,6 @@ class KeyUtils {
return getSecretBytes(16);
}
- static byte[] createUnrestrictedUnidentifiedAccess() {
- return getSecretBytes(16);
- }
-
static byte[] createStickerUploadKey() {
return getSecretBytes(32);
}
diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java
index 56376f0e..6db40566 100644
--- a/src/main/java/org/asamk/signal/manager/Manager.java
+++ b/src/main/java/org/asamk/signal/manager/Manager.java
@@ -18,6 +18,9 @@ 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;
@@ -40,7 +43,7 @@ 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.DecryptedMember;
import org.signal.zkgroup.InvalidInputException;
@@ -50,6 +53,7 @@ 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.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@@ -71,7 +75,6 @@ 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;
@@ -106,6 +109,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptM
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;
@@ -116,6 +120,9 @@ 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;
@@ -138,6 +145,7 @@ 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;
@@ -147,10 +155,10 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -158,31 +166,64 @@ 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.manager.ServiceConfig.getIasKeyStore;
public class Manager implements Closeable {
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 final 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() {
@@ -194,12 +235,12 @@ public class Manager implements Closeable {
}
private SignalServiceAccountManager createSignalServiceAccountManager() {
- GroupsV2Operations groupsV2Operations = capabilities.isGv2()
- ? new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration))
- : 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);
@@ -231,7 +272,9 @@ public class Manager implements Closeable {
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, String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent
+ ) throws IOException {
PathConfig pathConfig = PathConfig.createDefault(settingsPath);
if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
@@ -239,7 +282,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);
@@ -250,13 +297,12 @@ public class Manager implements Closeable {
Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent);
m.migrateLegacyConfigs();
- m.updateAccountAttributes();
return m;
}
private void migrateLegacyConfigs() {
- if (account.getProfileKey() == null) {
+ if (account.getProfileKey() == null && isRegistered()) {
// Old config file, creating new profile key
account.setProfileKey(KeyUtils.createProfileKey());
account.save();
@@ -276,6 +322,8 @@ public class Manager implements Closeable {
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 {
@@ -288,6 +336,7 @@ public class Manager implements Closeable {
account.setUuid(accountManager.getOwnUuid());
account.save();
}
+ updateAccountAttributes();
}
}
@@ -304,7 +353,9 @@ public class Manager implements Closeable {
this.groupsV2Api = accountManager.getGroupsV2Api();
if (voiceVerification) {
- accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.fromNullable(captcha), Optional.absent());
+ accountManager.requestVoiceVerificationCode(Locale.getDefault(),
+ Optional.fromNullable(captcha),
+ Optional.absent());
} else {
accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent());
}
@@ -314,7 +365,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 {
@@ -357,7 +416,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();
}
@@ -383,8 +446,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();
@@ -399,7 +466,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()
@@ -407,7 +483,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();
@@ -432,70 +511,166 @@ public class Manager implements Closeable {
accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys);
}
- private SignalServiceMessageReceiver getMessageReceiver() {
- final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2()
- ? ClientZkOperations.create(serviceConfiguration).getProfileOperations()
- : null;
- return new SignalServiceMessageReceiver(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), userAgent, null, timer, clientZkProfileOperations);
+ private SignalServiceMessageReceiver 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() {
- final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2()
- ? ClientZkOperations.create(serviceConfiguration).getProfileOperations()
- : null;
+ 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(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations, executor, ServiceConfig.MAX_ENVELOPE_SIZE);
+ 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) {
+ System.err.println("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) {
+ System.err.println("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());
}
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) {
@@ -532,89 +707,164 @@ public class Manager implements Closeable {
return g;
}
+ private GroupInfo getGroupForUpdating(byte[] 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;
+ }
+
public List getGroups() {
return account.getGroupStore().getGroups();
}
public Pair> sendGroupMessage(
- String messageText,
- List attachments,
- byte[] groupId
- )
- throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
- final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
- if (attachments != null) {
- messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments));
- }
-
+ SignalServiceDataMessage.Builder messageBuilder, byte[] groupId
+ ) throws IOException, GroupNotFoundException, NotAGroupMemberException {
final GroupInfo g = getGroupForSending(groupId);
- setGroupContext(messageBuilder, g);
+ GroupUtils.setGroupContext(messageBuilder, g);
messageBuilder.withExpiration(g.getMessageExpirationTime());
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
- private void setGroupContext(final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo) {
- if (groupInfo instanceof GroupInfoV1) {
- SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
- .withId(groupInfo.groupId)
- .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 Pair> sendGroupMessage(
+ String messageText, List attachments, byte[] groupId
+ ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
+ final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
+ .withBody(messageText);
+ if (attachments != null) {
+ messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments));
}
+
+ return sendGroupMessage(messageBuilder, groupId);
}
- public Pair> sendGroupMessageReaction(String emoji, boolean remove, String targetAuthor,
- long targetSentTimestamp, byte[] groupId)
- throws IOException, 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, byte[] 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);
- final GroupInfo g = getGroupForSending(groupId);
- setGroupContext(messageBuilder, g);
- return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
+
+ return sendGroupMessage(messageBuilder, groupId);
}
public Pair> sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
- SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
- .withId(groupId)
- .build();
- SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
- .asGroupMessage(group);
+ SignalServiceDataMessage.Builder messageBuilder;
- final GroupInfo g = getGroupForSending(groupId);
+ final GroupInfo g = getGroupForUpdating(groupId);
if (g instanceof GroupInfoV1) {
GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
+ SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
+ .withId(groupId)
+ .build();
+ messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
groupInfoV1.removeMember(account.getSelfAddress());
account.getGroupStore().updateGroup(groupInfoV1);
} else {
- throw new RuntimeException("TODO Not implemented!");
+ 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);
}
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
- private Pair> sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
- GroupInfoV1 g;
+ private Pair> sendUpdateGroupMessage(
+ byte[] 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 GroupInfoV1(KeyUtils.createGroupId());
- g.addMembers(Collections.singleton(account.getSelfAddress()));
- } else {
- GroupInfo group = getGroupForSending(groupId);
- if (!(group instanceof GroupInfoV1)) {
- throw new RuntimeException("TODO Not implemented!");
+ GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile);
+ if (gv2 == null) {
+ GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId());
+ gv1.addMembers(Collections.singleton(account.getSelfAddress()));
+ updateGroupV1(gv1, name, members, avatarFile);
+ messageBuilder = getGroupUpdateMessageBuilder(gv1);
+ g = gv1;
+ } else {
+ messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
+ g = gv2;
+ }
+ } else {
+ 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());
+ 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.groupId, result.second());
+ } else {
+ GroupInfoV1 gv1 = (GroupInfoV1) group;
+ updateGroupV1(gv1, name, members, avatarFile);
+ messageBuilder = getGroupUpdateMessageBuilder(gv1);
+ g = gv1;
}
- g = (GroupInfoV1) group;
}
+ account.getGroupStore().updateGroup(g);
+
+ final Pair> result = sendMessage(messageBuilder,
+ g.getMembersIncludingPendingWithout(account.getSelfAddress()));
+ return new Pair<>(g.groupId, 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;
}
@@ -634,7 +884,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);
@@ -645,20 +897,15 @@ public class Manager implements Closeable {
File aFile = getGroupAvatarFile(g.groupId);
Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
-
- account.getGroupStore().updateGroup(g);
-
- SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
-
- final Pair> result = sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
- return new Pair<>(g.groupId, result.second());
}
- Pair> sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
+ Pair> sendUpdateGroupMessage(
+ byte[] groupId, SignalServiceAddress recipient
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
GroupInfoV1 g;
GroupInfo group = getGroupForSending(groupId);
if (!(group instanceof GroupInfoV1)) {
- throw new RuntimeException("TODO Not implemented!");
+ throw new RuntimeException("Received an invalid group request for a v2 group!");
}
g = (GroupInfoV1) group;
@@ -689,10 +936,21 @@ public class Manager implements Closeable {
return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build())
- .withExpiration(g.messageExpirationTime);
+ .withExpiration(g.getMessageExpirationTime());
}
- Pair> sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException {
+ 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(
+ byte[] groupId, SignalServiceAddress recipient
+ ) throws IOException {
SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO)
.withId(groupId);
@@ -703,23 +961,28 @@ public class Manager implements Closeable {
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 Pair> sendMessage(String messageText, List attachments,
- List recipients)
- throws IOException, 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()) {
@@ -734,18 +997,20 @@ public class Manager implements Closeable {
return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
}
- 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);
+ 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);
return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
}
public Pair> sendEndSessionMessage(List recipients) throws IOException, InvalidNumberException {
- SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
- .asEndSessionMessage();
+ SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
final Collection signalServiceAddresses = getSignalServiceAddresses(recipients);
try {
@@ -804,20 +1069,13 @@ public class Manager implements Closeable {
account.save();
}
- public Pair> updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, 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(
+ byte[] groupId, String name, List members, String avatar
+ ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
+ return sendUpdateGroupMessage(groupId,
+ name,
+ members == null ? null : getSignalServiceAddresses(members),
+ avatar);
}
/**
@@ -840,7 +1098,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);
}
@@ -868,7 +1128,7 @@ public class Manager implements Closeable {
public String uploadStickerPack(String path) throws IOException, StickerPackInvalidException {
SignalServiceStickerManifestUpload manifest = getSignalServiceStickerManifestUpload(path);
- SignalServiceMessageSender messageSender = getMessageSender();
+ SignalServiceMessageSender messageSender = createMessageSender();
byte[] packKey = KeyUtils.createStickerUploadKey();
String packId = messageSender.uploadStickerManifest(manifest, packKey);
@@ -878,14 +1138,20 @@ public class Manager implements Closeable {
account.save();
try {
- 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();
+ 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 String path
+ ) throws IOException, StickerPackInvalidException {
ZipFile zip = null;
String rootPath = null;
@@ -922,7 +1188,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);
}
@@ -940,14 +1209,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 {
@@ -960,7 +1228,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());
@@ -971,7 +1241,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);
@@ -981,7 +1253,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);
@@ -991,7 +1265,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);
@@ -1001,7 +1277,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);
@@ -1024,136 +1302,90 @@ public class Manager implements Closeable {
return certificate;
}
- private byte[] getSelfUnidentifiedAccessKey() {
- return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
- }
-
- private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
- ProfileKey theirProfileKey = account.getProfileStore().getProfileKey(recipient);
- if (theirProfileKey == null) {
- return null;
- }
- SignalProfile targetProfile;
+ private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException {
+ SignalServiceMessageSender messageSender = createMessageSender();
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;
}
}
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) {
+ System.err.println("Failed to resolve uuids from server: " + 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 Pair> sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients)
- throws IOException {
+ 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);
- if (messagePipe == null) {
- messagePipe = getMessageReceiver().createMessagePipe();
- }
- if (unidentifiedMessagePipe == null) {
- unidentifiedMessagePipe = getMessageReceiver().createUnidentifiedMessagePipe();
- }
+ 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 new Pair<>(timestamp, result);
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
+ e.getIdentityKey(),
+ TrustLevel.UNTRUSTED);
return new Pair<>(timestamp, Collections.emptyList());
}
} else {
@@ -1188,11 +1420,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,
@@ -1204,32 +1436,49 @@ public class Manager implements Closeable {
try {
long startTime = System.currentTimeMillis();
messageSender.sendMessage(syncMessage, unidentifiedAccess);
- return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), false, System.currentTimeMillis() - startTime);
+ 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);
@@ -1244,21 +1493,36 @@ public class Manager implements Closeable {
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
}
- private GroupsV2AuthorizationString getGroupAuthForToday(final GroupSecretParams groupSecretParams) throws IOException, VerificationFailedException {
+ 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);
- return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(), today, groupSecretParams, authCredentialResponse);
+ 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) {
+ private List handleSignalServiceDataMessage(
+ SignalServiceDataMessage message,
+ boolean isSync,
+ SignalServiceAddress source,
+ SignalServiceAddress destination,
+ boolean ignoreAttachments
+ ) {
List actions = new ArrayList<>();
if (message.getGroupContext().isPresent()) {
if (message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
- GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
+ GroupInfo group = account.getGroupStore().getGroupByV1Id(groupInfo.getGroupId());
if (group == null || group instanceof GroupInfoV1) {
GroupInfoV1 groupV1 = (GroupInfoV1) group;
switch (groupInfo.getType()) {
@@ -1273,7 +1537,8 @@ public class Manager implements Closeable {
try {
retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.groupId);
} catch (IOException | InvalidMessageException | MissingConfigurationException e) {
- System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getRemoteId() + "): " + e.getMessage());
+ System.err.println("Failed to retrieve group avatar (" + avatar.asPointer()
+ .getRemoteId() + "): " + e.getMessage());
}
}
}
@@ -1283,7 +1548,8 @@ public class Manager implements Closeable {
}
if (groupInfo.getMembers().isPresent()) {
- groupV1.addMembers(groupInfo.getMembers().get()
+ groupV1.addMembers(groupInfo.getMembers()
+ .get()
.stream()
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toSet()));
@@ -1311,7 +1577,7 @@ public class Manager implements Closeable {
break;
}
} else {
- System.err.println("Received a group v1 message for a v2 group: " + group.getTitle());
+ // Received a group v1 message for a v2 group
}
}
if (message.getGroupContext().get().getGroupV2().isPresent()) {
@@ -1321,30 +1587,40 @@ public class Manager implements Closeable {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
- GroupInfo groupInfo = account.getGroupStore().getGroup(groupId);
+ GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId);
if (groupInfo instanceof GroupInfoV1) {
- // TODO upgrade group
+ // Received a v2 group message for a v2 group, we need to locally migrate the group
+ account.getGroupStore().deleteGroup(groupInfo.groupId);
+ GroupInfoV2 groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
+ groupInfoV2.setGroup(getDecryptedGroup(groupSecretParams));
+ account.getGroupStore().updateGroup(groupInfoV2);
+ System.err.println("Locally migrated group "
+ + Base64.encodeBytes(groupInfo.groupId)
+ + " to group v2, id: "
+ + Base64.encodeBytes(groupInfoV2.groupId)
+ + " !!!");
} else if (groupInfo == null || groupInfo instanceof GroupInfoV2) {
GroupInfoV2 groupInfoV2 = groupInfo == null
? new GroupInfoV2(groupId, groupMasterKey)
: (GroupInfoV2) groupInfo;
- if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < groupContext.getRevision()) {
- // TODO check if revision is only 1 behind and a signedGroupChange is available
- try {
- final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
- final DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
- groupInfoV2.setGroup(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) {
- }
+ if (groupInfoV2.getGroup() == null
+ || groupInfoV2.getGroup().getRevision() < groupContext.getRevision()) {
+ DecryptedGroup group = null;
+ if (groupContext.hasSignedGroupChange()
+ && groupInfoV2.getGroup() != null
+ && groupInfoV2.getGroup().getRevision() + 1 == groupContext.getRevision()) {
+ group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(),
+ groupContext.getSignedGroupChange(),
+ groupMasterKey);
+ if (group != null) {
+ storeProfileKeysFromMembers(group);
}
- } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
- System.err.println("Failed to retrieve Group V2 info, ignoring ...");
}
+ if (group == null) {
+ group = getDecryptedGroup(groupSecretParams);
+ }
+ groupInfoV2.setGroup(group);
account.getGroupStore().updateGroup(groupInfoV2);
}
}
@@ -1385,7 +1661,10 @@ 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());
+ System.err.println("Failed to retrieve attachment ("
+ + attachment.asPointer().getRemoteId()
+ + "): "
+ + e.getMessage());
}
}
}
@@ -1410,7 +1689,10 @@ public class Manager implements Closeable {
try {
retrieveAttachment(attachment);
} catch (IOException | InvalidMessageException | MissingConfigurationException e) {
- System.err.println("Failed to retrieve attachment (" + attachment.getRemoteId() + "): " + e.getMessage());
+ System.err.println("Failed to retrieve attachment ("
+ + attachment.getRemoteId()
+ + "): "
+ + e.getMessage());
}
}
}
@@ -1426,7 +1708,33 @@ public class Manager implements Closeable {
return actions;
}
- private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
+ private DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
+ try {
+ final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
+ DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
+ storeProfileKeysFromMembers(group);
+ return group;
+ } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
+ System.err.println("Failed to retrieve Group V2 info, ignoring ...");
+ return null;
+ }
+ }
+
+ private 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 = new File(getMessageCachePath());
if (!cachePath.exists()) {
return;
@@ -1448,7 +1756,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);
@@ -1492,15 +1802,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;
@@ -1517,7 +1830,8 @@ 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());
+ System.err.println("Failed to store encrypted message in disk cache, ignoring: "
+ + e.getMessage());
}
});
if (result.isPresent()) {
@@ -1543,8 +1857,7 @@ 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());
@@ -1597,7 +1910,9 @@ public class Manager implements Closeable {
}
}
- private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) {
+ private boolean isMessageBlocked(
+ SignalServiceEnvelope envelope, SignalServiceContent content
+ ) {
SignalServiceAddress source;
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
source = envelope.getSourceAddress();
@@ -1613,19 +1928,34 @@ 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());
- return groupInfo.getType() == SignalServiceGroup.Type.DELIVER && group != null && group.isBlocked();
+ if (message.getGroupContext().isPresent()) {
+ GroupInfo group = null;
+ if (message.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
+ if (groupInfo.getType() == SignalServiceGroup.Type.DELIVER) {
+ group = getGroup(groupInfo.getGroupId());
+ }
+ }
+ if (message.getGroupContext().get().getGroupV2().isPresent()) {
+ SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get();
+ final GroupMasterKey groupMasterKey = groupContext.getMasterKey();
+ byte[] groupId = GroupUtils.getGroupId(groupMasterKey);
+ group = account.getGroupStore().getGroupByV2Id(groupId);
+ }
+ if (group != null && group.isBlocked()) {
+ return true;
+ }
}
}
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 {
@@ -1641,14 +1971,25 @@ 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();
+ if (destination != null) {
+ actions.addAll(handleSignalServiceDataMessage(message.getMessage(),
+ true,
+ sender,
+ destination,
+ ignoreAttachments));
+ }
}
if (syncMessage.getRequest().isPresent()) {
RequestMessage rm = syncMessage.getRequest().get();
@@ -1667,7 +2008,9 @@ public class Manager implements Closeable {
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) {
@@ -1707,7 +2050,10 @@ public class Manager implements Closeable {
try {
Files.delete(tmpFile.toPath());
} catch (IOException e) {
- System.err.println("Failed to delete received groups temp file “" + tmpFile + "”: " + e.getMessage());
+ System.err.println("Failed to delete received groups temp file “"
+ + tmpFile
+ + "”: "
+ + e.getMessage());
}
}
}
@@ -1721,7 +2067,8 @@ public class Manager implements Closeable {
try {
setGroupBlocked(groupId, true);
} catch (GroupNotFoundException e) {
- System.err.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64.encodeBytes(groupId));
+ System.err.println("BlockedListMessage contained groupID that was not found in GroupStore: "
+ + Base64.encodeBytes(groupId));
}
}
}
@@ -1730,7 +2077,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();
@@ -1756,7 +2104,10 @@ public class Manager implements Closeable {
}
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();
@@ -1778,17 +2129,24 @@ 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());
+ System.err.println("Failed to delete received contacts temp file “"
+ + 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();
+ final List stickerPackOperationMessages = syncMessage.getStickerPackOperations()
+ .get();
for (StickerPackOperationMessage m : stickerPackOperationMessages) {
if (!m.getPackId().isPresent()) {
continue;
@@ -1800,7 +2158,8 @@ public class Manager implements Closeable {
}
sticker = new Sticker(m.getPackId().get(), m.getPackKey().get());
}
- sticker.setInstalled(!m.getType().isPresent() || m.getType().get() == StickerPackOperationMessage.Type.INSTALL);
+ sticker.setInstalled(!m.getType().isPresent()
+ || m.getType().get() == StickerPackOperationMessage.Type.INSTALL);
account.getStickerStore().updateSticker(sticker);
}
}
@@ -1816,7 +2175,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();
@@ -1831,7 +2192,9 @@ public class Manager implements Closeable {
return new File(pathConfig.getAvatarsPath(), "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
}
- private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException, MissingConfigurationException {
+ private File retrieveGroupAvatarAttachment(
+ SignalServiceAttachment attachment, byte[] groupId
+ ) throws IOException, InvalidMessageException, MissingConfigurationException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
@@ -1846,13 +2209,18 @@ public class Manager implements Closeable {
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 {
@@ -1874,7 +2242,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)) {
@@ -1886,23 +2256,30 @@ 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());
+ System.err.println("Failed to delete received attachment temp file “"
+ + 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);
}
@@ -1915,10 +2292,16 @@ public class Manager implements Closeable {
for (GroupInfo record : account.getGroupStore().getGroups()) {
if (record instanceof GroupInfoV1) {
GroupInfoV1 groupInfo = (GroupInfoV1) record;
- out.write(new DeviceGroup(groupInfo.groupId, Optional.fromNullable(groupInfo.name),
- new ArrayList<>(groupInfo.getMembers()), createGroupAvatarAttachment(groupInfo.groupId),
- groupInfo.isMember(account.getSelfAddress()), Optional.of(groupInfo.messageExpirationTime),
- Optional.fromNullable(groupInfo.color), groupInfo.blocked, Optional.fromNullable(groupInfo.inboxPosition), groupInfo.archived));
+ out.write(new DeviceGroup(groupInfo.groupId,
+ Optional.fromNullable(groupInfo.name),
+ new ArrayList<>(groupInfo.getMembers()),
+ createGroupAvatarAttachment(groupInfo.groupId),
+ groupInfo.isMember(account.getSelfAddress()),
+ Optional.of(groupInfo.messageExpirationTime),
+ Optional.fromNullable(groupInfo.color),
+ groupInfo.blocked,
+ Optional.fromNullable(groupInfo.inboxPosition),
+ groupInfo.archived));
}
}
}
@@ -1951,26 +2334,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 = 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,
+ 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));
}
}
@@ -2010,8 +2407,13 @@ public class Manager implements Closeable {
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));
}
@@ -2027,11 +2429,6 @@ public class Manager implements Closeable {
return account.getGroupStore().getGroup(groupId);
}
- public byte[] getGroupId(GroupMasterKey groupMasterKey) {
- final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
- return groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
- }
-
public List getIdentities() {
return account.getSignalProtocolStore().getIdentities();
}
@@ -2057,7 +2454,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) {
@@ -2086,7 +2484,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) {
@@ -2111,7 +2510,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) {
@@ -2123,8 +2523,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() {
@@ -2132,7 +2537,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/PathConfig.java b/src/main/java/org/asamk/signal/manager/PathConfig.java
index 2c2d938a..c0c9e1e7 100644
--- a/src/main/java/org/asamk/signal/manager/PathConfig.java
+++ b/src/main/java/org/asamk/signal/manager/PathConfig.java
@@ -7,11 +7,7 @@ public class PathConfig {
private final String avatarsPath;
public static PathConfig createDefault(final String settingsPath) {
- return new PathConfig(
- settingsPath + "/data",
- settingsPath + "/attachments",
- settingsPath + "/avatars"
- );
+ return new PathConfig(settingsPath + "/data", settingsPath + "/attachments", settingsPath + "/avatars");
}
private PathConfig(final String dataPath, final String attachmentsPath, final String 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..eb70b351 100644
--- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
+++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
@@ -70,12 +70,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 +103,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 4498fc65..5721b166 100644
--- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java
+++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java
@@ -13,6 +13,10 @@ 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.List;
import java.util.Map;
@@ -29,12 +33,16 @@ public class ServiceConfig {
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();
@@ -57,30 +65,46 @@ public class ServiceConfig {
} catch (Throwable ignored) {
zkGroupAvailable = false;
}
- capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, 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 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[0],
+ 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) {
+ 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);
}
diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java
index 466cbcc3..0a815ea9 100644
--- a/src/main/java/org/asamk/signal/manager/Utils.java
+++ b/src/main/java/org/asamk/signal/manager/Utils.java
@@ -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);
@@ -116,7 +131,11 @@ class Utils {
}
static String createDeviceLinkUri(DeviceLinkInfo info) {
- return "tsdevice:/?uuid=" + URLEncoder.encode(info.deviceIdentifier, StandardCharsets.UTF_8) + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()), StandardCharsets.UTF_8);
+ 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 {
@@ -180,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);
}
}
@@ -230,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());
@@ -251,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..1f7e69e3
--- /dev/null
+++ b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
@@ -0,0 +1,323 @@
+package org.asamk.signal.manager.helper;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.asamk.signal.storage.groups.GroupInfoV2;
+import org.asamk.signal.util.IOUtils;
+import org.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.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.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.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 {
+
+ 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 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) {
+ System.err.println("Failed to create V2 group: " + e.getMessage());
+ return null;
+ }
+ if (decryptedGroup == null) {
+ System.err.println("Failed to create V2 group!");
+ return null;
+ }
+
+ final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
+ 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) {
+ System.err.println("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 int noUuidCapability = members.stream()
+ .filter(address -> !address.getUuid().isPresent())
+ .collect(Collectors.toUnmodifiableSet())
+ .size();
+ if (noUuidCapability > 0) {
+ System.err.println("Cannot create a V2 group as " + noUuidCapability + " members don't have a UUID.");
+ return false;
+ }
+
+ final int noGv2Capability = members.stream()
+ .map(profileProvider::getProfile)
+ .filter(profile -> profile != null && !profile.getCapabilities().gv2)
+ .collect(Collectors.toUnmodifiableSet())
+ .size();
+ if (noGv2Capability > 0) {
+ System.err.println("Cannot create a V2 group as " + noGv2Capability + " members don't support Groups V2.");
+ 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)) return null;
+
+ 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 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(change.build(),
+ groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
+ Optional.absent());
+
+ return new Pair<>(decryptedGroupState, signedGroupChange);
+ }
+
+ 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 dbb0ac04..4f9d8628 100644
--- a/src/main/java/org/asamk/signal/storage/SignalAccount.java
+++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java
@@ -99,7 +99,9 @@ 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(
+ String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey
+ ) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
String fileName = getFileName(dataPath, username);
if (!new File(fileName).exists()) {
@@ -122,7 +124,17 @@ public class SignalAccount implements Closeable {
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(
+ String 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()) {
@@ -209,11 +221,14 @@ 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) {
@@ -281,7 +296,8 @@ public class SignalAccount implements Closeable {
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()) {
@@ -326,8 +342,7 @@ public class SignalAccount implements Closeable {
.putPOJO("contactStore", contactStore)
.putPOJO("recipientStore", recipientStore)
.putPOJO("profileStore", profileStore)
- .putPOJO("stickerStore", stickerStore)
- ;
+ .putPOJO("stickerStore", stickerStore);
try {
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
// Write to memory first to prevent corrupting the file in case of serialization errors
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 db4f4690..4cd410b8 100644
--- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java
+++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java
@@ -5,8 +5,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import java.util.HashSet;
import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
public abstract class GroupInfo {
@@ -23,6 +24,16 @@ public abstract class GroupInfo {
@JsonIgnore
public abstract Set getMembers();
+ @JsonIgnore
+ public Set getPendingMembers() {
+ return Set.of();
+ }
+
+ @JsonIgnore
+ public Set getRequestingMembers() {
+ return Set.of();
+ }
+
@JsonIgnore
public abstract boolean isBlocked();
@@ -34,13 +45,14 @@ public abstract class GroupInfo {
@JsonIgnore
public Set getMembersWithout(SignalServiceAddress address) {
- Set members = new HashSet<>();
- for (SignalServiceAddress member : getMembers()) {
- if (!member.matches(address)) {
- members.add(member);
- }
- }
- return members;
+ return getMembers().stream().filter(member -> !member.matches(address)).collect(Collectors.toSet());
+ }
+
+ @JsonIgnore
+ public Set getMembersIncludingPendingWithout(SignalServiceAddress address) {
+ return Stream.concat(getMembers().stream(), getPendingMembers().stream())
+ .filter(member -> !member.matches(address))
+ .collect(Collectors.toSet());
}
@JsonIgnore
@@ -52,4 +64,14 @@ public abstract class GroupInfo {
}
return false;
}
+
+ @JsonIgnore
+ public boolean isPendingMember(SignalServiceAddress address) {
+ for (SignalServiceAddress member : getPendingMembers()) {
+ if (member.matches(address)) {
+ return true;
+ }
+ }
+ 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
index 9ec5178b..42c40e94 100644
--- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java
+++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java
@@ -25,6 +25,9 @@ public class GroupInfoV1 extends GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
+ @JsonProperty
+ public byte[] expectedV2Id;
+
@JsonProperty
public String name;
@@ -52,8 +55,21 @@ public class GroupInfoV1 extends GroupInfo {
return name;
}
- public GroupInfoV1(@JsonProperty("groupId") byte[] groupId, @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) {
+ 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
+ ) {
super(groupId);
+ this.expectedV2Id = expectedV2Id;
this.name = name;
this.members.addAll(members);
this.color = color;
@@ -123,7 +139,9 @@ public class GroupInfoV1 extends GroupInfo {
private static class MembersSerializer extends JsonSerializer> {
@Override
- public void serialize(final Set value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
+ 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()) {
@@ -139,7 +157,9 @@ public class GroupInfoV1 extends GroupInfo {
private static class MembersDeserializer extends JsonDeserializer> {
@Override
- public Set deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
+ public Set deserialize(
+ JsonParser jsonParser, DeserializationContext deserializationContext
+ ) throws IOException {
Set addresses = new HashSet<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java
index 5e3115a1..a205d140 100644
--- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java
+++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java
@@ -46,7 +46,30 @@ public class GroupInfoV2 extends GroupInfo {
if (this.group == null) {
return Collections.emptySet();
}
- return group.getMembersList().stream()
+ 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());
}
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 c73858a1..2175e293 100644
--- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java
+++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java
@@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.IOUtils;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
@@ -24,6 +25,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@@ -48,7 +50,7 @@ public class JsonGroupStore {
public void updateGroup(GroupInfo group) {
groups.put(Base64.encodeBytes(group.groupId), group);
- if (group instanceof GroupInfoV2) {
+ if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
try {
IOUtils.createPrivateDirectories(groupCachePath);
try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.groupId))) {
@@ -60,8 +62,38 @@ public class JsonGroupStore {
}
}
+ public void deleteGroup(byte[] groupId) {
+ groups.remove(Base64.encodeBytes(groupId));
+ }
+
public GroupInfo getGroup(byte[] groupId) {
final GroupInfo group = groups.get(Base64.encodeBytes(groupId));
+ if (group == null & groupId.length == 16) {
+ return getGroupByV1Id(groupId);
+ }
+ loadDecryptedGroup(group);
+ return group;
+ }
+
+ public GroupInfo getGroupByV1Id(byte[] groupIdV1) {
+ GroupInfo group = groups.get(Base64.encodeBytes(groupIdV1));
+ if (group == null) {
+ group = groups.get(Base64.encodeBytes(GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(groupIdV1))));
+ }
+ loadDecryptedGroup(group);
+ return group;
+ }
+
+ public GroupInfo getGroupByV2Id(byte[] groupIdV2) {
+ GroupInfo group = groups.get(Base64.encodeBytes(groupIdV2));
+ if (group == null) {
+ for (GroupInfo g : groups.values()) {
+ if (g instanceof GroupInfoV1 && Arrays.equals(groupIdV2, ((GroupInfoV1) g).expectedV2Id)) {
+ group = g;
+ break;
+ }
+ }
+ }
loadDecryptedGroup(group);
return group;
}
@@ -103,7 +135,9 @@ public class JsonGroupStore {
private static class GroupsSerializer extends JsonSerializer