From ca52c0103136bc1f0cb140c33e81ff17d47aec2e Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Aug 2021 20:56:41 +0200 Subject: [PATCH 01/16] Adapt log level --- lib/src/main/java/org/asamk/signal/manager/Manager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 92dcf4d6..577badc8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -317,7 +317,7 @@ public class Manager implements Closeable { public void checkAccountState() throws IOException { if (account.getLastReceiveTimestamp() == 0) { - logger.warn("The Signal protocol expects that incoming messages are regularly received."); + logger.info("The Signal protocol expects that incoming messages are regularly received."); } else { var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp(); long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS); -- 2.51.0 From 95792be9bcc1068c470630c13e0aebc55ed3bdc7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Aug 2021 21:21:12 +0200 Subject: [PATCH 02/16] Align cli param names for recipient --- .../java/org/asamk/signal/commands/BlockCommand.java | 4 ++-- .../asamk/signal/commands/GetUserStatusCommand.java | 10 +++++----- .../org/asamk/signal/commands/SendReceiptCommand.java | 6 ++++-- .../java/org/asamk/signal/commands/TrustCommand.java | 4 ++-- .../java/org/asamk/signal/commands/UnblockCommand.java | 4 ++-- .../asamk/signal/commands/UpdateContactCommand.java | 4 ++-- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 7326c398..105c2016 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -25,7 +25,7 @@ public class BlockCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Block the given contacts or groups (no messages will be received)"); - subparser.addArgument("contact").help("Contact number").nargs("*"); + subparser.addArgument("recipient").help("Contact number").nargs("*"); subparser.addArgument("-g", "--group-id", "--group").help("Group ID").nargs("*"); } @@ -33,7 +33,7 @@ public class BlockCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var contacts = ns.getList("contact"); + final var contacts = ns.getList("recipient"); for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getUsername())) { try { m.setContactBlocked(contact, true); diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index 055dac9f..cf4be085 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -31,7 +31,7 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Check if the specified phone number/s have been registered"); - subparser.addArgument("number").help("Phone number").nargs("+"); + subparser.addArgument("recipient").help("Phone number").nargs("+"); } @Override @@ -41,7 +41,7 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { // Get a map of registration statuses Map> registered; try { - registered = m.areUsersRegistered(new HashSet<>(ns.getList("number"))); + registered = m.areUsersRegistered(new HashSet<>(ns.getList("recipient"))); } catch (IOException e) { logger.debug("Failed to check registered users", e); throw new IOErrorException("Unable to check if users are registered"); @@ -69,7 +69,7 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { private static final class JsonUserStatus { - public final String name; + public final String recipient; public final String number; @@ -77,8 +77,8 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { public final boolean isRegistered; - public JsonUserStatus(String name, String number, String uuid, boolean isRegistered) { - this.name = name; + public JsonUserStatus(String recipient, String number, String uuid, boolean isRegistered) { + this.recipient = recipient; this.number = number; this.uuid = uuid; this.isRegistered = isRegistered; diff --git a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java index afdbd4f8..70e2f015 100644 --- a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java @@ -27,7 +27,9 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { .type(long.class) .nargs("+") .help("Specify the timestamp of the messages for which a receipt should be sent."); - subparser.addArgument("--type").help("Specify the receipt type.").choices("read", "viewed").setDefault("read"); + subparser.addArgument("--type") + .help("Specify the receipt type (default is read receipt).") + .choices("read", "viewed"); } @Override @@ -41,7 +43,7 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { final var type = ns.getString("type"); try { - if ("read".equals(type)) { + if (type == null || "read".equals(type)) { m.sendReadReceipt(recipient, targetTimestamps); } else if ("viewed".equals(type)) { m.sendViewedReceipt(recipient, targetTimestamps); diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 22dcc5d8..aedc2c3e 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -24,7 +24,7 @@ public class TrustCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Set the trust level of a given number."); - subparser.addArgument("number").help("Specify the phone number, for which to set the trust.").required(true); + subparser.addArgument("recipient").help("Specify the phone number, for which to set the trust.").required(true); var mutTrust = subparser.addMutuallyExclusiveGroup(); mutTrust.addArgument("-a", "--trust-all-known-keys") .help("Trust all known keys of this user, only use this for testing.") @@ -37,7 +37,7 @@ public class TrustCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - var recipentString = ns.getString("number"); + var recipentString = ns.getString("recipient"); var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getUsername()); if (ns.getBoolean("trust-all-known-keys")) { boolean res = m.trustIdentityAllKeys(recipient); diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index c5b9d1ca..e931a60e 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -25,7 +25,7 @@ public class UnblockCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Unblock the given contacts or groups (messages will be received again)"); - subparser.addArgument("contact").help("Contact number").nargs("*"); + subparser.addArgument("recipient").help("Contact number").nargs("*"); subparser.addArgument("-g", "--group-id", "--group").help("Group ID").nargs("*"); } @@ -33,7 +33,7 @@ public class UnblockCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("contact"), m.getUsername())) { + for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("recipient"), m.getUsername())) { try { m.setContactBlocked(contactNumber, false); } catch (NotMasterDeviceException e) { diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index 2b7d5b4b..8b9f9aa5 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -23,7 +23,7 @@ public class UpdateContactCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Update the details of a given contact"); - subparser.addArgument("number").help("Contact number"); + subparser.addArgument("recipient").help("Contact number"); subparser.addArgument("-n", "--name").help("New contact name"); subparser.addArgument("-e", "--expiration").type(int.class).help("Set expiration time of messages (seconds)"); } @@ -32,7 +32,7 @@ public class UpdateContactCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - var recipientString = ns.getString("number"); + var recipientString = ns.getString("recipient"); var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername()); try { -- 2.51.0 From 7106a997cf2f2d8325ae4e11dfb8424aa60999eb Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Aug 2021 21:27:20 +0200 Subject: [PATCH 03/16] Update tests --- graalvm-config-dir/reflect-config.json | 44 ++++++++++++++++++++++---- run_tests.sh | 44 +++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index bba5ef48..aef740aa 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -337,12 +337,36 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"org.asamk.signal.commands.ListContactsCommand$JsonContact", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.asamk.signal.commands.ListDevicesCommand$JsonDevice", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.asamk.signal.commands.ListGroupsCommand$JsonGroup", "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"org.asamk.signal.commands.ListGroupsCommand$JsonGroupMember", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.asamk.signal.commands.ListIdentitiesCommand$JsonIdentity", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.asamk.signal.json.JsonAttachment", "allDeclaredFields":true, @@ -487,32 +511,38 @@ "allDeclaredConstructors":true }, { - "name": "org.asamk.signal.jsonrpc.JsonRpcBulkMessage", + "name":"org.asamk.signal.jsonrpc.JsonRpcBulkMessage", "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, { - "name": "org.asamk.signal.jsonrpc.JsonRpcException", + "name":"org.asamk.signal.jsonrpc.JsonRpcException", "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, { - "name": "org.asamk.signal.jsonrpc.JsonRpcMessage", - "allDeclaredFields": true, + "name":"org.asamk.signal.jsonrpc.JsonRpcMessage", + "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, { - "name": "org.asamk.signal.jsonrpc.JsonRpcRequest", - "allDeclaredFields": true, + "name":"org.asamk.signal.jsonrpc.JsonRpcRequest", + "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, { "name":"org.asamk.signal.jsonrpc.JsonRpcResponse", - "allDeclaredFields": true, + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.asamk.signal.jsonrpc.JsonRpcResponse$Error", + "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, diff --git a/run_tests.sh b/run_tests.sh index 1405ada3..4ee46845 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -23,21 +23,21 @@ PATH_LINK="$PATH_TEST_CONFIG/link" ./gradlew installDist -function run() { +run() { set -x "$SIGNAL_CLI" --service-environment="sandbox" $@ set +x } -function run_main() { +run_main() { run --config="$PATH_MAIN" $@ } -function run_linked() { +run_linked() { run --config="$PATH_LINK" $@ } -function register() { +register() { NUMBER=$1 PIN=$2 echo -n "Enter a captcha token (https://signalcaptchas.org/registration/generate.html): " @@ -52,7 +52,7 @@ function register() { fi } -function link() { +link() { NUMBER=$1 LINK_CODE_FILE="$PATH_TEST_CONFIG/link_code" rm -f "$LINK_CODE_FILE" @@ -76,6 +76,40 @@ register "$NUMBER_2" sleep 5 +# JSON-RPC +FIFO_FILE="${PATH_MAIN}/dbus-fifo" + +rm -f "$FIFO_FILE" +mkfifo "$FIFO_FILE" + +run_main -u "$NUMBER_1" send "$NUMBER_2" -m hi +run_main -u "$NUMBER_2" jsonRpc < "$FIFO_FILE" & + +exec 3<> "$FIFO_FILE" + echo '{"jsonrpc":"2.0","id":"id","method":"updateContact","params":{"recipient":"'"$NUMBER_1"'","name":"NUMBER_1","expiration":10}}' >&3 + echo '{"jsonrpc":"2.0","id":5,"method":"block","params":{"recipient":"'"$NUMBER_1"'"}}' >&3 + echo '{"jsonrpc":"2.0","id":null,"method":"unblock","params":{"recipient":"'"$NUMBER_1"'"}}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"listContacts"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"listGroups"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"listDevices"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"listIdentities"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"sendSyncRequest"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"sendContacts"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"version"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"updateAccount"}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"sendReceipt","params":{"recipient":"'"$NUMBER_1"'","targetTimestamp":1629919505575}}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"sendTyping","params":{"recipient":"'"$NUMBER_1"'"}}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"send","params":{"recipient":"'"$NUMBER_1"'","message":"some text"}}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"send","params":{"recipients":["'"$NUMBER_1"'","'"$NUMBER_2"'"],"message":"some other text"}}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"updateProfile","params":{"givenName":"n1","familyName":"n2","about":"ABA","aboutEmoji":"EMO","avatar":"LICENSE"}}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"getUserStatus","params":{"recipient":"'"$NUMBER_1"'"}}' >&3 + + # Error expected: + echo '{"jsonrpc":"2.0","id":7,"method":"sendReceipt","params":{"recipient":5}}' >&3 +exec 3>&- + +wait + run_main -u "$NUMBER_1" setPin "$TEST_PIN_1" run_main -u "$NUMBER_2" removePin -- 2.51.0 From 6ee0a95aa2a9a1c56e9f6b975ffc879d1892759c Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Aug 2021 23:05:46 +0200 Subject: [PATCH 04/16] Update URL for reaching Signal chat server --- .../main/java/org/asamk/signal/manager/config/LiveConfig.java | 2 +- .../java/org/asamk/signal/manager/config/SandboxConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index 4298547d..7762a4cb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -32,7 +32,7 @@ class LiveConfig { "fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe"); private final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87"; - private final static String URL = "https://textsecure-service.whispersystems.org"; + private final static String URL = "https://chat.signal.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"; diff --git a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java index 9ca9dc8b..12d87cf5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java @@ -32,7 +32,7 @@ class SandboxConfig { "51a56084c0b21c6b8f62b1bc792ec9bedac4c7c3964bb08ddcab868158c09982"); private final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87"; - private final static String URL = "https://textsecure-service-staging.whispersystems.org"; + private final static String URL = "https://chat.staging.signal.org"; private final static String CDN_URL = "https://cdn-staging.signal.org"; private final static String CDN2_URL = "https://cdn2-staging.signal.org"; private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api-staging.directory.signal.org"; -- 2.51.0 From 944c3327ee4ea552d968fca1db89fa3318cec2d9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 08:47:02 +0200 Subject: [PATCH 05/16] Extract GroupHelper --- .../org/asamk/signal/manager/Manager.java | 645 +----------------- .../signal/manager/helper/GroupHelper.java | 628 +++++++++++++++++ .../signal/manager/helper/GroupV2Helper.java | 40 +- 3 files changed, 692 insertions(+), 621 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 577badc8..3857d803 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -34,6 +34,7 @@ import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; @@ -45,7 +46,6 @@ import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; -import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.messageCache.CachedMessage; @@ -60,23 +60,9 @@ import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; -import org.signal.libsignal.metadata.InvalidMetadataMessageException; -import org.signal.libsignal.metadata.InvalidMetadataVersionException; -import org.signal.libsignal.metadata.ProtocolDuplicateMessageException; -import org.signal.libsignal.metadata.ProtocolInvalidKeyException; -import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException; import org.signal.libsignal.metadata.ProtocolInvalidMessageException; -import org.signal.libsignal.metadata.ProtocolInvalidVersionException; -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.storageservice.protos.groups.GroupChange; -import org.signal.storageservice.protos.groups.local.DecryptedGroup; 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.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.slf4j.Logger; @@ -93,11 +79,9 @@ import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.InvalidMessageStructureException; import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -107,7 +91,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; -import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; @@ -125,18 +108,15 @@ 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.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.ConflictException; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; -import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; 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.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; @@ -187,9 +167,9 @@ public class Manager implements Closeable { private final ExecutorService executor = Executors.newCachedThreadPool(); private final ProfileHelper profileHelper; - private final GroupV2Helper groupV2Helper; private final PinHelper pinHelper; private final SendHelper sendHelper; + private final GroupHelper groupHelper; private final AvatarStore avatarStore; private final AttachmentStore attachmentStore; @@ -235,12 +215,11 @@ public class Manager implements Closeable { dependencies::getProfileService, dependencies::getMessageReceiver, this::resolveSignalServiceAddress); - this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, + final GroupV2Helper groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, account::getSelfRecipientId, dependencies.getGroupsV2Operations(), dependencies.getGroupsV2Api(), - this::getGroupAuthForToday, this::resolveSignalServiceAddress); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); @@ -253,6 +232,13 @@ public class Manager implements Closeable { this::handleIdentityFailure, this::getGroup, this::refreshRegisteredUser); + this.groupHelper = new GroupHelper(account, + dependencies, + sendHelper, + groupV2Helper, + avatarStore, + this::resolveSignalServiceAddress, + this::resolveRecipient); } public String getUsername() { @@ -665,15 +651,6 @@ public class Manager implements Closeable { return ProfileUtils.decryptProfile(profileKey, encryptedProfile); } - private Optional createGroupAvatarAttachment(GroupId groupId) throws IOException { - final var streamDetails = avatarStore.retrieveGroupAvatar(groupId); - if (streamDetails == null) { - return Optional.absent(); - } - - return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); - } - private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { final var streamDetails = avatarStore.retrieveContactAvatar(address); if (streamDetails == null) { @@ -683,17 +660,6 @@ public class Manager implements Closeable { return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); } - private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { - var g = getGroup(groupId); - if (g == null) { - throw new GroupNotFoundException(groupId); - } - if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) { - throw new NotAGroupMemberException(groupId, g.getTitle()); - } - return g; - } - public List getGroups() { return account.getGroupStore().getGroups(); } @@ -701,53 +667,8 @@ public class Manager implements Closeable { public SendGroupMessageResults sendQuitGroupMessage( GroupId groupId, Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - var group = getGroupForUpdating(groupId); - if (group instanceof GroupInfoV1) { - return quitGroupV1((GroupInfoV1) group); - } - final var newAdmins = getRecipientIds(groupAdmins); - try { - return quitGroupV2((GroupInfoV2) group, newAdmins); - } catch (ConflictException e) { - // Detected conflicting update, refreshing group and trying again - group = getGroup(groupId, true); - return quitGroupV2((GroupInfoV2) group, newAdmins); - } - } - - private SendGroupMessageResults quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) - .withId(groupInfoV1.getGroupId().serialize()) - .build(); - - var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); - groupInfoV1.removeMember(account.getSelfRecipientId()); - account.getGroupStore().updateGroup(groupInfoV1); - return sendGroupMessage(messageBuilder, - groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - } - - private SendGroupMessageResults quitGroupV2( - final GroupInfoV2 groupInfoV2, final Set newAdmins - ) throws LastGroupAdminException, IOException { - final var currentAdmins = groupInfoV2.getAdminMembers(); - newAdmins.removeAll(currentAdmins); - newAdmins.retainAll(groupInfoV2.getMembers()); - if (currentAdmins.contains(getSelfRecipientId()) - && currentAdmins.size() == 1 - && groupInfoV2.getMembers().size() > 1 - && newAdmins.size() == 0) { - // Last admin can't leave the group, unless she's also the last member - throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle()); - } - final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); - groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); - account.getGroupStore().updateGroup(groupInfoV2); - - var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); - return sendGroupMessage(messageBuilder, - groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + return groupHelper.quitGroup(groupId, newAdmins); } public void deleteGroup(GroupId groupId) throws IOException { @@ -758,45 +679,7 @@ public class Manager implements Closeable { public Pair createGroup( String name, Set members, File avatarFile ) throws IOException, AttachmentInvalidException { - return createGroupInternal(name, members == null ? null : getRecipientIds(members), avatarFile); - } - - private Pair createGroupInternal( - String name, Set members, File avatarFile - ) throws IOException, AttachmentInvalidException { - final var selfRecipientId = account.getSelfRecipientId(); - if (members != null && members.contains(selfRecipientId)) { - members = new HashSet<>(members); - members.remove(selfRecipientId); - } - - var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name, - members == null ? Set.of() : members, - avatarFile); - - if (gv2Pair == null) { - // Failed to create v2 group, creating v1 group instead - var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); - gv1.addMembers(List.of(selfRecipientId)); - final var result = updateGroupV1(gv1, name, members, avatarFile); - return new Pair<>(gv1.getGroupId(), result); - } - - final var gv2 = gv2Pair.first(); - final var decryptedGroup = gv2Pair.second(); - - gv2.setGroup(decryptedGroup, this::resolveRecipient); - if (avatarFile != null) { - avatarStore.storeGroupAvatar(gv2.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - - account.getGroupStore().updateGroup(gv2); - - final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null); - - final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); - return new Pair<>(gv2.getGroupId(), result); + return groupHelper.createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); } public SendGroupMessageResults updateGroup( @@ -815,7 +698,7 @@ public class Manager implements Closeable { Integer expirationTimer, Boolean isAnnouncementGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - return updateGroupInternal(groupId, + return groupHelper.updateGroup(groupId, name, description, members == null ? null : getRecipientIds(members), @@ -831,267 +714,10 @@ public class Manager implements Closeable { isAnnouncementGroup); } - private SendGroupMessageResults updateGroupInternal( - final GroupId groupId, - final String name, - final String description, - final Set members, - final Set removeMembers, - final Set admins, - final Set removeAdmins, - final boolean resetGroupLink, - final GroupLinkState groupLinkState, - final GroupPermission addMemberPermission, - final GroupPermission editDetailsPermission, - final File avatarFile, - final Integer expirationTimer, - final Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - var group = getGroupForUpdating(groupId); - - if (group instanceof GroupInfoV2) { - try { - return updateGroupV2((GroupInfoV2) group, - name, - description, - members, - removeMembers, - admins, - removeAdmins, - resetGroupLink, - groupLinkState, - addMemberPermission, - editDetailsPermission, - avatarFile, - expirationTimer, - isAnnouncementGroup); - } catch (ConflictException e) { - // Detected conflicting update, refreshing group and trying again - group = getGroup(groupId, true); - return updateGroupV2((GroupInfoV2) group, - name, - description, - members, - removeMembers, - admins, - removeAdmins, - resetGroupLink, - groupLinkState, - addMemberPermission, - editDetailsPermission, - avatarFile, - expirationTimer, - isAnnouncementGroup); - } - } - - final var gv1 = (GroupInfoV1) group; - final var result = updateGroupV1(gv1, name, members, avatarFile); - if (expirationTimer != null) { - setExpirationTimer(gv1, expirationTimer); - } - return result; - } - - private SendGroupMessageResults updateGroupV1( - final GroupInfoV1 gv1, final String name, final Set members, final File avatarFile - ) throws IOException, AttachmentInvalidException { - updateGroupV1Details(gv1, name, members, avatarFile); - - account.getGroupStore().updateGroup(gv1); - - var messageBuilder = getGroupUpdateMessageBuilder(gv1); - return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - } - - private void updateGroupV1Details( - final GroupInfoV1 g, final String name, final Collection members, final File avatarFile - ) throws IOException { - if (name != null) { - g.name = name; - } - - if (members != null) { - final var newMemberAddresses = members.stream() - .filter(member -> !g.isMember(member)) - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList()); - final var newE164Members = new HashSet(); - for (var member : newMemberAddresses) { - if (!member.getNumber().isPresent()) { - continue; - } - newE164Members.add(member.getNumber().get()); - } - - final var registeredUsers = getRegisteredUsers(newE164Members); - if (registeredUsers.size() != newE164Members.size()) { - // Some of the new members are not registered on Signal - newE164Members.removeAll(registeredUsers.keySet()); - throw new IOException("Failed to add members " - + String.join(", ", newE164Members) - + " to group: Not registered on Signal"); - } - - g.addMembers(members); - } - - if (avatarFile != null) { - avatarStore.storeGroupAvatar(g.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - } - - private SendGroupMessageResults updateGroupV2( - final GroupInfoV2 group, - final String name, - final String description, - final Set members, - final Set removeMembers, - final Set admins, - final Set removeAdmins, - final boolean resetGroupLink, - final GroupLinkState groupLinkState, - final GroupPermission addMemberPermission, - final GroupPermission editDetailsPermission, - final File avatarFile, - final Integer expirationTimer, - final Boolean isAnnouncementGroup - ) throws IOException { - SendGroupMessageResults result = null; - if (group.isPendingMember(account.getSelfRecipientId())) { - var groupGroupChangePair = groupV2Helper.acceptInvite(group); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (members != null) { - final var newMembers = new HashSet<>(members); - newMembers.removeAll(group.getMembers()); - if (newMembers.size() > 0) { - var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - } - - if (removeMembers != null) { - var existingRemoveMembers = new HashSet<>(removeMembers); - existingRemoveMembers.retainAll(group.getMembers()); - existingRemoveMembers.remove(getSelfRecipientId());// self can be removed with sendQuitGroupMessage - if (existingRemoveMembers.size() > 0) { - var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - var pendingRemoveMembers = new HashSet<>(removeMembers); - pendingRemoveMembers.retainAll(group.getPendingMembers()); - if (pendingRemoveMembers.size() > 0) { - var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - } - - if (admins != null) { - final var newAdmins = new HashSet<>(admins); - newAdmins.retainAll(group.getMembers()); - newAdmins.removeAll(group.getAdminMembers()); - if (newAdmins.size() > 0) { - for (var admin : newAdmins) { - var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true); - result = sendUpdateGroupV2Message(group, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - } - } - - if (removeAdmins != null) { - final var existingRemoveAdmins = new HashSet<>(removeAdmins); - existingRemoveAdmins.retainAll(group.getAdminMembers()); - if (existingRemoveAdmins.size() > 0) { - for (var admin : existingRemoveAdmins) { - var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false); - result = sendUpdateGroupV2Message(group, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - } - } - - if (resetGroupLink) { - var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (groupLinkState != null) { - var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (addMemberPermission != null) { - var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (editDetailsPermission != null) { - var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (expirationTimer != null) { - var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (isAnnouncementGroup != null) { - var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (name != null || description != null || avatarFile != null) { - var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); - if (avatarFile != null) { - avatarStore.storeGroupAvatar(group.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - return result; - } - public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { - final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), - inviteLinkUrl.getPassword()); - final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(), - inviteLinkUrl.getPassword(), - groupJoinInfo); - final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), - groupJoinInfo.getRevision() + 1, - groupChange.toByteArray()); - - if (group.getGroup() == null) { - // Only requested member, can't send update to group members - return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of())); - } - - final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); - - return new Pair<>(group.getGroupId(), result); - } - - private SendGroupMessageResults sendUpdateGroupV2Message( - GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange - ) throws IOException { - final var selfRecipientId = account.getSelfRecipientId(); - final var members = group.getMembersIncludingPendingWithout(selfRecipientId); - group.setGroup(newDecryptedGroup, this::resolveRecipient); - members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId)); - account.getGroupStore().updateGroup(group); - - final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); - return sendGroupMessage(messageBuilder, members); + return groupHelper.joinGroup(inviteLinkUrl); } public SendMessageResults sendMessage( @@ -1134,100 +760,18 @@ public class Manager implements Closeable { } } - private SendGroupMessageResults sendGroupMessage( - final SignalServiceDataMessage.Builder messageBuilder, final Set members - ) throws IOException { - final var timestamp = System.currentTimeMillis(); - messageBuilder.withTimestamp(timestamp); - final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members); - return new SendGroupMessageResults(timestamp, results); - } - - private static int currentTimeDays() { - return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()); - } - - private GroupsV2AuthorizationString getGroupAuthForToday( - final GroupSecretParams groupSecretParams - ) throws IOException { - final var today = currentTimeDays(); - // Returns credentials for the next 7 days - final var credentials = dependencies.getGroupsV2Api().getCredentials(today); - // TODO cache credentials until they expire - var authCredentialResponse = credentials.get(today); - try { - return dependencies.getGroupsV2Api() - .getGroupsV2AuthorizationString(account.getUuid(), - today, - groupSecretParams, - authCredentialResponse); - } catch (VerificationFailedException e) { - throw new IOException(e); - } - } - SendGroupMessageResults sendGroupInfoMessage( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { - GroupInfoV1 g; - var group = getGroupForUpdating(groupId); - if (!(group instanceof GroupInfoV1)) { - throw new IOException("Received an invalid group request for a v2 group!"); - } - g = (GroupInfoV1) group; - final var recipientId = resolveRecipient(recipient); - if (!g.isMember(recipientId)) { - throw new NotAGroupMemberException(groupId, g.name); - } - - var messageBuilder = getGroupUpdateMessageBuilder(g); - - // Send group message only to the recipient who requested it - return sendGroupMessage(messageBuilder, Set.of(recipientId)); - } - - private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) - .withId(g.getGroupId().serialize()) - .withName(g.name) - .withMembers(g.getMembers() - .stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList())); - - try { - final var attachment = createGroupAvatarAttachment(g.getGroupId()); - if (attachment.isPresent()) { - group.withAvatar(attachment.get()); - } - } catch (IOException e) { - throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); - } - - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); - } - - private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { - var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) - .withRevision(g.getGroup().getRevision()) - .withSignedGroupChange(signedGroupChange); - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + return groupHelper.sendGroupInfoMessage(groupId, recipientId); } SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); - - var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); - - // Send group info request message to the recipient who sent us a message with this groupId - return sendGroupMessage(messageBuilder, Set.of(resolveRecipient(recipient))); + final var recipientId = resolveRecipient(recipient); + return groupHelper.sendGroupInfoRequest(groupId, recipientId); } public void sendReadReceipt( @@ -1361,6 +905,7 @@ public class Manager implements Closeable { private void setContactBlocked(RecipientId recipientId, boolean blocked) { var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + // TODO cycle our profile key account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); } @@ -1371,19 +916,10 @@ public class Manager implements Closeable { } group.setBlocked(blocked); + // TODO cycle our profile key account.getGroupStore().updateGroup(group); } - private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { - var contact = account.getContactStore().getContact(recipientId); - if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { - return; - } - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - account.getContactStore() - .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); - } - /** * Change the expiration timer for a contact */ @@ -1400,20 +936,14 @@ public class Manager implements Closeable { } } - /** - * Change the expiration timer for a group - */ - private void setExpirationTimer( - GroupInfoV1 groupInfoV1, int messageExpirationTimer - ) throws NotAGroupMemberException, GroupNotFoundException, IOException { - groupInfoV1.messageExpirationTime = messageExpirationTimer; - account.getGroupStore().updateGroup(groupInfoV1); - sendExpirationTimerUpdate(groupInfoV1.getGroupId()); - } - - private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - sendHelper.sendAsGroupMessage(messageBuilder, groupId); + private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { + var contact = account.getContactStore().getContact(recipientId); + if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { + return; + } + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore() + .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); } /** @@ -1583,10 +1113,6 @@ public class Manager implements Closeable { sendTypingMessage(action.toSignalService(), recipients); } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException { - return dependencies.getCipher().decrypt(envelope); - } - private void handleEndSession(RecipientId recipientId) { account.getSessionStore().deleteAllSessions(recipientId); } @@ -1614,7 +1140,7 @@ public class Manager implements Closeable { if (groupInfo.getAvatar().isPresent()) { var avatar = groupInfo.getAvatar().get(); - downloadGroupAvatar(avatar, groupV1.getGroupId()); + downloadGroupAvatar(groupV1.getGroupId(), avatar); } if (groupInfo.getName().isPresent()) { @@ -1658,7 +1184,7 @@ public class Manager implements Closeable { final var groupContext = message.getGroupContext().get().getGroupV2().get(); final var groupMasterKey = groupContext.getMasterKey(); - getOrMigrateGroup(groupMasterKey, + groupHelper.getOrMigrateGroup(groupMasterKey, groupContext.getRevision(), groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); } @@ -1743,65 +1269,6 @@ public class Manager implements Closeable { return actions; } - private GroupInfoV2 getOrMigrateGroup( - final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange - ) { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - - var groupId = GroupUtils.getGroupIdV2(groupSecretParams); - var groupInfo = getGroup(groupId); - final GroupInfoV2 groupInfoV2; - if (groupInfo instanceof GroupInfoV1) { - // Received a v2 group message for a v1 group, we need to locally migrate the group - account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId()); - groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); - logger.info("Locally migrated group {} to group v2, id: {}", - groupInfo.getGroupId().toBase64(), - groupInfoV2.getGroupId().toBase64()); - } else if (groupInfo instanceof GroupInfoV2) { - groupInfoV2 = (GroupInfoV2) groupInfo; - } else { - groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); - } - - if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) { - DecryptedGroup group = null; - if (signedGroupChange != null - && groupInfoV2.getGroup() != null - && groupInfoV2.getGroup().getRevision() + 1 == revision) { - group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), - signedGroupChange, - groupMasterKey); - } - if (group == null) { - group = groupV2Helper.getDecryptedGroup(groupSecretParams); - } - if (group != null) { - storeProfileKeysFromMembers(group); - final var avatar = group.getAvatar(); - if (avatar != null && !avatar.isEmpty()) { - downloadGroupAvatar(groupId, groupSecretParams, avatar); - } - } - groupInfoV2.setGroup(group, this::resolveRecipient); - account.getGroupStore().updateGroup(groupInfoV2); - } - - return groupInfoV2; - } - - private void storeProfileKeysFromMembers(final DecryptedGroup group) { - for (var member : group.getMembersList()) { - final var uuid = UuidUtil.parseOrThrow(member.getUuid().toByteArray()); - final var recipientId = account.getRecipientStore().resolveRecipient(uuid); - try { - account.getProfileStore() - .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray())); - } catch (InvalidInputException ignored) { - } - } - } - private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { Set queuedActions = new HashSet<>(); for (var cachedMessage : account.getMessageCache().getCachedMessages()) { @@ -1824,7 +1291,7 @@ public class Manager implements Closeable { List actions = null; if (!envelope.isReceipt()) { try { - content = decryptMessage(envelope); + content = dependencies.getCipher().decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { if (!envelope.hasSource()) { final var identifier = e.getSender(); @@ -1915,7 +1382,7 @@ public class Manager implements Closeable { } if (!envelope.isReceipt()) { try { - content = decryptMessage(envelope); + content = dependencies.getCipher().decrypt(envelope); } catch (Exception e) { exception = e; } @@ -2157,7 +1624,7 @@ public class Manager implements Closeable { } if (g.getAvatar().isPresent()) { - downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId()); + downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); } syncGroup.archived = g.isArchived(); account.getGroupStore().updateGroup(syncGroup); @@ -2333,7 +1800,7 @@ public class Manager implements Closeable { } } - private void downloadGroupAvatar(SignalServiceAttachment avatar, GroupId groupId) { + private void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { try { avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream)); } catch (IOException e) { @@ -2341,15 +1808,6 @@ public class Manager implements Closeable { } } - private void downloadGroupAvatar(GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey) { - try { - avatarStore.storeGroupAvatar(groupId, - outputStream -> retrieveGroupV2Avatar(groupSecretParams, cdnKey, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); - } - } - private void downloadProfileAvatar( SignalServiceAddress address, String avatarPath, ProfileKey profileKey ) { @@ -2392,29 +1850,6 @@ public class Manager implements Closeable { } } - private void retrieveGroupV2Avatar( - GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream - ) throws IOException { - var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); - - var tmpFile = IOUtils.createTempFile(); - try (InputStream input = dependencies.getMessageReceiver() - .retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { - var encryptedData = IOUtils.readFully(input); - - var decryptedData = groupOperations.decryptAvatar(encryptedData); - outputStream.write(decryptedData); - } finally { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } - } - private void retrieveProfileAvatar( String avatarPath, ProfileKey profileKey, OutputStream outputStream ) throws IOException { @@ -2490,7 +1925,7 @@ public class Manager implements Closeable { .stream() .map(this::resolveSignalServiceAddress) .collect(Collectors.toList()), - createGroupAvatarAttachment(groupInfo.getGroupId()), + groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()), groupInfo.isMember(account.getSelfRecipientId()), Optional.of(groupInfo.messageExpirationTime), Optional.fromNullable(groupInfo.color), @@ -2639,17 +2074,7 @@ public class Manager implements Closeable { } public GroupInfo getGroup(GroupId groupId) { - return getGroup(groupId, false); - } - - public GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { - final var group = account.getGroupStore().getGroup(groupId); - if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); - ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient); - account.getGroupStore().updateGroup(group); - } - return group; + return groupHelper.getGroup(groupId); } public List getIdentities() { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java new file mode 100644 index 00000000..0b9cc950 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -0,0 +1,628 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.AvatarStore; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.api.SendGroupMessageResults; +import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupLinkState; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.groups.LastGroupAdminException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.asamk.signal.manager.util.AttachmentUtils; +import org.asamk.signal.manager.util.IOUtils; +import org.signal.storageservice.protos.groups.GroupChange; +import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.signal.zkgroup.groups.GroupSecretParams; +import org.signal.zkgroup.profiles.ProfileKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; +import org.whispersystems.signalservice.api.push.exceptions.ConflictException; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class GroupHelper { + + private final static Logger logger = LoggerFactory.getLogger(GroupHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final SendHelper sendHelper; + private final GroupV2Helper groupV2Helper; + private final AvatarStore avatarStore; + private final SignalServiceAddressResolver addressResolver; + private final RecipientResolver recipientResolver; + + public GroupHelper( + final SignalAccount account, + final SignalDependencies dependencies, + final SendHelper sendHelper, + final GroupV2Helper groupV2Helper, + final AvatarStore avatarStore, + final SignalServiceAddressResolver addressResolver, + final RecipientResolver recipientResolver + ) { + this.account = account; + this.dependencies = dependencies; + this.sendHelper = sendHelper; + this.groupV2Helper = groupV2Helper; + this.avatarStore = avatarStore; + this.addressResolver = addressResolver; + this.recipientResolver = recipientResolver; + } + + public GroupInfo getGroup(GroupId groupId) { + return getGroup(groupId, false); + } + + public Optional createGroupAvatarAttachment(GroupIdV1 groupId) throws IOException { + final var streamDetails = avatarStore.retrieveGroupAvatar(groupId); + if (streamDetails == null) { + return Optional.absent(); + } + + return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); + } + + public GroupInfoV2 getOrMigrateGroup( + final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange + ) { + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + + var groupId = GroupUtils.getGroupIdV2(groupSecretParams); + var groupInfo = getGroup(groupId); + final GroupInfoV2 groupInfoV2; + if (groupInfo instanceof GroupInfoV1) { + // Received a v2 group message for a v1 group, we need to locally migrate the group + account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId()); + groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); + logger.info("Locally migrated group {} to group v2, id: {}", + groupInfo.getGroupId().toBase64(), + groupInfoV2.getGroupId().toBase64()); + } else if (groupInfo instanceof GroupInfoV2) { + groupInfoV2 = (GroupInfoV2) groupInfo; + } else { + groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); + } + + if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) { + DecryptedGroup group = null; + if (signedGroupChange != null + && groupInfoV2.getGroup() != null + && groupInfoV2.getGroup().getRevision() + 1 == revision) { + group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), + signedGroupChange, + groupMasterKey); + } + if (group == null) { + group = groupV2Helper.getDecryptedGroup(groupSecretParams); + } + if (group != null) { + storeProfileKeysFromMembers(group); + final var avatar = group.getAvatar(); + if (avatar != null && !avatar.isEmpty()) { + downloadGroupAvatar(groupId, groupSecretParams, avatar); + } + } + groupInfoV2.setGroup(group, recipientResolver); + account.getGroupStore().updateGroup(groupInfoV2); + } + + return groupInfoV2; + } + + public Pair createGroup( + String name, Set members, File avatarFile + ) throws IOException, AttachmentInvalidException { + final var selfRecipientId = account.getSelfRecipientId(); + if (members != null && members.contains(selfRecipientId)) { + members = new HashSet<>(members); + members.remove(selfRecipientId); + } + + var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name, + members == null ? Set.of() : members, + avatarFile); + + if (gv2Pair == null) { + // Failed to create v2 group, creating v1 group instead + var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); + gv1.addMembers(List.of(selfRecipientId)); + final var result = updateGroupV1(gv1, name, members, avatarFile); + return new Pair<>(gv1.getGroupId(), result); + } + + final var gv2 = gv2Pair.first(); + final var decryptedGroup = gv2Pair.second(); + + gv2.setGroup(decryptedGroup, recipientResolver); + if (avatarFile != null) { + avatarStore.storeGroupAvatar(gv2.getGroupId(), + outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); + } + + account.getGroupStore().updateGroup(gv2); + + final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null); + + final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); + return new Pair<>(gv2.getGroupId(), result); + } + + public SendGroupMessageResults updateGroup( + final GroupId groupId, + final String name, + final String description, + final Set members, + final Set removeMembers, + final Set admins, + final Set removeAdmins, + final boolean resetGroupLink, + final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, + final File avatarFile, + final Integer expirationTimer, + final Boolean isAnnouncementGroup + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { + var group = getGroupForUpdating(groupId); + + if (group instanceof GroupInfoV2) { + try { + return updateGroupV2((GroupInfoV2) group, + name, + description, + members, + removeMembers, + admins, + removeAdmins, + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer, + isAnnouncementGroup); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + group = getGroup(groupId, true); + return updateGroupV2((GroupInfoV2) group, + name, + description, + members, + removeMembers, + admins, + removeAdmins, + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer, + isAnnouncementGroup); + } + } + + final var gv1 = (GroupInfoV1) group; + final var result = updateGroupV1(gv1, name, members, avatarFile); + if (expirationTimer != null) { + setExpirationTimer(gv1, expirationTimer); + } + return result; + } + + public Pair joinGroup( + GroupInviteLinkUrl inviteLinkUrl + ) throws IOException, GroupLinkNotActiveException { + final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), + inviteLinkUrl.getPassword()); + final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(), + inviteLinkUrl.getPassword(), + groupJoinInfo); + final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), + groupJoinInfo.getRevision() + 1, + groupChange.toByteArray()); + + if (group.getGroup() == null) { + // Only requested member, can't send update to group members + return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of())); + } + + final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); + + return new Pair<>(group.getGroupId(), result); + } + + public SendGroupMessageResults quitGroup( + final GroupId groupId, final Set newAdmins + ) throws IOException, LastGroupAdminException, NotAGroupMemberException, GroupNotFoundException { + var group = getGroupForUpdating(groupId); + if (group instanceof GroupInfoV1) { + return quitGroupV1((GroupInfoV1) group); + } + + try { + return quitGroupV2((GroupInfoV2) group, newAdmins); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + group = getGroup(groupId, true); + return quitGroupV2((GroupInfoV2) group, newAdmins); + } + } + + public SendGroupMessageResults sendGroupInfoRequest( + GroupIdV1 groupId, RecipientId recipientId + ) throws IOException { + var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); + + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); + + // Send group info request message to the recipient who sent us a message with this groupId + return sendGroupMessage(messageBuilder, Set.of(recipientId)); + } + + public SendGroupMessageResults sendGroupInfoMessage( + GroupIdV1 groupId, RecipientId recipientId + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { + GroupInfoV1 g; + var group = getGroupForUpdating(groupId); + if (!(group instanceof GroupInfoV1)) { + throw new IOException("Received an invalid group request for a v2 group!"); + } + g = (GroupInfoV1) group; + + if (!g.isMember(recipientId)) { + throw new NotAGroupMemberException(groupId, g.name); + } + + var messageBuilder = getGroupUpdateMessageBuilder(g); + + // Send group message only to the recipient who requested it + return sendGroupMessage(messageBuilder, Set.of(recipientId)); + } + + private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { + final var group = account.getGroupStore().getGroup(groupId); + if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) { + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); + ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), recipientResolver); + account.getGroupStore().updateGroup(group); + } + return group; + } + + private void downloadGroupAvatar(GroupIdV2 groupId, GroupSecretParams groupSecretParams, String cdnKey) { + try { + avatarStore.storeGroupAvatar(groupId, + outputStream -> retrieveGroupV2Avatar(groupSecretParams, cdnKey, outputStream)); + } catch (IOException e) { + logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); + } + } + + private void retrieveGroupV2Avatar( + GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream + ) throws IOException { + var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); + + var tmpFile = IOUtils.createTempFile(); + try (InputStream input = dependencies.getMessageReceiver() + .retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { + var encryptedData = IOUtils.readFully(input); + + var decryptedData = groupOperations.decryptAvatar(encryptedData); + outputStream.write(decryptedData); + } finally { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); + } + } + } + + private void storeProfileKeysFromMembers(final DecryptedGroup group) { + for (var member : group.getMembersList()) { + final var uuid = UuidUtil.parseOrThrow(member.getUuid().toByteArray()); + final var recipientId = account.getRecipientStore().resolveRecipient(uuid); + try { + account.getProfileStore() + .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray())); + } catch (InvalidInputException ignored) { + } + } + } + + private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { + var g = getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } + if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) { + throw new NotAGroupMemberException(groupId, g.getTitle()); + } + return g; + } + + private SendGroupMessageResults updateGroupV1( + final GroupInfoV1 gv1, final String name, final Set members, final File avatarFile + ) throws IOException, AttachmentInvalidException { + updateGroupV1Details(gv1, name, members, avatarFile); + + account.getGroupStore().updateGroup(gv1); + + var messageBuilder = getGroupUpdateMessageBuilder(gv1); + return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + } + + private void updateGroupV1Details( + final GroupInfoV1 g, final String name, final Collection members, final File avatarFile + ) throws IOException { + if (name != null) { + g.name = name; + } + + if (members != null) { + g.addMembers(members); + } + + if (avatarFile != null) { + avatarStore.storeGroupAvatar(g.getGroupId(), + outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); + } + } + + /** + * Change the expiration timer for a group + */ + private void setExpirationTimer( + GroupInfoV1 groupInfoV1, int messageExpirationTimer + ) throws NotAGroupMemberException, GroupNotFoundException, IOException { + groupInfoV1.messageExpirationTime = messageExpirationTimer; + account.getGroupStore().updateGroup(groupInfoV1); + sendExpirationTimerUpdate(groupInfoV1.getGroupId()); + } + + private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + sendHelper.sendAsGroupMessage(messageBuilder, groupId); + } + + private SendGroupMessageResults updateGroupV2( + final GroupInfoV2 group, + final String name, + final String description, + final Set members, + final Set removeMembers, + final Set admins, + final Set removeAdmins, + final boolean resetGroupLink, + final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, + final File avatarFile, + final Integer expirationTimer, + final Boolean isAnnouncementGroup + ) throws IOException { + SendGroupMessageResults result = null; + if (group.isPendingMember(account.getSelfRecipientId())) { + var groupGroupChangePair = groupV2Helper.acceptInvite(group); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (members != null) { + final var newMembers = new HashSet<>(members); + newMembers.removeAll(group.getMembers()); + if (newMembers.size() > 0) { + var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + } + + if (removeMembers != null) { + var existingRemoveMembers = new HashSet<>(removeMembers); + existingRemoveMembers.retainAll(group.getMembers()); + existingRemoveMembers.remove(account.getSelfRecipientId());// self can be removed with sendQuitGroupMessage + if (existingRemoveMembers.size() > 0) { + var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + var pendingRemoveMembers = new HashSet<>(removeMembers); + pendingRemoveMembers.retainAll(group.getPendingMembers()); + if (pendingRemoveMembers.size() > 0) { + var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + } + + if (admins != null) { + final var newAdmins = new HashSet<>(admins); + newAdmins.retainAll(group.getMembers()); + newAdmins.removeAll(group.getAdminMembers()); + if (newAdmins.size() > 0) { + for (var admin : newAdmins) { + var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true); + result = sendUpdateGroupV2Message(group, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + } + } + + if (removeAdmins != null) { + final var existingRemoveAdmins = new HashSet<>(removeAdmins); + existingRemoveAdmins.retainAll(group.getAdminMembers()); + if (existingRemoveAdmins.size() > 0) { + for (var admin : existingRemoveAdmins) { + var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false); + result = sendUpdateGroupV2Message(group, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + } + } + + if (resetGroupLink) { + var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (groupLinkState != null) { + var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (addMemberPermission != null) { + var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (editDetailsPermission != null) { + var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (expirationTimer != null) { + var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (isAnnouncementGroup != null) { + var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (name != null || description != null || avatarFile != null) { + var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); + if (avatarFile != null) { + avatarStore.storeGroupAvatar(group.getGroupId(), + outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); + } + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + return result; + } + + private SendGroupMessageResults quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { + var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) + .withId(groupInfoV1.getGroupId().serialize()) + .build(); + + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); + groupInfoV1.removeMember(account.getSelfRecipientId()); + account.getGroupStore().updateGroup(groupInfoV1); + return sendGroupMessage(messageBuilder, + groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + } + + private SendGroupMessageResults quitGroupV2( + final GroupInfoV2 groupInfoV2, final Set newAdmins + ) throws LastGroupAdminException, IOException { + final var currentAdmins = groupInfoV2.getAdminMembers(); + newAdmins.removeAll(currentAdmins); + newAdmins.retainAll(groupInfoV2.getMembers()); + if (currentAdmins.contains(account.getSelfRecipientId()) + && currentAdmins.size() == 1 + && groupInfoV2.getMembers().size() > 1 + && newAdmins.size() == 0) { + // Last admin can't leave the group, unless she's also the last member + throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle()); + } + final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); + groupInfoV2.setGroup(groupGroupChangePair.first(), recipientResolver); + account.getGroupStore().updateGroup(groupInfoV2); + + var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); + return sendGroupMessage(messageBuilder, + groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + } + + private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { + var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) + .withId(g.getGroupId().serialize()) + .withName(g.name) + .withMembers(g.getMembers() + .stream() + .map(addressResolver::resolveSignalServiceAddress) + .collect(Collectors.toList())); + + try { + final var attachment = createGroupAvatarAttachment(g.getGroupId()); + if (attachment.isPresent()) { + group.withAvatar(attachment.get()); + } + } catch (IOException e) { + throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); + } + + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.getMessageExpirationTime()); + } + + private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { + var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) + .withRevision(g.getGroup().getRevision()) + .withSignedGroupChange(signedGroupChange); + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.getMessageExpirationTime()); + } + + private SendGroupMessageResults sendUpdateGroupV2Message( + GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange + ) throws IOException { + final var selfRecipientId = account.getSelfRecipientId(); + final var members = group.getMembersIncludingPendingWithout(selfRecipientId); + group.setGroup(newDecryptedGroup, recipientResolver); + members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId)); + account.getGroupStore().updateGroup(group); + + final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); + return sendGroupMessage(messageBuilder, members); + } + + private SendGroupMessageResults sendGroupMessage( + final SignalServiceDataMessage.Builder messageBuilder, final Set members + ) throws IOException { + final var timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); + final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members); + return new SendGroupMessageResults(timestamp, results); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index e161673e..19240cef 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -43,6 +43,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class GroupV2Helper { @@ -59,8 +60,6 @@ public class GroupV2Helper { private final GroupsV2Api groupsV2Api; - private final GroupAuthorizationProvider groupAuthorizationProvider; - private final SignalServiceAddressResolver addressResolver; public GroupV2Helper( @@ -69,7 +68,6 @@ public class GroupV2Helper { final SelfRecipientIdProvider selfRecipientIdProvider, final GroupsV2Operations groupsV2Operations, final GroupsV2Api groupsV2Api, - final GroupAuthorizationProvider groupAuthorizationProvider, final SignalServiceAddressResolver addressResolver ) { this.profileKeyCredentialProvider = profileKeyCredentialProvider; @@ -77,14 +75,12 @@ public class GroupV2Helper { this.selfRecipientIdProvider = selfRecipientIdProvider; this.groupsV2Operations = groupsV2Operations; this.groupsV2Api = groupsV2Api; - this.groupAuthorizationProvider = groupAuthorizationProvider; this.addressResolver = addressResolver; } public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) { try { - final var groupsV2AuthorizationString = groupAuthorizationProvider.getAuthorizationForToday( - groupSecretParams); + final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString); } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); @@ -99,7 +95,7 @@ public class GroupV2Helper { return groupsV2Api.getGroupJoinInfo(groupSecretParams, Optional.fromNullable(password).transform(GroupLinkPassword::serialize), - groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams)); + getGroupAuthForToday(groupSecretParams)); } public Pair createGroup( @@ -116,7 +112,7 @@ public class GroupV2Helper { final GroupsV2AuthorizationString groupAuthForToday; final DecryptedGroup decryptedGroup; try { - groupAuthForToday = groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams); + groupAuthForToday = getGroupAuthForToday(groupSecretParams); groupsV2Api.putNewGroup(newGroup, groupAuthForToday); decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday); } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { @@ -214,7 +210,7 @@ public class GroupV2Helper { final var avatarBytes = readAvatarBytes(avatarFile); var avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes, groupSecretParams, - groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams)); + getGroupAuthForToday(groupSecretParams)); change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey)); } @@ -487,7 +483,7 @@ public class GroupV2Helper { } var signedGroupChange = groupsV2Api.patchGroup(changeActions, - groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams), + getGroupAuthForToday(groupSecretParams), Optional.absent()); return new Pair<>(decryptedGroupState, signedGroupChange); @@ -503,7 +499,7 @@ public class GroupV2Helper { final var changeActions = change.setRevision(nextRevision).build(); return groupsV2Api.patchGroup(changeActions, - groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams), + getGroupAuthForToday(groupSecretParams), Optional.fromNullable(password).transform(GroupLinkPassword::serialize)); } @@ -534,4 +530,26 @@ public class GroupV2Helper { return null; } + + private static int currentTimeDays() { + return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()); + } + + private GroupsV2AuthorizationString getGroupAuthForToday( + final GroupSecretParams groupSecretParams + ) throws IOException { + final var today = currentTimeDays(); + // Returns credentials for the next 7 days + final var credentials = groupsV2Api.getCredentials(today); + // TODO cache credentials until they expire + var authCredentialResponse = credentials.get(today); + final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId()) + .getUuid() + .get(); + try { + return groupsV2Api.getGroupsV2AuthorizationString(uuid, today, groupSecretParams, authCredentialResponse); + } catch (VerificationFailedException e) { + throw new IOException(e); + } + } } -- 2.51.0 From 7f64a9812ca5bb10e8f57cacf3d22b904bd200b4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 09:34:06 +0200 Subject: [PATCH 06/16] Prevent non-admins from sending to announcement groups Only reactions are allowed --- .../org/asamk/signal/manager/Manager.java | 36 ++++++++++++------- .../GroupSendingNotAllowedException.java | 8 +++++ .../signal/manager/helper/GroupHelper.java | 7 ++-- .../signal/manager/helper/SendHelper.java | 25 ++++++++++--- .../signal/commands/RemoteDeleteCommand.java | 3 +- .../asamk/signal/commands/SendCommand.java | 3 +- .../signal/commands/SendReactionCommand.java | 3 +- .../signal/commands/SendTypingCommand.java | 3 +- .../signal/commands/UpdateGroupCommand.java | 3 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 17 ++++----- 10 files changed, 76 insertions(+), 32 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/groups/GroupSendingNotAllowedException.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 3857d803..13696235 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -31,6 +31,7 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -697,7 +698,7 @@ public class Manager implements Closeable { File avatarFile, Integer expirationTimer, Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { return groupHelper.updateGroup(groupId, name, description, @@ -722,7 +723,7 @@ public class Manager implements Closeable { public SendMessageResults sendMessage( SignalServiceDataMessage.Builder messageBuilder, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var results = new HashMap>(); long timestamp = System.currentTimeMillis(); messageBuilder.withTimestamp(timestamp); @@ -745,7 +746,7 @@ public class Manager implements Closeable { public void sendTypingMessage( SignalServiceTypingMessage.Action action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var timestamp = System.currentTimeMillis(); for (var recipient : recipients) { if (recipient instanceof RecipientIdentifier.Single) { @@ -806,7 +807,7 @@ public class Manager implements Closeable { public SendMessageResults sendMessage( Message message, Set recipients - ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); return sendMessage(messageBuilder, recipients); @@ -836,7 +837,7 @@ public class Manager implements Closeable { public SendMessageResults sendRemoteDeleteMessage( long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); return sendMessage(messageBuilder, recipients); @@ -848,7 +849,7 @@ public class Manager implements Closeable { RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var targetAuthorRecipientId = resolveRecipient(targetAuthor); var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, @@ -864,7 +865,7 @@ public class Manager implements Closeable { try { return sendMessage(messageBuilder, recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } finally { for (var recipient : recipients) { @@ -931,7 +932,7 @@ public class Manager implements Closeable { final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); try { sendMessage(messageBuilder, Set.of(recipient)); - } catch (NotAGroupMemberException | GroupNotFoundException e) { + } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } } @@ -1109,7 +1110,7 @@ public class Manager implements Closeable { public void sendTypingMessage( TypingAction action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { sendTypingMessage(action.toSignalService(), recipients); } @@ -1530,9 +1531,20 @@ public class Manager implements Closeable { } final var recipientId = resolveRecipient(source); - return !group.isMember(recipientId) || ( - group.isAnnouncementGroup() && !group.isAdmin(recipientId) - ); + if (!group.isMember(recipientId)) { + return true; + } + + if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { + return message.getBody().isPresent() + || message.getAttachments().isPresent() + || message.getQuote() + .isPresent() + || message.getPreviews().isPresent() + || message.getMentions().isPresent() + || message.getSticker().isPresent(); + } + return false; } private List handleMessage( diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/GroupSendingNotAllowedException.java b/lib/src/main/java/org/asamk/signal/manager/groups/GroupSendingNotAllowedException.java new file mode 100644 index 00000000..1a2fa432 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/groups/GroupSendingNotAllowedException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.groups; + +public class GroupSendingNotAllowedException extends Exception { + + public GroupSendingNotAllowedException(GroupId groupId, String groupName) { + super("User is not allowed to send message to group: " + groupName + " (" + groupId.toBase64() + ")"); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 0b9cc950..9ff3134e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -12,6 +12,7 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -195,7 +196,7 @@ public class GroupHelper { final File avatarFile, final Integer expirationTimer, final Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { var group = getGroupForUpdating(groupId); if (group instanceof GroupInfoV2) { @@ -410,13 +411,13 @@ public class GroupHelper { */ private void setExpirationTimer( GroupInfoV1 groupInfoV1, int messageExpirationTimer - ) throws NotAGroupMemberException, GroupNotFoundException, IOException { + ) throws NotAGroupMemberException, GroupNotFoundException, IOException, GroupSendingNotAllowedException { groupInfoV1.messageExpirationTime = messageExpirationTimer; account.getGroupStore().updateGroup(groupInfoV1); sendExpirationTimerUpdate(groupInfoV1.getGroupId()); } - private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { + private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); sendHelper.sendAsGroupMessage(messageBuilder, groupId); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index b04ff40d..f92d7bde 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.SignalAccount; @@ -86,19 +87,32 @@ public class SendHelper { */ public List sendAsGroupMessage( SignalServiceDataMessage.Builder messageBuilder, GroupId groupId - ) throws IOException, GroupNotFoundException, NotAGroupMemberException { + ) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException { final var g = getGroupForSending(groupId); return sendAsGroupMessage(messageBuilder, g); } private List sendAsGroupMessage( final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g - ) throws IOException { + ) throws IOException, GroupSendingNotAllowedException { GroupUtils.setGroupContext(messageBuilder, g); messageBuilder.withExpiration(g.getMessageExpirationTime()); + final var message = messageBuilder.build(); final var recipients = g.getMembersWithout(account.getSelfRecipientId()); - return sendGroupMessage(messageBuilder.build(), recipients); + + if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) { + if (message.getBody().isPresent() + || message.getAttachments().isPresent() + || message.getQuote().isPresent() + || message.getPreviews().isPresent() + || message.getMentions().isPresent() + || message.getSticker().isPresent()) { + throw new GroupSendingNotAllowedException(g.getGroupId(), g.getTitle()); + } + } + + return sendGroupMessage(message, recipients); } /** @@ -181,8 +195,11 @@ public class SendHelper { public void sendGroupTypingMessage( SignalServiceTypingMessage message, GroupId groupId - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var g = getGroupForSending(groupId); + if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) { + throw new GroupSendingNotAllowedException(groupId, g.getTitle()); + } final var messageSender = dependencies.getMessageSender(); final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId())); final var addresses = recipientIdList.stream() diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index f033e0b1..e482dd58 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -13,6 +13,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; @@ -60,7 +61,7 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { final var results = m.sendRemoteDeleteMessage(targetTimestamp, recipientIdentifiers); outputResult(outputWriter, results.getTimestamp()); ErrorUtils.handleSendMessageResults(results.getResults()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + 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 a1e4c296..c29d0268 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -17,6 +17,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; @@ -108,7 +109,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { ErrorUtils.handleSendMessageResults(results.getResults()); } catch (AttachmentInvalidException | IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } } diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index c8e339a1..98e5f5ec 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -13,6 +13,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; @@ -76,7 +77,7 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { recipientIdentifiers); outputResult(outputWriter, results.getTimestamp()); ErrorUtils.handleSendMessageResults(results.getResults()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index 14139885..ace4da85 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -11,6 +11,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; @@ -59,7 +60,7 @@ public class SendTypingCommand implements JsonRpcLocalCommand { m.sendTypingMessage(action, recipientIdentifiers); } catch (IOException | UntrustedIdentityException e) { throw new UserErrorException("Failed to send message: " + e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index fc2cfbc0..a8d556f3 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -17,6 +17,7 @@ import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; @@ -170,7 +171,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { outputResult(outputWriter, timestamp, isNewGroup ? groupId : null); } catch (AttachmentInvalidException e) { throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index c5be7501..315e1d8e 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -10,6 +10,7 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.identities.IdentityInfo; @@ -77,7 +78,7 @@ public class DbusSignalImpl implements Signal { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -104,7 +105,7 @@ public class DbusSignalImpl implements Signal { return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -120,7 +121,7 @@ public class DbusSignalImpl implements Signal { return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -158,7 +159,7 @@ public class DbusSignalImpl implements Signal { return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -176,7 +177,7 @@ public class DbusSignalImpl implements Signal { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -200,7 +201,7 @@ public class DbusSignalImpl implements Signal { return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); @@ -225,7 +226,7 @@ public class DbusSignalImpl implements Signal { return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -337,7 +338,7 @@ public class DbusSignalImpl implements Signal { } } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); -- 2.51.0 From cd3741d236c7cb64052ba0468a48d480769067e7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 10:28:04 +0200 Subject: [PATCH 07/16] Rename internal quitGroup method --- lib/src/main/java/org/asamk/signal/manager/Manager.java | 2 +- src/main/java/org/asamk/signal/commands/QuitGroupCommand.java | 2 +- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 13696235..5de65f3e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -665,7 +665,7 @@ public class Manager implements Closeable { return account.getGroupStore().getGroups(); } - public SendGroupMessageResults sendQuitGroupMessage( + public SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { final var newAdmins = getRecipientIds(groupAdmins); diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index c39f298d..03bf232b 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -54,7 +54,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { try { try { - final var results = m.sendQuitGroupMessage(groupId, groupAdmins); + final var results = m.quitGroup(groupId, groupAdmins); final var timestamp = results.getTimestamp(); outputResult(outputWriter, timestamp); handleSendMessageResults(results.getResults()); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 315e1d8e..392b2df0 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -420,7 +420,7 @@ public class DbusSignalImpl implements Signal { public void quitGroup(final byte[] groupId) { var group = getGroupId(groupId); try { - m.sendQuitGroupMessage(group, Set.of()); + m.quitGroup(group, Set.of()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException | LastGroupAdminException e) { -- 2.51.0 From e532a24cf8cba6aede416b1b21aa72e95519c383 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 10:56:30 +0200 Subject: [PATCH 08/16] Move more profile functionality to ProfileHelper --- .../asamk/signal/manager/HandleAction.java | 2 +- .../org/asamk/signal/manager/Manager.java | 247 ++-------------- .../signal/manager/helper/ProfileHelper.java | 265 +++++++++++++++++- 3 files changed, 282 insertions(+), 232 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java b/lib/src/main/java/org/asamk/signal/manager/HandleAction.java index 2396df06..8639806f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/HandleAction.java @@ -170,7 +170,7 @@ class RetrieveProfileAction implements HandleAction { @Override public void execute(Manager m) throws Throwable { - m.getRecipientProfile(recipientId, true); + m.refreshRecipientProfile(recipientId); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 5de65f3e..ac7b571f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -58,14 +58,12 @@ import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.KeyUtils; -import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.ProtocolInvalidMessageException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; -import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; @@ -106,8 +104,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; 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.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; @@ -137,7 +133,6 @@ import java.nio.file.Files; import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.Collection; import java.util.Date; import java.util.HashMap; @@ -205,26 +200,29 @@ public class Manager implements Closeable { account.getSignalProtocolStore(), executor, sessionLock); - this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); + this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); + this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, account.getProfileStore()::getProfileKey, this::getRecipientProfile, this::getSenderCertificate); - this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey, + this.profileHelper = new ProfileHelper(account, + dependencies, + avatarStore, + account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, dependencies::getProfileService, dependencies::getMessageReceiver, this::resolveSignalServiceAddress); - final GroupV2Helper groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, + final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, this::getRecipientProfile, account::getSelfRecipientId, dependencies.getGroupsV2Operations(), dependencies.getGroupsV2Api(), this::resolveSignalServiceAddress); - this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); - this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); - this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); this.sendHelper = new SendHelper(account, dependencies, unidentifiedAccessHelper, @@ -246,7 +244,7 @@ public class Manager implements Closeable { return account.getUsername(); } - public SignalServiceAddress getSelfAddress() { + private SignalServiceAddress getSelfAddress() { return account.getSelfAddress(); } @@ -377,45 +375,12 @@ public class Manager implements Closeable { public void setProfile( String givenName, final String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException { - var profile = getRecipientProfile(account.getSelfRecipientId()); - var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); - if (givenName != null) { - builder.withGivenName(givenName); - } - if (familyName != null) { - builder.withFamilyName(familyName); - } - if (about != null) { - builder.withAbout(about); - } - if (aboutEmoji != null) { - builder.withAboutEmoji(aboutEmoji); - } - var newProfile = builder.build(); - - try (final var streamDetails = avatar == null - ? avatarStore.retrieveProfileAvatar(getSelfAddress()) - : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { - dependencies.getAccountManager() - .setVersionedProfile(account.getUuid(), - account.getProfileKey(), - newProfile.getInternalServiceName(), - newProfile.getAbout() == null ? "" : newProfile.getAbout(), - newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), - Optional.absent(), - streamDetails); - } + profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); - if (avatar != null) { - if (avatar.isPresent()) { - avatarStore.storeProfileAvatar(getSelfAddress(), - outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream)); - } else { - avatarStore.deleteProfileAvatar(getSelfAddress()); - } - } - account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile); + sendSyncFetchProfileMessage(); + } + private void sendSyncFetchProfileMessage() throws IOException { sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); } @@ -522,134 +487,12 @@ public class Manager implements Closeable { return record; } - public Profile getRecipientProfile( - RecipientId recipientId - ) { - return getRecipientProfile(recipientId, false); - } - - private final Set pendingProfileRequest = new HashSet<>(); - - Profile getRecipientProfile( - RecipientId recipientId, boolean force - ) { - var profile = account.getProfileStore().getProfile(recipientId); - - var now = System.currentTimeMillis(); - // Profiles are cached for 24h before retrieving them again, unless forced - if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) { - return profile; - } - - synchronized (pendingProfileRequest) { - if (pendingProfileRequest.contains(recipientId)) { - return profile; - } - pendingProfileRequest.add(recipientId); - } - final SignalServiceProfile encryptedProfile; - try { - encryptedProfile = retrieveEncryptedProfile(recipientId); - } finally { - synchronized (pendingProfileRequest) { - pendingProfileRequest.remove(recipientId); - } - } - if (encryptedProfile == null) { - return null; - } - - profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile); - account.getProfileStore().storeProfile(recipientId, profile); - - return profile; - } - - private Profile decryptProfileIfKeyKnown( - final RecipientId recipientId, final SignalServiceProfile encryptedProfile - ) { - var profileKey = account.getProfileStore().getProfileKey(recipientId); - if (profileKey == null) { - return new Profile(System.currentTimeMillis(), - null, - null, - null, - null, - ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null), - ProfileUtils.getCapabilities(encryptedProfile)); - } - - return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile); - } - - private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) { - try { - return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile(); - } catch (IOException e) { - logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); - return null; - } - } - - private ProfileAndCredential retrieveProfileAndCredential( - final RecipientId recipientId, final SignalServiceProfile.RequestType requestType - ) throws IOException { - final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType); - final var profile = profileAndCredential.getProfile(); - - try { - var newIdentity = account.getIdentityKeyStore() - .saveIdentity(recipientId, - new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())), - new Date()); - - if (newIdentity) { - account.getSessionStore().archiveSessions(recipientId); - } - } catch (InvalidKeyException ignored) { - logger.warn("Got invalid identity key in profile for {}", - resolveSignalServiceAddress(recipientId).getIdentifier()); - } - return profileAndCredential; + public Profile getRecipientProfile(RecipientId recipientId) { + return profileHelper.getRecipientProfile(recipientId); } - private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) { - var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId); - if (profileKeyCredential != null) { - return profileKeyCredential; - } - - ProfileAndCredential profileAndCredential; - try { - profileAndCredential = retrieveProfileAndCredential(recipientId, - SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); - } catch (IOException e) { - logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); - return null; - } - - profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull(); - account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential); - - var profileKey = account.getProfileStore().getProfileKey(recipientId); - if (profileKey != null) { - final var profile = decryptProfileAndDownloadAvatar(recipientId, - profileKey, - profileAndCredential.getProfile()); - account.getProfileStore().storeProfile(recipientId, profile); - } - - return profileKeyCredential; - } - - private Profile decryptProfileAndDownloadAvatar( - final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile - ) { - if (encryptedProfile.getAvatar() != null) { - downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey); - } - - return ProfileUtils.decryptProfile(profileKey, encryptedProfile); + public void refreshRecipientProfile(RecipientId recipientId) { + profileHelper.refreshRecipientProfile(recipientId); } private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { @@ -1784,7 +1627,7 @@ public class Manager implements Closeable { if (syncMessage.getFetchType().isPresent()) { switch (syncMessage.getFetchType().get()) { case LOCAL_PROFILE: - getRecipientProfile(account.getSelfRecipientId(), true); + actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); case STORAGE_MANIFEST: // TODO } @@ -1820,20 +1663,6 @@ public class Manager implements Closeable { } } - private void downloadProfileAvatar( - SignalServiceAddress address, String avatarPath, ProfileKey profileKey - ) { - try { - avatarStore.storeProfileAvatar(address, - outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream)); - } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage()); - } - } - public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { return attachmentStore.getAttachmentFile(attachmentId); } @@ -1862,28 +1691,6 @@ public class Manager implements Closeable { } } - private void retrieveProfileAvatar( - String avatarPath, ProfileKey profileKey, OutputStream outputStream - ) throws IOException { - var tmpFile = IOUtils.createTempFile(); - try (var input = dependencies.getMessageReceiver() - .retrieveProfileAvatar(avatarPath, - tmpFile, - profileKey, - ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { - // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ... - IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); - } finally { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } - } - private void retrieveAttachment( final SignalServiceAttachment attachment, final OutputStream outputStream ) throws IOException { @@ -2069,17 +1876,15 @@ public class Manager implements Closeable { public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { final var recipientId = resolveRecipient(recipientIdentifier); - final var recipient = account.getRecipientStore().getRecipient(recipientId); - if (recipient == null) { - return null; - } - if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) { - return recipient.getContact().getName(); + final var contact = account.getRecipientStore().getContact(recipientId); + if (contact != null && !Util.isEmpty(contact.getName())) { + return contact.getName(); } - if (recipient.getProfile() != null && recipient.getProfile() != null) { - return recipient.getProfile().getDisplayName(); + final var profile = getRecipientProfile(recipientId); + if (profile != null) { + return profile.getDisplayName(); } return null; @@ -2188,7 +1993,7 @@ public class Manager implements Closeable { } } else { // Retrieve profile to get the current identity key from the server - retrieveEncryptedProfile(recipientId); + refreshRecipientProfile(recipientId); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index c3c74b0b..ac75a573 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -1,7 +1,20 @@ package org.asamk.signal.manager.helper; +import org.asamk.signal.manager.AvatarStore; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.ProfileUtils; +import org.asamk.signal.manager.util.Utils; import org.signal.zkgroup.profiles.ProfileKey; +import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; @@ -12,29 +25,43 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.internal.ServiceResponse; +import java.io.File; import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Base64; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; import io.reactivex.rxjava3.core.Single; public final class ProfileHelper { - private final ProfileKeyProvider profileKeyProvider; + private final static Logger logger = LoggerFactory.getLogger(ProfileHelper.class); + private final SignalAccount account; + private final SignalDependencies dependencies; + private final AvatarStore avatarStore; + private final ProfileKeyProvider profileKeyProvider; private final UnidentifiedAccessProvider unidentifiedAccessProvider; - private final ProfileServiceProvider profileServiceProvider; - private final MessageReceiverProvider messageReceiverProvider; - private final SignalServiceAddressResolver addressResolver; public ProfileHelper( + final SignalAccount account, + final SignalDependencies dependencies, + final AvatarStore avatarStore, final ProfileKeyProvider profileKeyProvider, final UnidentifiedAccessProvider unidentifiedAccessProvider, final ProfileServiceProvider profileServiceProvider, final MessageReceiverProvider messageReceiverProvider, final SignalServiceAddressResolver addressResolver ) { + this.account = account; + this.dependencies = dependencies; + this.avatarStore = avatarStore; this.profileKeyProvider = profileKeyProvider; this.unidentifiedAccessProvider = unidentifiedAccessProvider; this.profileServiceProvider = profileServiceProvider; @@ -42,7 +69,193 @@ public final class ProfileHelper { this.addressResolver = addressResolver; } - public ProfileAndCredential retrieveProfileSync( + public Profile getRecipientProfile(RecipientId recipientId) { + return getRecipientProfile(recipientId, false); + } + + public void refreshRecipientProfile(RecipientId recipientId) { + getRecipientProfile(recipientId, true); + } + + public ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) { + var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId); + if (profileKeyCredential != null) { + return profileKeyCredential; + } + + ProfileAndCredential profileAndCredential; + try { + profileAndCredential = retrieveProfileAndCredential(recipientId, + SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); + } catch (IOException e) { + logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); + return null; + } + + profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull(); + account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential); + + var profileKey = account.getProfileStore().getProfileKey(recipientId); + if (profileKey != null) { + final var profile = decryptProfileAndDownloadAvatar(recipientId, + profileKey, + profileAndCredential.getProfile()); + account.getProfileStore().storeProfile(recipientId, profile); + } + + return profileKeyCredential; + } + + /** + * @param givenName if null, the previous givenName will be kept + * @param familyName if null, the previous familyName will be kept + * @param about if null, the previous about text will be kept + * @param aboutEmoji if null, the previous about emoji will be kept + * @param avatar if avatar is null the image from the local avatar store is used (if present), + */ + public void setProfile( + String givenName, final String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException { + var profile = getRecipientProfile(account.getSelfRecipientId()); + var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); + if (givenName != null) { + builder.withGivenName(givenName); + } + if (familyName != null) { + builder.withFamilyName(familyName); + } + if (about != null) { + builder.withAbout(about); + } + if (aboutEmoji != null) { + builder.withAboutEmoji(aboutEmoji); + } + var newProfile = builder.build(); + + try (final var streamDetails = avatar == null + ? avatarStore.retrieveProfileAvatar(account.getSelfAddress()) + : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { + dependencies.getAccountManager() + .setVersionedProfile(account.getUuid(), + account.getProfileKey(), + newProfile.getInternalServiceName(), + newProfile.getAbout() == null ? "" : newProfile.getAbout(), + newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), + Optional.absent(), + streamDetails); + } + + if (avatar != null) { + if (avatar.isPresent()) { + avatarStore.storeProfileAvatar(account.getSelfAddress(), + outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream)); + } else { + avatarStore.deleteProfileAvatar(account.getSelfAddress()); + } + } + account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile); + } + + private final Set pendingProfileRequest = new HashSet<>(); + + private Profile getRecipientProfile(RecipientId recipientId, boolean force) { + var profile = account.getProfileStore().getProfile(recipientId); + + var now = System.currentTimeMillis(); + // Profiles are cached for 24h before retrieving them again, unless forced + if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) { + return profile; + } + + synchronized (pendingProfileRequest) { + if (pendingProfileRequest.contains(recipientId)) { + return profile; + } + pendingProfileRequest.add(recipientId); + } + final SignalServiceProfile encryptedProfile; + try { + encryptedProfile = retrieveEncryptedProfile(recipientId); + } finally { + synchronized (pendingProfileRequest) { + pendingProfileRequest.remove(recipientId); + } + } + if (encryptedProfile == null) { + return null; + } + + profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile); + account.getProfileStore().storeProfile(recipientId, profile); + + return profile; + } + + private Profile decryptProfileIfKeyKnown( + final RecipientId recipientId, final SignalServiceProfile encryptedProfile + ) { + var profileKey = account.getProfileStore().getProfileKey(recipientId); + if (profileKey == null) { + return new Profile(System.currentTimeMillis(), + null, + null, + null, + null, + ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null), + ProfileUtils.getCapabilities(encryptedProfile)); + } + + return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile); + } + + private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) { + try { + return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile(); + } catch (IOException e) { + logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); + return null; + } + } + + private SignalServiceProfile retrieveProfileSync(String username) throws IOException { + return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent()); + } + + private ProfileAndCredential retrieveProfileAndCredential( + final RecipientId recipientId, final SignalServiceProfile.RequestType requestType + ) throws IOException { + final var profileAndCredential = retrieveProfileSync(recipientId, requestType); + final var profile = profileAndCredential.getProfile(); + + try { + var newIdentity = account.getIdentityKeyStore() + .saveIdentity(recipientId, + new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())), + new Date()); + + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } + } catch (InvalidKeyException ignored) { + logger.warn("Got invalid identity key in profile for {}", + addressResolver.resolveSignalServiceAddress(recipientId).getIdentifier()); + } + return profileAndCredential; + } + + private Profile decryptProfileAndDownloadAvatar( + final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile + ) { + if (encryptedProfile.getAvatar() != null) { + downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId), + encryptedProfile.getAvatar(), + profileKey); + } + + return ProfileUtils.decryptProfile(profileKey, encryptedProfile); + } + + private ProfileAndCredential retrieveProfileSync( RecipientId recipientId, SignalServiceProfile.RequestType requestType ) throws IOException { try { @@ -58,11 +271,7 @@ public final class ProfileHelper { } } - public SignalServiceProfile retrieveProfileSync(String username) throws IOException { - return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent()); - } - - public Single retrieveProfile( + private Single retrieveProfile( RecipientId recipientId, SignalServiceProfile.RequestType requestType ) throws IOException { var unidentifiedAccess = getUnidentifiedAccess(recipientId); @@ -106,6 +315,42 @@ public final class ProfileHelper { }); } + private void downloadProfileAvatar( + SignalServiceAddress address, String avatarPath, ProfileKey profileKey + ) { + try { + avatarStore.storeProfileAvatar(address, + outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream)); + } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage()); + } + } + + private void retrieveProfileAvatar( + String avatarPath, ProfileKey profileKey, OutputStream outputStream + ) throws IOException { + var tmpFile = IOUtils.createTempFile(); + try (var input = dependencies.getMessageReceiver() + .retrieveProfileAvatar(avatarPath, + tmpFile, + profileKey, + ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { + // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ... + IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); + } finally { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); + } + } + } + private Optional getUnidentifiedAccess(RecipientId recipientId) { var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId); -- 2.51.0 From debbaa81ba9a371c5529bac543a3ef8c10fcc5f5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 12:05:15 +0200 Subject: [PATCH 09/16] Extract AttachmentHelper and SyncHelper --- .../org/asamk/signal/manager/Manager.java | 504 ++---------------- .../manager/helper/AttachmentHelper.java | 122 +++++ .../signal/manager/helper/GroupHelper.java | 18 + .../signal/manager/helper/SyncHelper.java | 373 +++++++++++++ 4 files changed, 562 insertions(+), 455 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index ac7b571f..936f625c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -35,11 +35,13 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.helper.AttachmentHelper; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.jobs.Context; import org.asamk.signal.manager.jobs.Job; @@ -55,8 +57,6 @@ import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.StickerPackId; -import org.asamk.signal.manager.util.AttachmentUtils; -import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; @@ -69,7 +69,6 @@ import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.fingerprint.Fingerprint; import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; @@ -82,30 +81,15 @@ import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; -import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; -import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -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.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -113,23 +97,17 @@ import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableExcept 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.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; import java.io.Closeable; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; @@ -165,6 +143,8 @@ public class Manager implements Closeable { private final ProfileHelper profileHelper; private final PinHelper pinHelper; private final SendHelper sendHelper; + private final SyncHelper syncHelper; + private final AttachmentHelper attachmentHelper; private final GroupHelper groupHelper; private final AvatarStore avatarStore; @@ -204,6 +184,7 @@ public class Manager implements Closeable { this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, account.getProfileStore()::getProfileKey, @@ -233,11 +214,19 @@ public class Manager implements Closeable { this::refreshRegisteredUser); this.groupHelper = new GroupHelper(account, dependencies, + attachmentHelper, sendHelper, groupV2Helper, avatarStore, this::resolveSignalServiceAddress, this::resolveRecipient); + this.syncHelper = new SyncHelper(account, + attachmentHelper, + sendHelper, + groupHelper, + avatarStore, + this::resolveSignalServiceAddress, + this::resolveRecipient); } public String getUsername() { @@ -376,12 +365,7 @@ public class Manager implements Closeable { String givenName, final String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException { profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); - - sendSyncFetchProfileMessage(); - } - - private void sendSyncFetchProfileMessage() throws IOException { - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); + syncHelper.sendSyncFetchProfileMessage(); } public void unregister() throws IOException { @@ -495,15 +479,6 @@ public class Manager implements Closeable { profileHelper.refreshRecipientProfile(recipientId); } - private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { - final var streamDetails = avatarStore.retrieveContactAvatar(address); - if (streamDetails == null) { - return Optional.absent(); - } - - return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); - } - public List getGroups() { return account.getGroupStore().getGroups(); } @@ -516,8 +491,7 @@ public class Manager implements Closeable { } public void deleteGroup(GroupId groupId) throws IOException { - account.getGroupStore().deleteGroup(groupId); - avatarStore.deleteGroupAvatar(groupId); + groupHelper.deleteGroup(groupId); } public Pair createGroup( @@ -660,21 +634,9 @@ public class Manager implements Closeable { final SignalServiceDataMessage.Builder messageBuilder, final Message message ) throws AttachmentInvalidException, IOException { messageBuilder.withBody(message.getMessageText()); - if (message.getAttachments() != null) { - var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(message.getAttachments()); - - // Upload attachments here, so we only upload once even for multiple recipients - var messageSender = dependencies.getMessageSender(); - var attachmentPointers = new ArrayList(attachmentStreams.size()); - for (var attachment : attachmentStreams) { - if (attachment.isStream()) { - attachmentPointers.add(messageSender.uploadAttachment(attachment.asStream())); - } else if (attachment.isPointer()) { - attachmentPointers.add(attachment.asPointer()); - } - } - - messageBuilder.withAttachments(attachmentPointers); + final var attachments = message.getAttachments(); + if (attachments != null) { + messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments)); } } @@ -822,51 +784,7 @@ public class Manager implements Closeable { } public void requestAllSyncData() throws IOException { - requestSyncGroups(); - requestSyncContacts(); - requestSyncBlocked(); - requestSyncConfiguration(); - requestSyncKeys(); - } - - private void requestSyncGroups() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncContacts() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncBlocked() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncConfiguration() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncKeys() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); + syncHelper.requestAllSyncData(); } private byte[] getSenderCertificate() { @@ -984,7 +902,7 @@ public class Manager implements Closeable { if (groupInfo.getAvatar().isPresent()) { var avatar = groupInfo.getAvatar().get(); - downloadGroupAvatar(groupV1.getGroupId(), avatar); + groupHelper.downloadGroupAvatar(groupV1.getGroupId(), avatar); } if (groupInfo.getName().isPresent()) { @@ -1059,13 +977,31 @@ public class Manager implements Closeable { if (!ignoreAttachments) { if (message.getAttachments().isPresent()) { for (var attachment : message.getAttachments().get()) { - downloadAttachment(attachment); + attachmentHelper.downloadAttachment(attachment); } } if (message.getSharedContacts().isPresent()) { for (var contact : message.getSharedContacts().get()) { if (contact.getAvatar().isPresent()) { - downloadAttachment(contact.getAvatar().get().getAttachment()); + attachmentHelper.downloadAttachment(contact.getAvatar().get().getAttachment()); + } + } + } + if (message.getPreviews().isPresent()) { + final var previews = message.getPreviews().get(); + for (var preview : previews) { + if (preview.getImage().isPresent()) { + attachmentHelper.downloadAttachment(preview.getImage().get()); + } + } + } + if (message.getQuote().isPresent()) { + final var quote = message.getQuote().get(); + + for (var quotedAttachment : quote.getAttachments()) { + final var thumbnail = quotedAttachment.getThumbnail(); + if (thumbnail != null) { + attachmentHelper.downloadAttachment(thumbnail); } } } @@ -1082,24 +1018,6 @@ public class Manager implements Closeable { } this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey); } - if (message.getPreviews().isPresent()) { - final var previews = message.getPreviews().get(); - for (var preview : previews) { - if (preview.getImage().isPresent()) { - downloadAttachment(preview.getImage().get()); - } - } - } - if (message.getQuote().isPresent()) { - final var quote = message.getQuote().get(); - - for (var quotedAttachment : quote.getAttachments()) { - final var thumbnail = quotedAttachment.getThumbnail(); - if (thumbnail != null) { - downloadAttachment(thumbnail); - } - } - } if (message.getSticker().isPresent()) { final var messageSticker = message.getSticker().get(); final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId()); @@ -1441,65 +1359,11 @@ public class Manager implements Closeable { // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); } if (syncMessage.getGroups().isPresent()) { - File tmpFile = null; try { - tmpFile = IOUtils.createTempFile(); final var groupsMessage = syncMessage.getGroups().get(); - try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) { - var s = new DeviceGroupsInputStream(attachmentAsStream); - DeviceGroup g; - while (true) { - try { - g = s.read(); - } catch (IOException e) { - logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage()); - continue; - } - if (g == null) { - break; - } - var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId())); - if (syncGroup != null) { - if (g.getName().isPresent()) { - syncGroup.name = g.getName().get(); - } - syncGroup.addMembers(g.getMembers() - .stream() - .map(this::resolveRecipient) - .collect(Collectors.toSet())); - if (!g.isActive()) { - syncGroup.removeMember(account.getSelfRecipientId()); - } else { - // Add ourself to the member set as it's marked as active - syncGroup.addMembers(List.of(account.getSelfRecipientId())); - } - syncGroup.blocked = g.isBlocked(); - if (g.getColor().isPresent()) { - syncGroup.color = g.getColor().get(); - } - - if (g.getAvatar().isPresent()) { - downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); - } - syncGroup.archived = g.isArchived(); - account.getGroupStore().updateGroup(syncGroup); - } - } - } + attachmentHelper.retrieveAttachment(groupsMessage, syncHelper::handleSyncDeviceGroups); } catch (Exception e) { - logger.warn("Failed to handle received sync groups “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } finally { - if (tmpFile != null) { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received groups temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } + logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage()); } } if (syncMessage.getBlockedList().isPresent()) { @@ -1520,75 +1384,12 @@ public class Manager implements Closeable { } } if (syncMessage.getContacts().isPresent()) { - File tmpFile = null; try { - tmpFile = IOUtils.createTempFile(); final var contactsMessage = syncMessage.getContacts().get(); - try (var attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream() - .asPointer(), tmpFile)) { - var s = new DeviceContactsInputStream(attachmentAsStream); - DeviceContact c; - while (true) { - try { - c = s.read(); - } catch (IOException e) { - logger.warn("Sync contacts contained invalid contact, ignoring: {}", - e.getMessage()); - continue; - } - if (c == null) { - break; - } - if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { - account.setProfileKey(c.getProfileKey().get()); - } - final var recipientId = resolveRecipientTrusted(c.getAddress()); - var contact = account.getContactStore().getContact(recipientId); - final var builder = contact == null - ? Contact.newBuilder() - : Contact.newBuilder(contact); - if (c.getName().isPresent()) { - builder.withName(c.getName().get()); - } - if (c.getColor().isPresent()) { - builder.withColor(c.getColor().get()); - } - if (c.getProfileKey().isPresent()) { - account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get()); - } - if (c.getVerified().isPresent()) { - final var verifiedMessage = c.getVerified().get(); - account.getIdentityKeyStore() - .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()), - verifiedMessage.getIdentityKey(), - TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); - } - if (c.getExpirationTimer().isPresent()) { - builder.withMessageExpirationTime(c.getExpirationTimer().get()); - } - builder.withBlocked(c.isBlocked()); - builder.withArchived(c.isArchived()); - account.getContactStore().storeContact(recipientId, builder.build()); - - if (c.getAvatar().isPresent()) { - downloadContactAvatar(c.getAvatar().get(), c.getAddress()); - } - } - } + attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(), + syncHelper::handleSyncDeviceContacts); } catch (Exception e) { - logger.warn("Failed to handle received sync contacts “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } finally { - if (tmpFile != null) { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received contacts temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } + logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); } } if (syncMessage.getVerified().isPresent()) { @@ -1647,227 +1448,20 @@ public class Manager implements Closeable { return actions; } - private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) { - try { - avatarStore.storeContactAvatar(address, outputStream -> retrieveAttachment(avatar, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage()); - } - } - - private void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { - try { - avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); - } - } - public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { return attachmentStore.getAttachmentFile(attachmentId); } - private void downloadAttachment(final SignalServiceAttachment attachment) { - if (!attachment.isPointer()) { - logger.warn("Invalid state, can't store an attachment stream."); - } - - var pointer = attachment.asPointer(); - if (pointer.getPreview().isPresent()) { - final var preview = pointer.getPreview().get(); - try { - attachmentStore.storeAttachmentPreview(pointer.getRemoteId(), - outputStream -> outputStream.write(preview, 0, preview.length)); - } catch (IOException e) { - logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage()); - } - } - - try { - attachmentStore.storeAttachment(pointer.getRemoteId(), - outputStream -> retrieveAttachmentPointer(pointer, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage()); - } - } - - private void retrieveAttachment( - final SignalServiceAttachment attachment, final OutputStream outputStream - ) throws IOException { - if (attachment.isPointer()) { - var pointer = attachment.asPointer(); - retrieveAttachmentPointer(pointer, outputStream); - } else { - var stream = attachment.asStream(); - IOUtils.copyStream(stream.getInputStream(), outputStream); - } - } - - private void retrieveAttachmentPointer( - SignalServiceAttachmentPointer pointer, OutputStream outputStream - ) throws IOException { - var tmpFile = IOUtils.createTempFile(); - try (var input = retrieveAttachmentAsStream(pointer, tmpFile)) { - IOUtils.copyStream(input, outputStream); - } catch (MissingConfigurationException | InvalidMessageException e) { - throw new IOException(e); - } finally { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } - } - - private InputStream retrieveAttachmentAsStream( - SignalServiceAttachmentPointer pointer, File tmpFile - ) throws IOException, InvalidMessageException, MissingConfigurationException { - return dependencies.getMessageReceiver() - .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); - } - void sendGroups() throws IOException { - var groupsFile = IOUtils.createTempFile(); - - try { - try (OutputStream fos = new FileOutputStream(groupsFile)) { - var out = new DeviceGroupsOutputStream(fos); - for (var record : getGroups()) { - if (record instanceof GroupInfoV1) { - var groupInfo = (GroupInfoV1) record; - out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), - Optional.fromNullable(groupInfo.name), - groupInfo.getMembers() - .stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList()), - groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()), - groupInfo.isMember(account.getSelfRecipientId()), - Optional.of(groupInfo.messageExpirationTime), - Optional.fromNullable(groupInfo.color), - groupInfo.blocked, - Optional.absent(), - groupInfo.archived)); - } - } - } - - if (groupsFile.exists() && groupsFile.length() > 0) { - try (var groupsFileStream = new FileInputStream(groupsFile)) { - var attachmentStream = SignalServiceAttachment.newStreamBuilder() - .withStream(groupsFileStream) - .withContentType("application/octet-stream") - .withLength(groupsFile.length()) - .build(); - - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); - } - } - } finally { - try { - Files.delete(groupsFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage()); - } - } + syncHelper.sendGroups(); } public void sendContacts() throws IOException { - var contactsFile = IOUtils.createTempFile(); - - try { - try (OutputStream fos = new FileOutputStream(contactsFile)) { - var out = new DeviceContactsOutputStream(fos); - for (var contactPair : account.getContactStore().getContacts()) { - final var recipientId = contactPair.first(); - final var contact = contactPair.second(); - final var address = resolveSignalServiceAddress(recipientId); - - var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId); - VerifiedMessage verifiedMessage = null; - if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(address, - currentIdentity.getIdentityKey(), - currentIdentity.getTrustLevel().toVerifiedState(), - currentIdentity.getDateAdded().getTime()); - } - - var profileKey = account.getProfileStore().getProfileKey(recipientId); - out.write(new DeviceContact(address, - Optional.fromNullable(contact.getName()), - createContactAvatarAttachment(address), - Optional.fromNullable(contact.getColor()), - Optional.fromNullable(verifiedMessage), - Optional.fromNullable(profileKey), - contact.isBlocked(), - Optional.of(contact.getMessageExpirationTime()), - Optional.absent(), - contact.isArchived())); - } - - 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.of(account.getProfileKey()), - false, - Optional.absent(), - Optional.absent(), - false)); - } - } - - if (contactsFile.exists() && contactsFile.length() > 0) { - try (var contactsFileStream = new FileInputStream(contactsFile)) { - var attachmentStream = SignalServiceAttachment.newStreamBuilder() - .withStream(contactsFileStream) - .withContentType("application/octet-stream") - .withLength(contactsFile.length()) - .build(); - - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, - true))); - } - } - } finally { - try { - Files.delete(contactsFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage()); - } - } + syncHelper.sendContacts(); } void sendBlockedList() throws IOException { - var addresses = new ArrayList(); - for (var record : account.getContactStore().getContacts()) { - if (record.second().isBlocked()) { - addresses.add(resolveSignalServiceAddress(record.first())); - } - } - var groupIds = new ArrayList(); - for (var record : getGroups()) { - if (record.isBlocked()) { - groupIds.add(record.getGroupId().serialize()); - } - } - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); - } - - private void sendVerifiedMessage( - SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel - ) throws IOException { - var verifiedMessage = new VerifiedMessage(destination, - identityKey, - trustLevel.toVerifiedState(), - System.currentTimeMillis()); - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); + syncHelper.sendBlockedList(); } public List> getContacts() { @@ -1974,7 +1568,7 @@ public class Manager implements Closeable { account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); try { var address = account.getRecipientStore().resolveServiceAddress(recipientId); - sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); + syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); } catch (IOException e) { logger.warn("Failed to send verification sync message: {}", e.getMessage()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java new file mode 100644 index 00000000..88a611b9 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -0,0 +1,122 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.AttachmentStore; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.util.AttachmentUtils; +import org.asamk.signal.manager.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +public class AttachmentHelper { + + private final static Logger logger = LoggerFactory.getLogger(AttachmentHelper.class); + + private final SignalDependencies dependencies; + private final AttachmentStore attachmentStore; + + public AttachmentHelper( + final SignalDependencies dependencies, final AttachmentStore attachmentStore + ) { + this.dependencies = dependencies; + this.attachmentStore = attachmentStore; + } + + public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException { + var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); + + // Upload attachments here, so we only upload once even for multiple recipients + var messageSender = dependencies.getMessageSender(); + var attachmentPointers = new ArrayList(attachmentStreams.size()); + for (var attachment : attachmentStreams) { + if (attachment.isStream()) { + attachmentPointers.add(messageSender.uploadAttachment(attachment.asStream())); + } else if (attachment.isPointer()) { + attachmentPointers.add(attachment.asPointer()); + } + } + return attachmentPointers; + } + + public void downloadAttachment(final SignalServiceAttachment attachment) { + if (!attachment.isPointer()) { + logger.warn("Invalid state, can't store an attachment stream."); + } + + var pointer = attachment.asPointer(); + if (pointer.getPreview().isPresent()) { + final var preview = pointer.getPreview().get(); + try { + attachmentStore.storeAttachmentPreview(pointer.getRemoteId(), + outputStream -> outputStream.write(preview, 0, preview.length)); + } catch (IOException e) { + logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage()); + } + } + + try { + attachmentStore.storeAttachment(pointer.getRemoteId(), + outputStream -> this.retrieveAttachment(pointer, outputStream)); + } catch (IOException e) { + logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage()); + } + } + + void retrieveAttachment(SignalServiceAttachment attachment, OutputStream outputStream) throws IOException { + retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream)); + } + + public void retrieveAttachment( + SignalServiceAttachment attachment, AttachmentHandler consumer + ) throws IOException { + if (attachment.isStream()) { + try (var input = attachment.asStream().getInputStream()) { + consumer.handle(input); + } + return; + } + + var tmpFile = IOUtils.createTempFile(); + try (var input = retrieveAttachmentAsStream(attachment.asPointer(), tmpFile)) { + consumer.handle(input); + } finally { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); + } + } + } + + private InputStream retrieveAttachmentAsStream( + SignalServiceAttachmentPointer pointer, File tmpFile + ) throws IOException { + try { + return dependencies.getMessageReceiver() + .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); + } catch (MissingConfigurationException | InvalidMessageException e) { + throw new IOException(e); + } + } + + @FunctionalInterface + public interface AttachmentHandler { + + void handle(InputStream inputStream) throws IOException; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 9ff3134e..5566d9d7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; @@ -59,6 +60,7 @@ public class GroupHelper { private final SignalAccount account; private final SignalDependencies dependencies; + private final AttachmentHelper attachmentHelper; private final SendHelper sendHelper; private final GroupV2Helper groupV2Helper; private final AvatarStore avatarStore; @@ -68,6 +70,7 @@ public class GroupHelper { public GroupHelper( final SignalAccount account, final SignalDependencies dependencies, + final AttachmentHelper attachmentHelper, final SendHelper sendHelper, final GroupV2Helper groupV2Helper, final AvatarStore avatarStore, @@ -76,6 +79,7 @@ public class GroupHelper { ) { this.account = account; this.dependencies = dependencies; + this.attachmentHelper = attachmentHelper; this.sendHelper = sendHelper; this.groupV2Helper = groupV2Helper; this.avatarStore = avatarStore; @@ -87,6 +91,15 @@ public class GroupHelper { return getGroup(groupId, false); } + public void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { + try { + avatarStore.storeGroupAvatar(groupId, + outputStream -> attachmentHelper.retrieveAttachment(avatar, outputStream)); + } catch (IOException e) { + logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); + } + } + public Optional createGroupAvatarAttachment(GroupIdV1 groupId) throws IOException { final var streamDetails = avatarStore.retrieveGroupAvatar(groupId); if (streamDetails == null) { @@ -282,6 +295,11 @@ public class GroupHelper { } } + public void deleteGroup(GroupId groupId) throws IOException { + account.getGroupStore().deleteGroup(groupId); + avatarStore.deleteGroupAvatar(groupId); + } + public SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, RecipientId recipientId ) throws IOException { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java new file mode 100644 index 00000000..48dc206e --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -0,0 +1,373 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.AvatarStore; +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.asamk.signal.manager.util.AttachmentUtils; +import org.asamk.signal.manager.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; +import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; +import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; +import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class SyncHelper { + + private final static Logger logger = LoggerFactory.getLogger(SyncHelper.class); + + private final SignalAccount account; + private final AttachmentHelper attachmentHelper; + private final SendHelper sendHelper; + private final GroupHelper groupHelper; + private final AvatarStore avatarStore; + private final SignalServiceAddressResolver addressResolver; + private final RecipientResolver recipientResolver; + + public SyncHelper( + final SignalAccount account, + final AttachmentHelper attachmentHelper, + final SendHelper sendHelper, + final GroupHelper groupHelper, + final AvatarStore avatarStore, + final SignalServiceAddressResolver addressResolver, + final RecipientResolver recipientResolver + ) { + this.account = account; + this.attachmentHelper = attachmentHelper; + this.sendHelper = sendHelper; + this.groupHelper = groupHelper; + this.avatarStore = avatarStore; + this.addressResolver = addressResolver; + this.recipientResolver = recipientResolver; + } + + public void requestAllSyncData() throws IOException { + requestSyncGroups(); + requestSyncContacts(); + requestSyncBlocked(); + requestSyncConfiguration(); + requestSyncKeys(); + } + + public void sendSyncFetchProfileMessage() throws IOException { + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); + } + + public void sendGroups() throws IOException { + var groupsFile = IOUtils.createTempFile(); + + try { + try (OutputStream fos = new FileOutputStream(groupsFile)) { + var out = new DeviceGroupsOutputStream(fos); + for (var record : account.getGroupStore().getGroups()) { + if (record instanceof GroupInfoV1) { + var groupInfo = (GroupInfoV1) record; + out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), + Optional.fromNullable(groupInfo.name), + groupInfo.getMembers() + .stream() + .map(addressResolver::resolveSignalServiceAddress) + .collect(Collectors.toList()), + groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()), + groupInfo.isMember(account.getSelfRecipientId()), + Optional.of(groupInfo.messageExpirationTime), + Optional.fromNullable(groupInfo.color), + groupInfo.blocked, + Optional.absent(), + groupInfo.archived)); + } + } + } + + if (groupsFile.exists() && groupsFile.length() > 0) { + try (var groupsFileStream = new FileInputStream(groupsFile)) { + var attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(groupsFileStream) + .withContentType("application/octet-stream") + .withLength(groupsFile.length()) + .build(); + + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + } + } + } finally { + try { + Files.delete(groupsFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage()); + } + } + } + + public void sendContacts() throws IOException { + var contactsFile = IOUtils.createTempFile(); + + try { + try (OutputStream fos = new FileOutputStream(contactsFile)) { + var out = new DeviceContactsOutputStream(fos); + for (var contactPair : account.getContactStore().getContacts()) { + final var recipientId = contactPair.first(); + final var contact = contactPair.second(); + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + + var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId); + VerifiedMessage verifiedMessage = null; + if (currentIdentity != null) { + verifiedMessage = new VerifiedMessage(address, + currentIdentity.getIdentityKey(), + currentIdentity.getTrustLevel().toVerifiedState(), + currentIdentity.getDateAdded().getTime()); + } + + var profileKey = account.getProfileStore().getProfileKey(recipientId); + out.write(new DeviceContact(address, + Optional.fromNullable(contact.getName()), + createContactAvatarAttachment(address), + Optional.fromNullable(contact.getColor()), + Optional.fromNullable(verifiedMessage), + Optional.fromNullable(profileKey), + contact.isBlocked(), + Optional.of(contact.getMessageExpirationTime()), + Optional.absent(), + contact.isArchived())); + } + + 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.of(account.getProfileKey()), + false, + Optional.absent(), + Optional.absent(), + false)); + } + } + + if (contactsFile.exists() && contactsFile.length() > 0) { + try (var contactsFileStream = new FileInputStream(contactsFile)) { + var attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(contactsFileStream) + .withContentType("application/octet-stream") + .withLength(contactsFile.length()) + .build(); + + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, + true))); + } + } + } finally { + try { + Files.delete(contactsFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage()); + } + } + } + + public void sendBlockedList() throws IOException { + var addresses = new ArrayList(); + for (var record : account.getContactStore().getContacts()) { + if (record.second().isBlocked()) { + addresses.add(addressResolver.resolveSignalServiceAddress(record.first())); + } + } + var groupIds = new ArrayList(); + for (var record : account.getGroupStore().getGroups()) { + if (record.isBlocked()) { + groupIds.add(record.getGroupId().serialize()); + } + } + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); + } + + public void sendVerifiedMessage( + SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel + ) throws IOException { + var verifiedMessage = new VerifiedMessage(destination, + identityKey, + trustLevel.toVerifiedState(), + System.currentTimeMillis()); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); + } + + public void handleSyncDeviceGroups(final InputStream input) { + final var s = new DeviceGroupsInputStream(input); + DeviceGroup g; + while (true) { + try { + g = s.read(); + } catch (IOException e) { + logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage()); + continue; + } + if (g == null) { + break; + } + var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId())); + if (syncGroup != null) { + if (g.getName().isPresent()) { + syncGroup.name = g.getName().get(); + } + syncGroup.addMembers(g.getMembers() + .stream() + .map(recipientResolver::resolveRecipient) + .collect(Collectors.toSet())); + if (!g.isActive()) { + syncGroup.removeMember(account.getSelfRecipientId()); + } else { + // Add ourself to the member set as it's marked as active + syncGroup.addMembers(List.of(account.getSelfRecipientId())); + } + syncGroup.blocked = g.isBlocked(); + if (g.getColor().isPresent()) { + syncGroup.color = g.getColor().get(); + } + + if (g.getAvatar().isPresent()) { + groupHelper.downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); + } + syncGroup.archived = g.isArchived(); + account.getGroupStore().updateGroup(syncGroup); + } + } + } + + public void handleSyncDeviceContacts(final InputStream input) { + final var s = new DeviceContactsInputStream(input); + DeviceContact c; + while (true) { + try { + c = s.read(); + } catch (IOException e) { + logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage()); + continue; + } + if (c == null) { + break; + } + if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { + account.setProfileKey(c.getProfileKey().get()); + } + final var recipientId = account.getRecipientStore().resolveRecipientTrusted(c.getAddress()); + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + if (c.getName().isPresent()) { + builder.withName(c.getName().get()); + } + if (c.getColor().isPresent()) { + builder.withColor(c.getColor().get()); + } + if (c.getProfileKey().isPresent()) { + account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get()); + } + if (c.getVerified().isPresent()) { + final var verifiedMessage = c.getVerified().get(); + account.getIdentityKeyStore() + .setIdentityTrustLevel(account.getRecipientStore() + .resolveRecipientTrusted(verifiedMessage.getDestination()), + verifiedMessage.getIdentityKey(), + TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + } + if (c.getExpirationTimer().isPresent()) { + builder.withMessageExpirationTime(c.getExpirationTimer().get()); + } + builder.withBlocked(c.isBlocked()); + builder.withArchived(c.isArchived()); + account.getContactStore().storeContact(recipientId, builder.build()); + + if (c.getAvatar().isPresent()) { + downloadContactAvatar(c.getAvatar().get(), c.getAddress()); + } + } + } + + private void requestSyncGroups() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() + .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS) + .build(); + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); + } + + private void requestSyncContacts() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() + .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS) + .build(); + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); + } + + private void requestSyncBlocked() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() + .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED) + .build(); + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); + } + + private void requestSyncConfiguration() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() + .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION) + .build(); + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); + } + + private void requestSyncKeys() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() + .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS) + .build(); + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); + } + + private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { + final var streamDetails = avatarStore.retrieveContactAvatar(address); + if (streamDetails == null) { + return Optional.absent(); + } + + return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); + } + + private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) { + try { + avatarStore.storeContactAvatar(address, + outputStream -> attachmentHelper.retrieveAttachment(avatar, outputStream)); + } catch (IOException e) { + logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage()); + } + } +} -- 2.51.0 From 8bc6c0abcbdc70b1049df08712cdeff046f48f5e Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 15:25:02 +0200 Subject: [PATCH 10/16] Extract ContactHelper and IncomingMessageHandler --- .../asamk/signal/manager/HandleAction.java | 219 ------ .../org/asamk/signal/manager/JobExecutor.java | 17 + .../org/asamk/signal/manager/Manager.java | 622 ++---------------- .../signal/manager/actions/HandleAction.java | 8 + .../manager/actions/RenewSessionAction.java | 36 + .../actions/RetrieveProfileAction.java | 33 + .../manager/actions/SendGroupInfoAction.java | 39 ++ .../actions/SendGroupInfoRequestAction.java | 39 ++ .../manager/actions/SendReceiptAction.java | 36 + .../actions/SendSyncBlockedListAction.java | 20 + .../actions/SendSyncContactsAction.java | 20 + .../manager/actions/SendSyncGroupsAction.java | 20 + .../manager/helper/AttachmentHelper.java | 5 + .../signal/manager/helper/ContactHelper.java | 41 ++ .../signal/manager/helper/GroupHelper.java | 15 + .../helper/IncomingMessageHandler.java | 492 ++++++++++++++ .../signal/manager/helper/SendHelper.java | 10 + .../asamk/signal/manager/jobs/Context.java | 42 +- .../asamk/signal/commands/BlockCommand.java | 7 + .../asamk/signal/commands/UnblockCommand.java | 7 + .../org/asamk/signal/dbus/DbusSignalImpl.java | 4 + 21 files changed, 953 insertions(+), 779 deletions(-) delete mode 100644 lib/src/main/java/org/asamk/signal/manager/HandleAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/JobExecutor.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/HandleAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/RetrieveProfileAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoRequestAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendSyncBlockedListAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendSyncContactsAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendSyncGroupsAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java diff --git a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java b/lib/src/main/java/org/asamk/signal/manager/HandleAction.java deleted file mode 100644 index 8639806f..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java +++ /dev/null @@ -1,219 +0,0 @@ -package org.asamk.signal.manager; - -import org.asamk.signal.manager.groups.GroupIdV1; -import org.asamk.signal.manager.storage.recipients.RecipientId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; - -import java.util.List; -import java.util.Objects; - -interface HandleAction { - - void execute(Manager m) throws Throwable; -} - -class SendReceiptAction implements HandleAction { - - private final SignalServiceAddress address; - private final long timestamp; - - public SendReceiptAction(final SignalServiceAddress address, final long timestamp) { - this.address = address; - this.timestamp = timestamp; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendDeliveryReceipt(address, List.of(timestamp)); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final var that = (SendReceiptAction) o; - return timestamp == that.timestamp && address.equals(that.address); - } - - @Override - public int hashCode() { - return Objects.hash(address, timestamp); - } -} - -class SendSyncContactsAction implements HandleAction { - - private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction(); - - private SendSyncContactsAction() { - } - - public static SendSyncContactsAction create() { - return INSTANCE; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendContacts(); - } -} - -class SendSyncGroupsAction implements HandleAction { - - private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction(); - - private SendSyncGroupsAction() { - } - - public static SendSyncGroupsAction create() { - return INSTANCE; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendGroups(); - } -} - -class SendSyncBlockedListAction implements HandleAction { - - private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction(); - - private SendSyncBlockedListAction() { - } - - public static SendSyncBlockedListAction create() { - return INSTANCE; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendBlockedList(); - } -} - -class SendGroupInfoRequestAction implements HandleAction { - - private final SignalServiceAddress address; - private final GroupIdV1 groupId; - - public SendGroupInfoRequestAction(final SignalServiceAddress address, final GroupIdV1 groupId) { - this.address = address; - this.groupId = groupId; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendGroupInfoRequest(groupId, address); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final var that = (SendGroupInfoRequestAction) o; - - if (!address.equals(that.address)) return false; - return groupId.equals(that.groupId); - } - - @Override - public int hashCode() { - var result = address.hashCode(); - result = 31 * result + groupId.hashCode(); - return result; - } -} - -class SendGroupInfoAction implements HandleAction { - - private final SignalServiceAddress address; - private final GroupIdV1 groupId; - - public SendGroupInfoAction(final SignalServiceAddress address, final GroupIdV1 groupId) { - this.address = address; - this.groupId = groupId; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendGroupInfoMessage(groupId, address); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final var that = (SendGroupInfoAction) o; - - if (!address.equals(that.address)) return false; - return groupId.equals(that.groupId); - } - - @Override - public int hashCode() { - var result = address.hashCode(); - result = 31 * result + groupId.hashCode(); - return result; - } -} - -class RetrieveProfileAction implements HandleAction { - - private final RecipientId recipientId; - - public RetrieveProfileAction(final RecipientId recipientId) { - this.recipientId = recipientId; - } - - @Override - public void execute(Manager m) throws Throwable { - m.refreshRecipientProfile(recipientId); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final RetrieveProfileAction that = (RetrieveProfileAction) o; - - return recipientId.equals(that.recipientId); - } - - @Override - public int hashCode() { - return recipientId.hashCode(); - } -} - -class RenewSessionAction implements HandleAction { - - private final RecipientId recipientId; - - public RenewSessionAction(final RecipientId recipientId) { - this.recipientId = recipientId; - } - - @Override - public void execute(Manager m) throws Throwable { - m.renewSession(recipientId); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final RenewSessionAction that = (RenewSessionAction) o; - - return recipientId.equals(that.recipientId); - } - - @Override - public int hashCode() { - return recipientId.hashCode(); - } -} diff --git a/lib/src/main/java/org/asamk/signal/manager/JobExecutor.java b/lib/src/main/java/org/asamk/signal/manager/JobExecutor.java new file mode 100644 index 00000000..b86d7da1 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/JobExecutor.java @@ -0,0 +1,17 @@ +package org.asamk.signal.manager; + +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.jobs.Job; + +public class JobExecutor { + + private final Context context; + + public JobExecutor(final Context context) { + this.context = context; + } + + public void enqueueJob(Job job) { + job.run(context); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 936f625c..0d06dfba 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -16,6 +16,7 @@ */ package org.asamk.signal.manager; +import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -26,29 +27,26 @@ import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; -import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.AttachmentHelper; +import org.asamk.signal.manager.helper.ContactHelper; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; +import org.asamk.signal.manager.helper.IncomingMessageHandler; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.jobs.Context; -import org.asamk.signal.manager.jobs.Job; -import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; -import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.messageCache.CachedMessage; @@ -60,10 +58,7 @@ import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; -import org.signal.libsignal.metadata.ProtocolInvalidMessageException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; -import org.signal.zkgroup.InvalidInputException; -import org.signal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; @@ -85,10 +80,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemo import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; -import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -109,7 +102,6 @@ import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SignatureException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; @@ -146,19 +138,10 @@ public class Manager implements Closeable { private final SyncHelper syncHelper; private final AttachmentHelper attachmentHelper; private final GroupHelper groupHelper; + private final ContactHelper contactHelper; + private final IncomingMessageHandler incomingMessageHandler; - private final AvatarStore avatarStore; - private final AttachmentStore attachmentStore; - private final StickerPackStore stickerPackStore; - private final SignalSessionLock sessionLock = new SignalSessionLock() { - private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); - - @Override - public Lock acquire() { - LEGACY_LOCK.lock(); - return LEGACY_LOCK::unlock; - } - }; + private final Context context; Manager( SignalAccount account, @@ -173,6 +156,15 @@ public class Manager implements Closeable { account.getUsername(), account.getPassword(), account.getDeviceId()); + final var sessionLock = new SignalSessionLock() { + private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); + + @Override + public Lock acquire() { + LEGACY_LOCK.lock(); + return LEGACY_LOCK::unlock; + } + }; this.dependencies = new SignalDependencies(account.getSelfAddress(), serviceEnvironmentConfig, userAgent, @@ -180,9 +172,9 @@ public class Manager implements Closeable { account.getSignalProtocolStore(), executor, sessionLock); - this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); - this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); - this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); + final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); @@ -220,6 +212,7 @@ public class Manager implements Closeable { avatarStore, this::resolveSignalServiceAddress, this::resolveRecipient); + this.contactHelper = new ContactHelper(account); this.syncHelper = new SyncHelper(account, attachmentHelper, sendHelper, @@ -227,16 +220,31 @@ public class Manager implements Closeable { avatarStore, this::resolveSignalServiceAddress, this::resolveRecipient); + + this.context = new Context(account, + dependencies.getAccountManager(), + dependencies.getMessageReceiver(), + stickerPackStore, + sendHelper, + groupHelper, + syncHelper, + profileHelper); + var jobExecutor = new JobExecutor(context); + + this.incomingMessageHandler = new IncomingMessageHandler(account, + dependencies, + this::resolveRecipient, + groupHelper, + contactHelper, + attachmentHelper, + syncHelper, + jobExecutor); } public String getUsername() { return account.getUsername(); } - private SignalServiceAddress getSelfAddress() { - return account.getSelfAddress(); - } - public RecipientId getSelfRecipientId() { return account.getSelfRecipientId(); } @@ -326,15 +334,15 @@ public class Manager implements Closeable { } })); - // Note "contactDetails" has no optionals. It only gives us info on users who are registered - var contactDetails = getRegisteredUsers(canonicalizedNumbers.values() + // Note "registeredUsers" has no optionals. It only gives us info on users who are registered + var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values() .stream() .filter(s -> !s.isEmpty()) .collect(Collectors.toSet())); return numbers.stream().collect(Collectors.toMap(n -> n, n -> { final var number = canonicalizedNumbers.get(n); - final var uuid = contactDetails.get(number); + final var uuid = registeredUsers.get(number); return new Pair<>(number.isEmpty() ? null : number, uuid); })); } @@ -475,10 +483,6 @@ public class Manager implements Closeable { return profileHelper.getRecipientProfile(recipientId); } - public void refreshRecipientProfile(RecipientId recipientId) { - profileHelper.refreshRecipientProfile(recipientId); - } - public List getGroups() { return account.getGroupStore().getGroups(); } @@ -578,20 +582,6 @@ public class Manager implements Closeable { } } - SendGroupMessageResults sendGroupInfoMessage( - GroupIdV1 groupId, SignalServiceAddress recipient - ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { - final var recipientId = resolveRecipient(recipient); - return groupHelper.sendGroupInfoMessage(groupId, recipientId); - } - - SendGroupMessageResults sendGroupInfoRequest( - GroupIdV1 groupId, SignalServiceAddress recipient - ) throws IOException { - final var recipientId = resolveRecipient(recipient); - return groupHelper.sendGroupInfoRequest(groupId, recipientId); - } - public void sendReadReceipt( RecipientIdentifier.Single sender, List messageIds ) throws IOException, UntrustedIdentityException { @@ -612,16 +602,6 @@ public class Manager implements Closeable { sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); } - void sendDeliveryReceipt( - SignalServiceAddress remoteAddress, List messageIds - ) throws IOException, UntrustedIdentityException { - var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, - messageIds, - System.currentTimeMillis()); - - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); - } - public SendMessageResults sendMessage( Message message, Set recipients ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { @@ -674,56 +654,38 @@ public class Manager implements Closeable { throw new AssertionError(e); } finally { for (var recipient : recipients) { - final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); - handleEndSession(recipientId); + final var recipientId = resolveRecipient(recipient); + account.getSessionStore().deleteAllSessions(recipientId); } } } - void renewSession(RecipientId recipientId) throws IOException { - account.getSessionStore().archiveSessions(recipientId); - if (!recipientId.equals(getSelfRecipientId())) { - sendHelper.sendNullMessage(recipientId); - } - } - public void setContactName( RecipientIdentifier.Single recipient, String name ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - final var recipientId = resolveRecipient(recipient); - var contact = account.getContactStore().getContact(recipientId); - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - account.getContactStore().storeContact(recipientId, builder.withName(name).build()); + contactHelper.setContactName(resolveRecipient(recipient), name); } public void setContactBlocked( RecipientIdentifier.Single recipient, boolean blocked - ) throws NotMasterDeviceException { + ) throws NotMasterDeviceException, IOException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - setContactBlocked(resolveRecipient(recipient), blocked); - } - - private void setContactBlocked(RecipientId recipientId, boolean blocked) { - var contact = account.getContactStore().getContact(recipientId); - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + contactHelper.setContactBlocked(resolveRecipient(recipient), blocked); // TODO cycle our profile key - account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); + syncHelper.sendBlockedList(); } - public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { - var group = getGroup(groupId); - if (group == null) { - throw new GroupNotFoundException(groupId); - } - - group.setBlocked(blocked); + public void setGroupBlocked( + final GroupId groupId, final boolean blocked + ) throws GroupNotFoundException, IOException { + groupHelper.setGroupBlocked(groupId, blocked); // TODO cycle our profile key - account.getGroupStore().updateGroup(group); + syncHelper.sendBlockedList(); } /** @@ -733,7 +695,7 @@ public class Manager implements Closeable { RecipientIdentifier.Single recipient, int messageExpirationTimer ) throws IOException { var recipientId = resolveRecipient(recipient); - setExpirationTimer(recipientId, messageExpirationTimer); + contactHelper.setExpirationTimer(recipientId, messageExpirationTimer); final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); try { sendMessage(messageBuilder, Set.of(recipient)); @@ -742,16 +704,6 @@ public class Manager implements Closeable { } } - private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { - var contact = account.getContactStore().getContact(recipientId); - if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { - return; - } - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - account.getContactStore() - .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); - } - /** * Upload the sticker pack from path. * @@ -875,162 +827,6 @@ public class Manager implements Closeable { sendTypingMessage(action.toSignalService(), recipients); } - private void handleEndSession(RecipientId recipientId) { - account.getSessionStore().deleteAllSessions(recipientId); - } - - private List handleSignalServiceDataMessage( - SignalServiceDataMessage message, - boolean isSync, - SignalServiceAddress source, - SignalServiceAddress destination, - boolean ignoreAttachments - ) { - var actions = new ArrayList(); - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - var groupInfo = message.getGroupContext().get().getGroupV1().get(); - var groupId = GroupId.v1(groupInfo.getGroupId()); - var group = getGroup(groupId); - if (group == null || group instanceof GroupInfoV1) { - var groupV1 = (GroupInfoV1) group; - switch (groupInfo.getType()) { - case UPDATE: { - if (groupV1 == null) { - groupV1 = new GroupInfoV1(groupId); - } - - if (groupInfo.getAvatar().isPresent()) { - var avatar = groupInfo.getAvatar().get(); - groupHelper.downloadGroupAvatar(groupV1.getGroupId(), avatar); - } - - if (groupInfo.getName().isPresent()) { - groupV1.name = groupInfo.getName().get(); - } - - if (groupInfo.getMembers().isPresent()) { - groupV1.addMembers(groupInfo.getMembers() - .get() - .stream() - .map(this::resolveRecipient) - .collect(Collectors.toSet())); - } - - account.getGroupStore().updateGroup(groupV1); - break; - } - case DELIVER: - if (groupV1 == null && !isSync) { - actions.add(new SendGroupInfoRequestAction(source, groupId)); - } - break; - case QUIT: { - if (groupV1 != null) { - groupV1.removeMember(resolveRecipient(source)); - account.getGroupStore().updateGroup(groupV1); - } - break; - } - case REQUEST_INFO: - if (groupV1 != null && !isSync) { - actions.add(new SendGroupInfoAction(source, groupV1.getGroupId())); - } - break; - } - } else { - // Received a group v1 message for a v2 group - } - } - if (message.getGroupContext().get().getGroupV2().isPresent()) { - final var groupContext = message.getGroupContext().get().getGroupV2().get(); - final var groupMasterKey = groupContext.getMasterKey(); - - groupHelper.getOrMigrateGroup(groupMasterKey, - groupContext.getRevision(), - groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); - } - } - - final var conversationPartnerAddress = isSync ? destination : source; - if (conversationPartnerAddress != null && message.isEndSession()) { - handleEndSession(resolveRecipient(conversationPartnerAddress)); - } - if (message.isExpirationUpdate() || message.getBody().isPresent()) { - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - var groupInfo = message.getGroupContext().get().getGroupV1().get(); - var group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId())); - if (group != null) { - if (group.messageExpirationTime != message.getExpiresInSeconds()) { - group.messageExpirationTime = message.getExpiresInSeconds(); - account.getGroupStore().updateGroup(group); - } - } - } else if (message.getGroupContext().get().getGroupV2().isPresent()) { - // disappearing message timer already stored in the DecryptedGroup - } - } else if (conversationPartnerAddress != null) { - setExpirationTimer(resolveRecipient(conversationPartnerAddress), message.getExpiresInSeconds()); - } - } - if (!ignoreAttachments) { - if (message.getAttachments().isPresent()) { - for (var attachment : message.getAttachments().get()) { - attachmentHelper.downloadAttachment(attachment); - } - } - if (message.getSharedContacts().isPresent()) { - for (var contact : message.getSharedContacts().get()) { - if (contact.getAvatar().isPresent()) { - attachmentHelper.downloadAttachment(contact.getAvatar().get().getAttachment()); - } - } - } - if (message.getPreviews().isPresent()) { - final var previews = message.getPreviews().get(); - for (var preview : previews) { - if (preview.getImage().isPresent()) { - attachmentHelper.downloadAttachment(preview.getImage().get()); - } - } - } - if (message.getQuote().isPresent()) { - final var quote = message.getQuote().get(); - - for (var quotedAttachment : quote.getAttachments()) { - final var thumbnail = quotedAttachment.getThumbnail(); - if (thumbnail != null) { - attachmentHelper.downloadAttachment(thumbnail); - } - } - } - } - if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { - final ProfileKey profileKey; - try { - profileKey = new ProfileKey(message.getProfileKey().get()); - } catch (InvalidInputException e) { - throw new AssertionError(e); - } - if (source.matches(account.getSelfAddress())) { - this.account.setProfileKey(profileKey); - } - this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey); - } - if (message.getSticker().isPresent()) { - final var messageSticker = message.getSticker().get(); - final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId()); - var sticker = account.getStickerStore().getSticker(stickerPackId); - if (sticker == null) { - sticker = new Sticker(stickerPackId, messageSticker.getPackKey()); - account.getStickerStore().updateSticker(sticker); - } - enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); - } - return actions; - } - private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { Set queuedActions = new HashSet<>(); for (var cachedMessage : account.getMessageCache().getCachedMessages()) { @@ -1070,7 +866,7 @@ public class Manager implements Closeable { cachedMessage.delete(); return null; } - actions = handleMessage(envelope, content, ignoreAttachments); + actions = incomingMessageHandler.handleMessage(envelope, content, ignoreAttachments); } handler.handleMessage(envelope, content, null); cachedMessage.delete(); @@ -1095,8 +891,6 @@ public class Manager implements Closeable { while (!Thread.interrupted()) { SignalServiceEnvelope envelope; - SignalServiceContent content = null; - Exception exception = null; final CachedMessage[] cachedMessage = {null}; account.setLastReceiveTimestamp(System.currentTimeMillis()); logger.debug("Checking for new message from server"); @@ -1137,58 +931,17 @@ public class Manager implements Closeable { continue; } - if (envelope.hasSource()) { - // Store uuid if we don't have it already - // address/uuid in envelope is sent by server - resolveRecipientTrusted(envelope.getSourceAddress()); - } - if (!envelope.isReceipt()) { - try { - content = dependencies.getCipher().decrypt(envelope); - } catch (Exception e) { - exception = e; - } - if (!envelope.hasSource() && content != null) { - // Store uuid if we don't have it already - // address/uuid is validated by unidentified sender certificate - resolveRecipientTrusted(content.getSender()); - } - var actions = handleMessage(envelope, content, ignoreAttachments); - if (exception instanceof ProtocolInvalidMessageException) { - final var sender = resolveRecipient(((ProtocolInvalidMessageException) exception).getSender()); - logger.debug("Received invalid message, queuing renew session action."); - actions.add(new RenewSessionAction(sender)); - } - if (hasCaughtUpWithOldMessages) { - for (var action : actions) { - try { - action.execute(this); - } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - logger.warn("Message action failed.", e); - } - } - } else { - queuedActions.addAll(actions); - } - } - final var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content); - if (isMessageBlocked(envelope, content)) { - logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); - } else if (notAllowedToSendToGroup) { - logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", - (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), - envelope.getTimestamp()); - } else { - handler.handleMessage(envelope, content, exception); + final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); + queuedActions.addAll(result.first()); + final var exception = result.second(); + + if (hasCaughtUpWithOldMessages) { + handleQueuedActions(queuedActions); } if (cachedMessage[0] != null) { if (exception instanceof ProtocolUntrustedIdentityException) { final var identifier = ((ProtocolUntrustedIdentityException) exception).getSender(); final var recipientId = resolveRecipient(identifier); - queuedActions.add(new RetrieveProfileAction(recipientId)); if (!envelope.hasSource()) { try { cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); @@ -1205,10 +958,10 @@ public class Manager implements Closeable { handleQueuedActions(queuedActions); } - private void handleQueuedActions(final Set queuedActions) { + private void handleQueuedActions(final Collection queuedActions) { for (var action : queuedActions) { try { - action.execute(this); + action.execute(context); } catch (Throwable e) { if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { Thread.currentThread().interrupt(); @@ -1218,252 +971,19 @@ public class Manager implements Closeable { } } - private boolean isMessageBlocked( - SignalServiceEnvelope envelope, SignalServiceContent content - ) { - SignalServiceAddress source; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - source = envelope.getSourceAddress(); - } else if (content != null) { - source = content.getSender(); - } else { - return false; - } - final var recipientId = resolveRecipient(source); - if (isContactBlocked(recipientId)) { - return true; - } - - if (content != null && content.getDataMessage().isPresent()) { - var message = content.getDataMessage().get(); - if (message.getGroupContext().isPresent()) { - var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); - var group = getGroup(groupId); - if (group != null && group.isBlocked()) { - return true; - } - } - } - return false; - } - public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { final var recipientId = resolveRecipient(recipient); - return isContactBlocked(recipientId); - } - - private boolean isContactBlocked(final RecipientId recipientId) { - var sourceContact = account.getContactStore().getContact(recipientId); - return sourceContact != null && sourceContact.isBlocked(); - } - - private boolean isNotAllowedToSendToGroup( - SignalServiceEnvelope envelope, SignalServiceContent content - ) { - SignalServiceAddress source; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - source = envelope.getSourceAddress(); - } else if (content != null) { - source = content.getSender(); - } else { - return false; - } - - if (content == null || !content.getDataMessage().isPresent()) { - return false; - } - - var message = content.getDataMessage().get(); - if (!message.getGroupContext().isPresent()) { - return false; - } - - if (message.getGroupContext().get().getGroupV1().isPresent()) { - var groupInfo = message.getGroupContext().get().getGroupV1().get(); - if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) { - return false; - } - } - - var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); - var group = getGroup(groupId); - if (group == null) { - return false; - } - - final var recipientId = resolveRecipient(source); - if (!group.isMember(recipientId)) { - return true; - } - - if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { - return message.getBody().isPresent() - || message.getAttachments().isPresent() - || message.getQuote() - .isPresent() - || message.getPreviews().isPresent() - || message.getMentions().isPresent() - || message.getSticker().isPresent(); - } - return false; - } - - private List handleMessage( - SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments - ) { - var actions = new ArrayList(); - if (content != null) { - final SignalServiceAddress sender; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - sender = envelope.getSourceAddress(); - } else { - sender = content.getSender(); - } - - if (content.getDataMessage().isPresent()) { - var message = content.getDataMessage().get(); - - if (content.isNeedsReceipt()) { - actions.add(new SendReceiptAction(sender, message.getTimestamp())); - } - - actions.addAll(handleSignalServiceDataMessage(message, - false, - sender, - account.getSelfAddress(), - ignoreAttachments)); - } - if (content.getSyncMessage().isPresent()) { - account.setMultiDevice(true); - var syncMessage = content.getSyncMessage().get(); - if (syncMessage.getSent().isPresent()) { - var message = syncMessage.getSent().get(); - final var destination = message.getDestination().orNull(); - actions.addAll(handleSignalServiceDataMessage(message.getMessage(), - true, - sender, - destination, - ignoreAttachments)); - } - if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { - var rm = syncMessage.getRequest().get(); - if (rm.isContactsRequest()) { - actions.add(SendSyncContactsAction.create()); - } - if (rm.isGroupsRequest()) { - actions.add(SendSyncGroupsAction.create()); - } - if (rm.isBlockedListRequest()) { - actions.add(SendSyncBlockedListAction.create()); - } - // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); - } - if (syncMessage.getGroups().isPresent()) { - try { - final var groupsMessage = syncMessage.getGroups().get(); - attachmentHelper.retrieveAttachment(groupsMessage, syncHelper::handleSyncDeviceGroups); - } catch (Exception e) { - logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage()); - } - } - if (syncMessage.getBlockedList().isPresent()) { - final var blockedListMessage = syncMessage.getBlockedList().get(); - for (var address : blockedListMessage.getAddresses()) { - setContactBlocked(resolveRecipient(address), true); - } - for (var groupId : blockedListMessage.getGroupIds() - .stream() - .map(GroupId::unknownVersion) - .collect(Collectors.toSet())) { - try { - setGroupBlocked(groupId, true); - } catch (GroupNotFoundException e) { - logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", - groupId.toBase64()); - } - } - } - if (syncMessage.getContacts().isPresent()) { - try { - final var contactsMessage = syncMessage.getContacts().get(); - attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(), - syncHelper::handleSyncDeviceContacts); - } catch (Exception e) { - logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); - } - } - if (syncMessage.getVerified().isPresent()) { - final var verifiedMessage = syncMessage.getVerified().get(); - account.getIdentityKeyStore() - .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()), - verifiedMessage.getIdentityKey(), - TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); - } - if (syncMessage.getStickerPackOperations().isPresent()) { - final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); - for (var m : stickerPackOperationMessages) { - if (!m.getPackId().isPresent()) { - continue; - } - final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); - final var installed = !m.getType().isPresent() - || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; - - var sticker = account.getStickerStore().getSticker(stickerPackId); - if (m.getPackKey().isPresent()) { - if (sticker == null) { - sticker = new Sticker(stickerPackId, m.getPackKey().get()); - } - if (installed) { - enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); - } - } - - if (sticker != null) { - sticker.setInstalled(installed); - account.getStickerStore().updateSticker(sticker); - } - } - } - if (syncMessage.getFetchType().isPresent()) { - switch (syncMessage.getFetchType().get()) { - case LOCAL_PROFILE: - actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); - case STORAGE_MANIFEST: - // TODO - } - } - if (syncMessage.getKeys().isPresent()) { - final var keysMessage = syncMessage.getKeys().get(); - if (keysMessage.getStorageService().isPresent()) { - final var storageKey = keysMessage.getStorageService().get(); - account.setStorageKey(storageKey); - } - } - if (syncMessage.getConfiguration().isPresent()) { - // TODO - } - } - } - return actions; + return contactHelper.isContactBlocked(recipientId); } public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { - return attachmentStore.getAttachmentFile(attachmentId); - } - - void sendGroups() throws IOException { - syncHelper.sendGroups(); + return attachmentHelper.getAttachmentFile(attachmentId); } public void sendContacts() throws IOException { syncHelper.sendContacts(); } - void sendBlockedList() throws IOException { - syncHelper.sendBlockedList(); - } - public List> getContacts() { return account.getContactStore().getContacts(); } @@ -1471,7 +991,7 @@ public class Manager implements Closeable { public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { final var recipientId = resolveRecipient(recipientIdentifier); - final var contact = account.getRecipientStore().getContact(recipientId); + final var contact = account.getContactStore().getContact(recipientId); if (contact != null && !Util.isEmpty(contact.getName())) { return contact.getName(); } @@ -1587,7 +1107,7 @@ public class Manager implements Closeable { } } else { // Retrieve profile to get the current identity key from the server - refreshRecipientProfile(recipientId); + profileHelper.refreshRecipientProfile(recipientId); } } @@ -1660,20 +1180,12 @@ public class Manager implements Closeable { return account.getRecipientStore().resolveRecipientTrusted(address); } - private void enqueueJob(Job job) { - var context = new Context(account, - dependencies.getAccountManager(), - dependencies.getMessageReceiver(), - stickerPackStore); - job.run(context); - } - @Override public void close() throws IOException { close(true); } - void close(boolean closeAccount) throws IOException { + private void close(boolean closeAccount) throws IOException { executor.shutdown(); dependencies.getSignalWebSocket().disconnect(); diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/HandleAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/HandleAction.java new file mode 100644 index 00000000..cfe13bce --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/HandleAction.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public interface HandleAction { + + void execute(Context context) throws Throwable; +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java new file mode 100644 index 00000000..07194cd0 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java @@ -0,0 +1,36 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class RenewSessionAction implements HandleAction { + + private final RecipientId recipientId; + + public RenewSessionAction(final RecipientId recipientId) { + this.recipientId = recipientId; + } + + @Override + public void execute(Context context) throws Throwable { + context.getAccount().getSessionStore().archiveSessions(recipientId); + if (!recipientId.equals(context.getAccount().getSelfRecipientId())) { + context.getSendHelper().sendNullMessage(recipientId); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final RenewSessionAction that = (RenewSessionAction) o; + + return recipientId.equals(that.recipientId); + } + + @Override + public int hashCode() { + return recipientId.hashCode(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveProfileAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveProfileAction.java new file mode 100644 index 00000000..329e7cd2 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveProfileAction.java @@ -0,0 +1,33 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class RetrieveProfileAction implements HandleAction { + + private final RecipientId recipientId; + + public RetrieveProfileAction(final RecipientId recipientId) { + this.recipientId = recipientId; + } + + @Override + public void execute(Context context) throws Throwable { + context.getProfileHelper().refreshRecipientProfile(recipientId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final RetrieveProfileAction that = (RetrieveProfileAction) o; + + return recipientId.equals(that.recipientId); + } + + @Override + public int hashCode() { + return recipientId.hashCode(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoAction.java new file mode 100644 index 00000000..6f66ceeb --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoAction.java @@ -0,0 +1,39 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class SendGroupInfoAction implements HandleAction { + + private final RecipientId recipientId; + private final GroupIdV1 groupId; + + public SendGroupInfoAction(final RecipientId recipientId, final GroupIdV1 groupId) { + this.recipientId = recipientId; + this.groupId = groupId; + } + + @Override + public void execute(Context context) throws Throwable { + context.getGroupHelper().sendGroupInfoMessage(groupId, recipientId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final var that = (SendGroupInfoAction) o; + + if (!recipientId.equals(that.recipientId)) return false; + return groupId.equals(that.groupId); + } + + @Override + public int hashCode() { + var result = recipientId.hashCode(); + result = 31 * result + groupId.hashCode(); + return result; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoRequestAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoRequestAction.java new file mode 100644 index 00000000..4ded0a31 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoRequestAction.java @@ -0,0 +1,39 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class SendGroupInfoRequestAction implements HandleAction { + + private final RecipientId recipientId; + private final GroupIdV1 groupId; + + public SendGroupInfoRequestAction(final RecipientId recipientId, final GroupIdV1 groupId) { + this.recipientId = recipientId; + this.groupId = groupId; + } + + @Override + public void execute(Context context) throws Throwable { + context.getGroupHelper().sendGroupInfoRequest(groupId, recipientId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final var that = (SendGroupInfoRequestAction) o; + + if (!recipientId.equals(that.recipientId)) return false; + return groupId.equals(that.groupId); + } + + @Override + public int hashCode() { + var result = recipientId.hashCode(); + result = 31 * result + groupId.hashCode(); + return result; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java new file mode 100644 index 00000000..8341304c --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java @@ -0,0 +1,36 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +import java.util.List; +import java.util.Objects; + +public class SendReceiptAction implements HandleAction { + + private final RecipientId recipientId; + private final long timestamp; + + public SendReceiptAction(final RecipientId recipientId, final long timestamp) { + this.recipientId = recipientId; + this.timestamp = timestamp; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSendHelper().sendDeliveryReceipt(recipientId, List.of(timestamp)); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final var that = (SendReceiptAction) o; + return timestamp == that.timestamp && recipientId.equals(that.recipientId); + } + + @Override + public int hashCode() { + return Objects.hash(recipientId, timestamp); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncBlockedListAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncBlockedListAction.java new file mode 100644 index 00000000..4aea9e69 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncBlockedListAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class SendSyncBlockedListAction implements HandleAction { + + private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction(); + + private SendSyncBlockedListAction() { + } + + public static SendSyncBlockedListAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSyncHelper().sendBlockedList(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncContactsAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncContactsAction.java new file mode 100644 index 00000000..f590e982 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncContactsAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class SendSyncContactsAction implements HandleAction { + + private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction(); + + private SendSyncContactsAction() { + } + + public static SendSyncContactsAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSyncHelper().sendContacts(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncGroupsAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncGroupsAction.java new file mode 100644 index 00000000..3f18732f --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncGroupsAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class SendSyncGroupsAction implements HandleAction { + + private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction(); + + private SendSyncGroupsAction() { + } + + public static SendSyncGroupsAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSyncHelper().sendGroups(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index 88a611b9..d3931955 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import java.io.File; @@ -35,6 +36,10 @@ public class AttachmentHelper { this.attachmentStore = attachmentStore; } + public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { + return attachmentStore.getAttachmentFile(attachmentId); + } + public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException { var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java new file mode 100644 index 00000000..71b2ded8 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java @@ -0,0 +1,41 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class ContactHelper { + + private final SignalAccount account; + + public ContactHelper(final SignalAccount account) { + this.account = account; + } + + public boolean isContactBlocked(final RecipientId recipientId) { + var sourceContact = account.getContactStore().getContact(recipientId); + return sourceContact != null && sourceContact.isBlocked(); + } + + public void setContactName(final RecipientId recipientId, final String name) { + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore().storeContact(recipientId, builder.withName(name).build()); + } + + public void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { + var contact = account.getContactStore().getContact(recipientId); + if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { + return; + } + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore() + .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); + } + + public void setContactBlocked(RecipientId recipientId, boolean blocked) { + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 5566d9d7..3ddd6edd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -91,6 +91,11 @@ public class GroupHelper { return getGroup(groupId, false); } + public boolean isGroupBlocked(final GroupId groupId) { + var group = getGroup(groupId); + return group != null && group.isBlocked(); + } + public void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { try { avatarStore.storeGroupAvatar(groupId, @@ -300,6 +305,16 @@ public class GroupHelper { avatarStore.deleteGroupAvatar(groupId); } + public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { + var group = getGroup(groupId); + if (group == null) { + throw new GroupNotFoundException(groupId); + } + + group.setBlocked(blocked); + account.getGroupStore().updateGroup(group); + } + public SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, RecipientId recipientId ) throws IOException { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java new file mode 100644 index 00000000..369d3205 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -0,0 +1,492 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.JobExecutor; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.actions.HandleAction; +import org.asamk.signal.manager.actions.RenewSessionAction; +import org.asamk.signal.manager.actions.RetrieveProfileAction; +import org.asamk.signal.manager.actions.SendGroupInfoAction; +import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; +import org.asamk.signal.manager.actions.SendReceiptAction; +import org.asamk.signal.manager.actions.SendSyncBlockedListAction; +import org.asamk.signal.manager.actions.SendSyncContactsAction; +import org.asamk.signal.manager.actions.SendSyncGroupsAction; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.asamk.signal.manager.storage.stickers.Sticker; +import org.asamk.signal.manager.storage.stickers.StickerPackId; +import org.signal.libsignal.metadata.ProtocolInvalidMessageException; +import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public final class IncomingMessageHandler { + + private final static Logger logger = LoggerFactory.getLogger(IncomingMessageHandler.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final RecipientResolver recipientResolver; + private final GroupHelper groupHelper; + private final ContactHelper contactHelper; + private final AttachmentHelper attachmentHelper; + private final SyncHelper syncHelper; + private final JobExecutor jobExecutor; + + public IncomingMessageHandler( + final SignalAccount account, + final SignalDependencies dependencies, + final RecipientResolver recipientResolver, + final GroupHelper groupHelper, + final ContactHelper contactHelper, + final AttachmentHelper attachmentHelper, + final SyncHelper syncHelper, + final JobExecutor jobExecutor + ) { + this.account = account; + this.dependencies = dependencies; + this.recipientResolver = recipientResolver; + this.groupHelper = groupHelper; + this.contactHelper = contactHelper; + this.attachmentHelper = attachmentHelper; + this.syncHelper = syncHelper; + this.jobExecutor = jobExecutor; + } + + public Pair, Exception> handleEnvelope( + final SignalServiceEnvelope envelope, + final boolean ignoreAttachments, + final Manager.ReceiveMessageHandler handler + ) { + final var actions = new ArrayList(); + if (envelope.hasSource()) { + // Store uuid if we don't have it already + // address/uuid in envelope is sent by server + account.getRecipientStore().resolveRecipientTrusted(envelope.getSourceAddress()); + } + SignalServiceContent content = null; + Exception exception = null; + if (!envelope.isReceipt()) { + try { + content = dependencies.getCipher().decrypt(envelope); + } catch (ProtocolUntrustedIdentityException e) { + final var recipientId = account.getRecipientStore().resolveRecipient(e.getSender()); + actions.add(new RetrieveProfileAction(recipientId)); + } catch (ProtocolInvalidMessageException e) { + final var sender = account.getRecipientStore().resolveRecipient(e.getSender()); + logger.debug("Received invalid message, queuing renew session action."); + actions.add(new RenewSessionAction(sender)); + exception = e; + } catch (Exception e) { + exception = e; + } + + if (!envelope.hasSource() && content != null) { + // Store uuid if we don't have it already + // address/uuid is validated by unidentified sender certificate + account.getRecipientStore().resolveRecipientTrusted(content.getSender()); + } + + actions.addAll(handleMessage(envelope, content, ignoreAttachments)); + } + + if (isMessageBlocked(envelope, content)) { + logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); + } else if (isNotAllowedToSendToGroup(envelope, content)) { + logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", + (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), + envelope.getTimestamp()); + } else { + handler.handleMessage(envelope, content, exception); + } + return new Pair<>(actions, exception); + } + + public List handleMessage( + SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments + ) { + var actions = new ArrayList(); + if (content != null) { + final RecipientId sender; + if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { + sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); + } else { + sender = recipientResolver.resolveRecipient(content.getSender()); + } + + if (content.getDataMessage().isPresent()) { + var message = content.getDataMessage().get(); + + if (content.isNeedsReceipt()) { + actions.add(new SendReceiptAction(sender, message.getTimestamp())); + } + + actions.addAll(handleSignalServiceDataMessage(message, + false, + sender, + account.getSelfRecipientId(), + ignoreAttachments)); + } + if (content.getSyncMessage().isPresent()) { + account.setMultiDevice(true); + var syncMessage = content.getSyncMessage().get(); + if (syncMessage.getSent().isPresent()) { + var message = syncMessage.getSent().get(); + final var destination = message.getDestination().orNull(); + actions.addAll(handleSignalServiceDataMessage(message.getMessage(), + true, + sender, + destination == null ? null : recipientResolver.resolveRecipient(destination), + ignoreAttachments)); + } + if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { + var rm = syncMessage.getRequest().get(); + if (rm.isContactsRequest()) { + actions.add(SendSyncContactsAction.create()); + } + if (rm.isGroupsRequest()) { + actions.add(SendSyncGroupsAction.create()); + } + if (rm.isBlockedListRequest()) { + actions.add(SendSyncBlockedListAction.create()); + } + // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); + } + if (syncMessage.getGroups().isPresent()) { + try { + final var groupsMessage = syncMessage.getGroups().get(); + attachmentHelper.retrieveAttachment(groupsMessage, syncHelper::handleSyncDeviceGroups); + } catch (Exception e) { + logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage()); + } + } + if (syncMessage.getBlockedList().isPresent()) { + final var blockedListMessage = syncMessage.getBlockedList().get(); + for (var address : blockedListMessage.getAddresses()) { + contactHelper.setContactBlocked(recipientResolver.resolveRecipient(address), true); + } + for (var groupId : blockedListMessage.getGroupIds() + .stream() + .map(GroupId::unknownVersion) + .collect(Collectors.toSet())) { + try { + groupHelper.setGroupBlocked(groupId, true); + } catch (GroupNotFoundException e) { + logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", + groupId.toBase64()); + } + } + } + if (syncMessage.getContacts().isPresent()) { + try { + final var contactsMessage = syncMessage.getContacts().get(); + attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(), + syncHelper::handleSyncDeviceContacts); + } catch (Exception e) { + logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); + } + } + if (syncMessage.getVerified().isPresent()) { + final var verifiedMessage = syncMessage.getVerified().get(); + account.getIdentityKeyStore() + .setIdentityTrustLevel(account.getRecipientStore() + .resolveRecipientTrusted(verifiedMessage.getDestination()), + verifiedMessage.getIdentityKey(), + TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + } + if (syncMessage.getStickerPackOperations().isPresent()) { + final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); + for (var m : stickerPackOperationMessages) { + if (!m.getPackId().isPresent()) { + continue; + } + final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); + final var installed = !m.getType().isPresent() + || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; + + var sticker = account.getStickerStore().getSticker(stickerPackId); + if (m.getPackKey().isPresent()) { + if (sticker == null) { + sticker = new Sticker(stickerPackId, m.getPackKey().get()); + } + if (installed) { + jobExecutor.enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); + } + } + + if (sticker != null) { + sticker.setInstalled(installed); + account.getStickerStore().updateSticker(sticker); + } + } + } + if (syncMessage.getFetchType().isPresent()) { + switch (syncMessage.getFetchType().get()) { + case LOCAL_PROFILE: + actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); + case STORAGE_MANIFEST: + // TODO + } + } + if (syncMessage.getKeys().isPresent()) { + final var keysMessage = syncMessage.getKeys().get(); + if (keysMessage.getStorageService().isPresent()) { + final var storageKey = keysMessage.getStorageService().get(); + account.setStorageKey(storageKey); + } + } + if (syncMessage.getConfiguration().isPresent()) { + // TODO + } + } + } + return actions; + } + + private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) { + SignalServiceAddress source; + if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { + source = envelope.getSourceAddress(); + } else if (content != null) { + source = content.getSender(); + } else { + return false; + } + final var recipientId = recipientResolver.resolveRecipient(source); + if (contactHelper.isContactBlocked(recipientId)) { + return true; + } + + if (content != null && content.getDataMessage().isPresent()) { + var message = content.getDataMessage().get(); + if (message.getGroupContext().isPresent()) { + var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + return groupHelper.isGroupBlocked(groupId); + } + } + + return false; + } + + private boolean isNotAllowedToSendToGroup(SignalServiceEnvelope envelope, SignalServiceContent content) { + SignalServiceAddress source; + if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { + source = envelope.getSourceAddress(); + } else if (content != null) { + source = content.getSender(); + } else { + return false; + } + + if (content == null || !content.getDataMessage().isPresent()) { + return false; + } + + var message = content.getDataMessage().get(); + if (!message.getGroupContext().isPresent()) { + return false; + } + + if (message.getGroupContext().get().getGroupV1().isPresent()) { + var groupInfo = message.getGroupContext().get().getGroupV1().get(); + if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) { + return false; + } + } + + var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + var group = groupHelper.getGroup(groupId); + if (group == null) { + return false; + } + + final var recipientId = recipientResolver.resolveRecipient(source); + if (!group.isMember(recipientId)) { + return true; + } + + if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { + return message.getBody().isPresent() + || message.getAttachments().isPresent() + || message.getQuote() + .isPresent() + || message.getPreviews().isPresent() + || message.getMentions().isPresent() + || message.getSticker().isPresent(); + } + return false; + } + + private List handleSignalServiceDataMessage( + SignalServiceDataMessage message, + boolean isSync, + RecipientId source, + RecipientId destination, + boolean ignoreAttachments + ) { + var actions = new ArrayList(); + if (message.getGroupContext().isPresent()) { + if (message.getGroupContext().get().getGroupV1().isPresent()) { + var groupInfo = message.getGroupContext().get().getGroupV1().get(); + var groupId = GroupId.v1(groupInfo.getGroupId()); + var group = groupHelper.getGroup(groupId); + if (group == null || group instanceof GroupInfoV1) { + var groupV1 = (GroupInfoV1) group; + switch (groupInfo.getType()) { + case UPDATE: { + if (groupV1 == null) { + groupV1 = new GroupInfoV1(groupId); + } + + if (groupInfo.getAvatar().isPresent()) { + var avatar = groupInfo.getAvatar().get(); + groupHelper.downloadGroupAvatar(groupV1.getGroupId(), avatar); + } + + if (groupInfo.getName().isPresent()) { + groupV1.name = groupInfo.getName().get(); + } + + if (groupInfo.getMembers().isPresent()) { + groupV1.addMembers(groupInfo.getMembers() + .get() + .stream() + .map(recipientResolver::resolveRecipient) + .collect(Collectors.toSet())); + } + + account.getGroupStore().updateGroup(groupV1); + break; + } + case DELIVER: + if (groupV1 == null && !isSync) { + actions.add(new SendGroupInfoRequestAction(source, groupId)); + } + break; + case QUIT: { + if (groupV1 != null) { + groupV1.removeMember(source); + account.getGroupStore().updateGroup(groupV1); + } + break; + } + case REQUEST_INFO: + if (groupV1 != null && !isSync) { + actions.add(new SendGroupInfoAction(source, groupV1.getGroupId())); + } + break; + } + } else { + // Received a group v1 message for a v2 group + } + } + if (message.getGroupContext().get().getGroupV2().isPresent()) { + final var groupContext = message.getGroupContext().get().getGroupV2().get(); + final var groupMasterKey = groupContext.getMasterKey(); + + groupHelper.getOrMigrateGroup(groupMasterKey, + groupContext.getRevision(), + groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); + } + } + + final var conversationPartnerAddress = isSync ? destination : source; + if (conversationPartnerAddress != null && message.isEndSession()) { + account.getSessionStore().deleteAllSessions(conversationPartnerAddress); + } + if (message.isExpirationUpdate() || message.getBody().isPresent()) { + if (message.getGroupContext().isPresent()) { + if (message.getGroupContext().get().getGroupV1().isPresent()) { + var groupInfo = message.getGroupContext().get().getGroupV1().get(); + var group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId())); + if (group != null) { + if (group.messageExpirationTime != message.getExpiresInSeconds()) { + group.messageExpirationTime = message.getExpiresInSeconds(); + account.getGroupStore().updateGroup(group); + } + } + } else if (message.getGroupContext().get().getGroupV2().isPresent()) { + // disappearing message timer already stored in the DecryptedGroup + } + } else if (conversationPartnerAddress != null) { + contactHelper.setExpirationTimer(conversationPartnerAddress, message.getExpiresInSeconds()); + } + } + if (!ignoreAttachments) { + if (message.getAttachments().isPresent()) { + for (var attachment : message.getAttachments().get()) { + attachmentHelper.downloadAttachment(attachment); + } + } + if (message.getSharedContacts().isPresent()) { + for (var contact : message.getSharedContacts().get()) { + if (contact.getAvatar().isPresent()) { + attachmentHelper.downloadAttachment(contact.getAvatar().get().getAttachment()); + } + } + } + if (message.getPreviews().isPresent()) { + final var previews = message.getPreviews().get(); + for (var preview : previews) { + if (preview.getImage().isPresent()) { + attachmentHelper.downloadAttachment(preview.getImage().get()); + } + } + } + if (message.getQuote().isPresent()) { + final var quote = message.getQuote().get(); + + for (var quotedAttachment : quote.getAttachments()) { + final var thumbnail = quotedAttachment.getThumbnail(); + if (thumbnail != null) { + attachmentHelper.downloadAttachment(thumbnail); + } + } + } + } + if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { + final ProfileKey profileKey; + try { + profileKey = new ProfileKey(message.getProfileKey().get()); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + if (account.getSelfRecipientId().equals(source)) { + this.account.setProfileKey(profileKey); + } + this.account.getProfileStore().storeProfileKey(source, profileKey); + } + if (message.getSticker().isPresent()) { + final var messageSticker = message.getSticker().get(); + final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId()); + var sticker = account.getStickerStore().getSticker(stickerPackId); + if (sticker == null) { + sticker = new Sticker(stickerPackId, messageSticker.getPackKey()); + account.getStickerStore().updateSticker(sticker); + } + jobExecutor.enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); + } + return actions; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index f92d7bde..058a04f2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -131,6 +131,16 @@ public class SendHelper { return result; } + public void sendDeliveryReceipt( + RecipientId recipientId, List messageIds + ) throws IOException, UntrustedIdentityException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, + messageIds, + System.currentTimeMillis()); + + sendReceiptMessage(receiptMessage, recipientId); + } + public void sendReceiptMessage( final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId ) throws IOException, UntrustedIdentityException { diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java index d34669a4..82c3bf16 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java @@ -1,27 +1,43 @@ package org.asamk.signal.manager.jobs; import org.asamk.signal.manager.StickerPackStore; +import org.asamk.signal.manager.helper.GroupHelper; +import org.asamk.signal.manager.helper.ProfileHelper; +import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.storage.SignalAccount; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; public class Context { - private SignalAccount account; - private SignalServiceAccountManager accountManager; - private SignalServiceMessageReceiver messageReceiver; - private StickerPackStore stickerPackStore; + private final SignalAccount account; + private final SignalServiceAccountManager accountManager; + private final SignalServiceMessageReceiver messageReceiver; + private final StickerPackStore stickerPackStore; + private final SendHelper sendHelper; + private final GroupHelper groupHelper; + private final SyncHelper syncHelper; + private final ProfileHelper profileHelper; public Context( final SignalAccount account, final SignalServiceAccountManager accountManager, final SignalServiceMessageReceiver messageReceiver, - final StickerPackStore stickerPackStore + final StickerPackStore stickerPackStore, + final SendHelper sendHelper, + final GroupHelper groupHelper, + final SyncHelper syncHelper, + final ProfileHelper profileHelper ) { this.account = account; this.accountManager = accountManager; this.messageReceiver = messageReceiver; this.stickerPackStore = stickerPackStore; + this.sendHelper = sendHelper; + this.groupHelper = groupHelper; + this.syncHelper = syncHelper; + this.profileHelper = profileHelper; } public SignalAccount getAccount() { @@ -39,4 +55,20 @@ public class Context { public StickerPackStore getStickerPackStore() { return stickerPackStore; } + + public SendHelper getSendHelper() { + return sendHelper; + } + + public GroupHelper getGroupHelper() { + return groupHelper; + } + + public SyncHelper getSyncHelper() { + return syncHelper; + } + + public ProfileHelper getProfileHelper() { + return profileHelper; + } } diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 105c2016..77a622b1 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -13,6 +14,8 @@ import org.asamk.signal.util.CommandUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; + public class BlockCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(BlockCommand.class); @@ -39,6 +42,8 @@ public class BlockCommand implements JsonRpcLocalCommand { m.setContactBlocked(contact, true); } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to sync block to linked devices: " + e.getMessage()); } } @@ -49,6 +54,8 @@ public class BlockCommand implements JsonRpcLocalCommand { m.setGroupBlocked(groupId, true); } catch (GroupNotFoundException e) { logger.warn("Group not found {}: {}", groupId.toBase64(), e.getMessage()); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to sync block to linked devices: " + e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index e931a60e..46bd9daa 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -13,6 +14,8 @@ import org.asamk.signal.util.CommandUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; + public class UnblockCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(UnblockCommand.class); @@ -38,6 +41,8 @@ public class UnblockCommand implements JsonRpcLocalCommand { m.setContactBlocked(contactNumber, false); } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to sync unblock to linked devices: " + e.getMessage()); } } @@ -47,6 +52,8 @@ public class UnblockCommand implements JsonRpcLocalCommand { m.setGroupBlocked(groupId, false); } catch (GroupNotFoundException e) { logger.warn("Unknown group id: {}", groupId); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to sync unblock to linked devices: " + e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 392b2df0..6a7cc764 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -253,6 +253,8 @@ public class DbusSignalImpl implements Signal { m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); } } @@ -262,6 +264,8 @@ public class DbusSignalImpl implements Signal { m.setGroupBlocked(getGroupId(groupId), blocked); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); } } -- 2.51.0 From 634437d22dc7120718fdb693b6b8c072aa153e60 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 15:26:25 +0200 Subject: [PATCH 11/16] Delete cached failed messages after 30 days --- lib/src/main/java/org/asamk/signal/manager/Manager.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 0d06dfba..bbdfa9e5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -851,6 +851,11 @@ public class Manager implements Closeable { try { content = dependencies.getCipher().decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { + if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { + // Envelope is more than a month old, cleaning up. + cachedMessage.delete(); + return null; + } if (!envelope.hasSource()) { final var identifier = e.getSender(); final var recipientId = resolveRecipient(identifier); -- 2.51.0 From 85c5caeacaa1b335ff38ed3d8bce9c02b8daca13 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 27 Aug 2021 09:04:14 +0200 Subject: [PATCH 12/16] Don't handle blocked or forbidden messages --- .../asamk/signal/manager/helper/IncomingMessageHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 369d3205..b0b42545 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -107,8 +107,6 @@ public final class IncomingMessageHandler { // address/uuid is validated by unidentified sender certificate account.getRecipientStore().resolveRecipientTrusted(content.getSender()); } - - actions.addAll(handleMessage(envelope, content, ignoreAttachments)); } if (isMessageBlocked(envelope, content)) { @@ -118,6 +116,7 @@ public final class IncomingMessageHandler { (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), envelope.getTimestamp()); } else { + actions.addAll(handleMessage(envelope, content, ignoreAttachments)); handler.handleMessage(envelope, content, exception); } return new Pair<>(actions, exception); -- 2.51.0 From 8bcd8d87d219ae0496986cba4bd6b89f3b2ad6f6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 21:23:30 +0200 Subject: [PATCH 13/16] Update libsignal-service-java --- lib/build.gradle.kts | 2 +- .../org/asamk/signal/manager/AvatarStore.java | 2 +- .../org/asamk/signal/manager/Manager.java | 217 ++++++++------- .../manager/UntrustedIdentityException.java | 27 ++ .../manager/api/RecipientIdentifier.java | 4 +- .../signal/manager/helper/GroupV2Helper.java | 62 ++--- .../helper/IncomingMessageHandler.java | 262 +++++++++--------- .../signal/manager/helper/ProfileHelper.java | 10 +- .../signal/manager/helper/SendHelper.java | 54 +++- .../signal/manager/helper/SyncHelper.java | 51 +--- .../signal/manager/storage/SignalAccount.java | 16 +- .../asamk/signal/manager/storage/Utils.java | 12 + .../storage/contacts/LegacyContactInfo.java | 6 +- .../manager/storage/groups/GroupInfoV2.java | 9 +- .../manager/storage/groups/GroupStore.java | 14 +- .../storage/identities/IdentityKeyStore.java | 3 +- .../storage/profiles/LegacyProfileStore.java | 6 +- .../profiles/LegacySignalProfileEntry.java | 12 +- .../storage/protocol/LegacyIdentityInfo.java | 10 +- .../protocol/LegacyJsonIdentityKeyStore.java | 16 +- .../protocol/LegacyJsonSessionStore.java | 12 +- .../storage/protocol/LegacySessionInfo.java | 6 +- .../storage/protocol/SignalProtocolStore.java | 12 + .../recipients/LegacyRecipientStore.java | 14 +- .../manager/storage/recipients/Recipient.java | 11 +- .../storage/recipients/RecipientAddress.java | 89 ++++++ .../storage/recipients/RecipientResolver.java | 8 + .../storage/recipients/RecipientStore.java | 97 ++++--- .../storage/sessions/SessionStore.java | 17 +- .../org/asamk/signal/manager/util/Utils.java | 14 +- run_tests.sh | 4 +- .../signal/JsonDbusReceiveMessageHandler.java | 2 +- .../asamk/signal/ReceiveMessageHandler.java | 13 +- .../signal/commands/JoinGroupCommand.java | 9 +- .../signal/commands/ListContactsCommand.java | 3 +- .../signal/commands/ListGroupsCommand.java | 4 +- .../commands/ListIdentitiesCommand.java | 3 +- .../signal/commands/QuitGroupCommand.java | 6 +- .../signal/commands/RemoteDeleteCommand.java | 6 +- .../asamk/signal/commands/SendCommand.java | 21 +- .../signal/commands/SendReactionCommand.java | 6 +- .../signal/commands/SendReceiptCommand.java | 5 +- .../signal/commands/SendTypingCommand.java | 5 +- .../signal/commands/UpdateGroupCommand.java | 6 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 3 + .../org/asamk/signal/json/JsonMention.java | 7 +- .../signal/json/JsonMessageEnvelope.java | 30 +- .../java/org/asamk/signal/json/JsonQuote.java | 3 +- .../org/asamk/signal/json/JsonReaction.java | 4 +- .../signal/json/JsonSyncDataMessage.java | 4 +- .../signal/json/JsonSyncReadMessage.java | 4 +- src/main/java/org/asamk/signal/util/Util.java | 2 +- 52 files changed, 683 insertions(+), 542 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/UntrustedIdentityException.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index dcb99cee..316ce564 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -14,7 +14,7 @@ repositories { } dependencies { - api("com.github.turasa:signal-service-java:2.15.3_unofficial_25") + api("com.github.turasa:signal-service-java:2.15.3_unofficial_26") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.slf4j:slf4j-api:1.7.30") diff --git a/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java b/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java index 8a1e6172..12a6525e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java @@ -82,7 +82,7 @@ public class AvatarStore { } private String getLegacyIdentifier(final SignalServiceAddress address) { - return address.getNumber().or(() -> address.getUuid().get().toString()); + return address.getNumber().or(() -> address.getUuid().toString()); } private File getProfileAvatarFile(SignalServiceAddress address) { diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index bbdfa9e5..9e38853b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -73,7 +73,6 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; @@ -83,6 +82,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -200,7 +200,7 @@ public class Manager implements Closeable { dependencies, unidentifiedAccessHelper, this::resolveSignalServiceAddress, - this::resolveRecipient, + account.getRecipientStore(), this::handleIdentityFailure, this::getGroup, this::refreshRegisteredUser); @@ -211,15 +211,14 @@ public class Manager implements Closeable { groupV2Helper, avatarStore, this::resolveSignalServiceAddress, - this::resolveRecipient); + account.getRecipientStore()); this.contactHelper = new ContactHelper(account); this.syncHelper = new SyncHelper(account, attachmentHelper, sendHelper, groupHelper, avatarStore, - this::resolveSignalServiceAddress, - this::resolveRecipient); + this::resolveSignalServiceAddress); this.context = new Context(account, dependencies.getAccountManager(), @@ -233,7 +232,8 @@ public class Manager implements Closeable { this.incomingMessageHandler = new IncomingMessageHandler(account, dependencies, - this::resolveRecipient, + account.getRecipientStore(), + this::resolveSignalServiceAddress, groupHelper, contactHelper, attachmentHelper, @@ -328,7 +328,7 @@ public class Manager implements Closeable { public Map> areUsersRegistered(Set numbers) throws IOException { Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { try { - return canonicalizePhoneNumber(n); + return PhoneNumberFormatter.formatNumber(n, account.getUsername()); } catch (InvalidNumberException e) { return ""; } @@ -490,7 +490,7 @@ public class Manager implements Closeable { public SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - final var newAdmins = getRecipientIds(groupAdmins); + final var newAdmins = resolveRecipients(groupAdmins); return groupHelper.quitGroup(groupId, newAdmins); } @@ -501,7 +501,7 @@ public class Manager implements Closeable { public Pair createGroup( String name, Set members, File avatarFile ) throws IOException, AttachmentInvalidException { - return groupHelper.createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); + return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile); } public SendGroupMessageResults updateGroup( @@ -523,10 +523,10 @@ public class Manager implements Closeable { return groupHelper.updateGroup(groupId, name, description, - members == null ? null : getRecipientIds(members), - removeMembers == null ? null : getRecipientIds(removeMembers), - admins == null ? null : getRecipientIds(admins), - removeAdmins == null ? null : getRecipientIds(removeAdmins), + members == null ? null : resolveRecipients(members), + removeMembers == null ? null : resolveRecipients(removeMembers), + admins == null ? null : resolveRecipients(admins), + removeAdmins == null ? null : resolveRecipients(removeAdmins), resetGroupLink, groupLinkState, addMemberPermission, @@ -662,7 +662,7 @@ public class Manager implements Closeable { public void setContactName( RecipientIdentifier.Single recipient, String name - ) throws NotMasterDeviceException { + ) throws NotMasterDeviceException, UnregisteredUserException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } @@ -755,53 +755,28 @@ public class Manager implements Closeable { return certificate; } - private Set getRecipientIds(Collection recipients) { - final var signalServiceAddresses = new HashSet(recipients.size()); - final var addressesMissingUuid = new HashSet(); - - for (var number : recipients) { - final var resolvedAddress = resolveSignalServiceAddress(resolveRecipient(number)); - if (resolvedAddress.getUuid().isPresent()) { - signalServiceAddresses.add(resolvedAddress); - } else { - addressesMissingUuid.add(resolvedAddress); - } - } - - final var numbersMissingUuid = addressesMissingUuid.stream() - .map(a -> a.getNumber().get()) - .collect(Collectors.toSet()); - Map registeredUsers; - try { - registeredUsers = getRegisteredUsers(numbersMissingUuid); - } catch (IOException e) { - logger.warn("Failed to resolve uuids from server, ignoring: {}", e.getMessage()); - registeredUsers = Map.of(); - } - - for (var address : addressesMissingUuid) { - final var number = address.getNumber().get(); - if (registeredUsers.containsKey(number)) { - final var newAddress = resolveSignalServiceAddress(resolveRecipientTrusted(new SignalServiceAddress( - registeredUsers.get(number), - number))); - signalServiceAddresses.add(newAddress); - } else { - signalServiceAddresses.add(address); - } - } - - return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet()); - } - private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException { final var address = resolveSignalServiceAddress(recipientId); if (!address.getNumber().isPresent()) { return recipientId; } final var number = address.getNumber().get(); - final var uuidMap = getRegisteredUsers(Set.of(number)); - return resolveRecipientTrusted(new SignalServiceAddress(uuidMap.getOrDefault(number, null), number)); + final var uuid = getRegisteredUser(number); + return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); + } + + private UUID getRegisteredUser(final String number) throws IOException { + final Map uuidMap; + try { + uuidMap = getRegisteredUsers(Set.of(number)); + } catch (NumberFormatException e) { + throw new UnregisteredUserException(number, e); + } + final var uuid = uuidMap.get(number); + if (uuid == null) { + throw new UnregisteredUserException(number, null); + } + return uuid; } private Map getRegisteredUsers(final Set numbers) throws IOException { @@ -856,9 +831,9 @@ public class Manager implements Closeable { cachedMessage.delete(); return null; } - if (!envelope.hasSource()) { + if (!envelope.hasSourceUuid()) { final var identifier = e.getSender(); - final var recipientId = resolveRecipient(identifier); + final var recipientId = account.getRecipientStore().resolveRecipient(identifier); try { account.getMessageCache().replaceSender(cachedMessage, recipientId); } catch (IOException ioException) { @@ -901,8 +876,8 @@ public class Manager implements Closeable { logger.debug("Checking for new message from server"); try { var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> { - final var recipientId = envelope1.hasSource() - ? resolveRecipient(envelope1.getSourceIdentifier()) + final var recipientId = envelope1.hasSourceUuid() + ? resolveRecipient(envelope1.getSourceAddress()) : null; // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); @@ -944,10 +919,10 @@ public class Manager implements Closeable { handleQueuedActions(queuedActions); } if (cachedMessage[0] != null) { - if (exception instanceof ProtocolUntrustedIdentityException) { - final var identifier = ((ProtocolUntrustedIdentityException) exception).getSender(); - final var recipientId = resolveRecipient(identifier); - if (!envelope.hasSource()) { + if (exception instanceof UntrustedIdentityException) { + final var address = ((UntrustedIdentityException) exception).getSender(); + final var recipientId = resolveRecipient(address); + if (!envelope.hasSourceUuid()) { try { cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); } catch (IOException ioException) { @@ -977,7 +952,12 @@ public class Manager implements Closeable { } public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { - final var recipientId = resolveRecipient(recipient); + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } return contactHelper.isContactBlocked(recipientId); } @@ -994,7 +974,12 @@ public class Manager implements Closeable { } public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { - final var recipientId = resolveRecipient(recipientIdentifier); + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipientIdentifier); + } catch (UnregisteredUserException e) { + return null; + } final var contact = account.getContactStore().getContact(recipientId); if (contact != null && !Util.isEmpty(contact.getName())) { @@ -1018,7 +1003,12 @@ public class Manager implements Closeable { } public List getIdentities(RecipientIdentifier.Single recipient) { - final var identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); + IdentityInfo identity; + try { + identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); + } catch (UnregisteredUserException e) { + identity = null; + } return identity == null ? List.of() : List.of(identity); } @@ -1029,7 +1019,12 @@ public class Manager implements Closeable { * @param fingerprint Fingerprint */ public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { - var recipientId = resolveRecipient(recipient); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } return trustIdentity(recipientId, identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), TrustLevel.TRUSTED_VERIFIED); @@ -1042,8 +1037,13 @@ public class Manager implements Closeable { * @param safetyNumber Safety number */ public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { - var recipientId = resolveRecipient(recipient); - var address = account.getRecipientStore().resolveServiceAddress(recipientId); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), TrustLevel.TRUSTED_VERIFIED); @@ -1056,8 +1056,13 @@ public class Manager implements Closeable { * @param safetyNumber Scannable safety number */ public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { - var recipientId = resolveRecipient(recipient); - var address = account.getRecipientStore().resolveServiceAddress(recipientId); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> { final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); try { @@ -1074,7 +1079,12 @@ public class Manager implements Closeable { * @param recipient username of the identity */ public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { - var recipientId = resolveRecipient(recipient); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); } @@ -1092,7 +1102,7 @@ public class Manager implements Closeable { account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); try { - var address = account.getRecipientStore().resolveServiceAddress(recipientId); + var address = resolveSignalServiceAddress(recipientId); syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); } catch (IOException e) { logger.warn("Failed to send verification sync message: {}", e.getMessage()); @@ -1136,48 +1146,61 @@ public class Manager implements Closeable { theirIdentityKey); } - @Deprecated - public SignalServiceAddress resolveSignalServiceAddress(String identifier) { - var address = Utils.getSignalServiceAddressFromIdentifier(identifier); - - return resolveSignalServiceAddress(address); - } - - @Deprecated public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { if (address.matches(account.getSelfAddress())) { return account.getSelfAddress(); } - return account.getRecipientStore().resolveServiceAddress(address); + return resolveSignalServiceAddress(resolveRecipient(address)); } - public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { - return account.getRecipientStore().resolveServiceAddress(recipientId); + public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { + return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); } - private String canonicalizePhoneNumber(final String number) throws InvalidNumberException { - return PhoneNumberFormatter.formatNumber(number, account.getUsername()); - } + public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { + final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); + if (address.getUuid().isPresent()) { + return address.toSignalServiceAddress(); + } - private RecipientId resolveRecipient(final String identifier) { - var address = Utils.getSignalServiceAddressFromIdentifier(identifier); + // Address in recipient store doesn't have a uuid, this shouldn't happen + // Try to retrieve the uuid from the server + final var number = address.getNumber().get(); + try { + return resolveSignalServiceAddress(getRegisteredUser(number)); + } catch (IOException e) { + logger.warn("Failed to get uuid for e164 number: {}", number, e); + // Return SignalServiceAddress with unknown UUID + return address.toSignalServiceAddress(); + } + } - return resolveRecipient(address); + private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { + final var recipientIds = new HashSet(recipients.size()); + for (var number : recipients) { + final var recipientId = resolveRecipient(number); + recipientIds.add(recipientId); + } + return recipientIds; } - private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) { - final SignalServiceAddress address; + private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { if (recipient instanceof RecipientIdentifier.Uuid) { - address = new SignalServiceAddress(((RecipientIdentifier.Uuid) recipient).uuid, null); + return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); } else { - address = new SignalServiceAddress(null, ((RecipientIdentifier.Number) recipient).number); + final var number = ((RecipientIdentifier.Number) recipient).number; + return account.getRecipientStore().resolveRecipient(number, () -> { + try { + return getRegisteredUser(number); + } catch (IOException e) { + return null; + } + }); } - - return resolveRecipient(address); } - public RecipientId resolveRecipient(SignalServiceAddress address) { + private RecipientId resolveRecipient(SignalServiceAddress address) { return account.getRecipientStore().resolveRecipient(address); } diff --git a/lib/src/main/java/org/asamk/signal/manager/UntrustedIdentityException.java b/lib/src/main/java/org/asamk/signal/manager/UntrustedIdentityException.java new file mode 100644 index 00000000..3b90b9e4 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/UntrustedIdentityException.java @@ -0,0 +1,27 @@ +package org.asamk.signal.manager; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public class UntrustedIdentityException extends Exception { + + private final SignalServiceAddress sender; + private final Integer senderDevice; + + public UntrustedIdentityException(final SignalServiceAddress sender) { + this(sender, null); + } + + public UntrustedIdentityException(final SignalServiceAddress sender, final Integer senderDevice) { + super("Untrusted identity: " + sender.getIdentifier()); + this.sender = sender; + this.senderDevice = senderDevice; + } + + public SignalServiceAddress getSender() { + return sender; + } + + public Integer getSenderDevice() { + return senderDevice; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java index cbcf1724..4a66cbb3 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -32,9 +32,7 @@ public abstract class RecipientIdentifier { } public static Single fromAddress(SignalServiceAddress address) { - return address.getUuid().isPresent() - ? new Uuid(address.getUuid().get()) - : new Number(address.getNumber().get()); + return new Uuid(address.getUuid()); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 19240cef..3187fca1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -150,11 +150,9 @@ public class GroupV2Helper { if (!areMembersValid(members)) return null; - var self = new GroupCandidate(addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) - .getUuid() - .orNull(), Optional.fromNullable(profileKeyCredential)); + var self = new GroupCandidate(getSelfUuid(), Optional.fromNullable(profileKeyCredential)); var candidates = members.stream() - .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(), + .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid(), Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member)))) .collect(Collectors.toSet()); @@ -169,18 +167,6 @@ public class GroupV2Helper { } private boolean areMembersValid(final Set members) { - final var noUuidCapability = members.stream() - .map(addressResolver::resolveSignalServiceAddress) - .filter(address -> !address.getUuid().isPresent()) - .map(SignalServiceAddress::getNumber) - .map(Optional::get) - .collect(Collectors.toSet()); - if (noUuidCapability.size() > 0) { - logger.warn("Cannot create a V2 group as some members don't have a UUID: {}", - String.join(", ", noUuidCapability)); - return false; - } - final var noGv2Capability = members.stream() .map(profileProvider::getProfile) .filter(profile -> profile != null && !profile.getCapabilities().contains(Profile.Capability.gv2)) @@ -214,11 +200,8 @@ public class GroupV2Helper { change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey)); } - final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId()) - .getUuid(); - if (uuid.isPresent()) { - change.setSourceUuid(UuidUtil.toByteString(uuid.get())); - } + final var uuid = getSelfUuid(); + change.setSourceUuid(UuidUtil.toByteString(uuid)); return commitChange(groupInfoV2, change); } @@ -233,13 +216,11 @@ public class GroupV2Helper { } var candidates = newMembers.stream() - .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(), + .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid(), Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member)))) .collect(Collectors.toSet()); - final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) - .getUuid() - .get(); + final var uuid = getSelfUuid(); final var change = groupOperations.createModifyGroupMembershipChange(candidates, uuid); change.setSourceUuid(UuidUtil.toByteString(uuid)); @@ -251,9 +232,7 @@ public class GroupV2Helper { GroupInfoV2 groupInfoV2, Set membersToMakeAdmin ) throws IOException { var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList(); - final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) - .getUuid() - .get(); + final var selfUuid = getSelfUuid(); var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfUuid); if (selfPendingMember.isPresent()) { @@ -263,7 +242,6 @@ public class GroupV2Helper { final var adminUuids = membersToMakeAdmin.stream() .map(addressResolver::resolveSignalServiceAddress) .map(SignalServiceAddress::getUuid) - .map(Optional::get) .collect(Collectors.toList()); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); return commitChange(groupInfoV2, groupOperations.createLeaveAndPromoteMembersToAdmin(selfUuid, adminUuids)); @@ -275,8 +253,6 @@ public class GroupV2Helper { final var memberUuids = members.stream() .map(addressResolver::resolveSignalServiceAddress) .map(SignalServiceAddress::getUuid) - .filter(Optional::isPresent) - .map(Optional::get) .collect(Collectors.toSet()); return ejectMembers(groupInfoV2, memberUuids); } @@ -288,8 +264,6 @@ public class GroupV2Helper { final var memberUuids = members.stream() .map(addressResolver::resolveSignalServiceAddress) .map(SignalServiceAddress::getUuid) - .filter(Optional::isPresent) - .map(Optional::get) .map(uuid -> DecryptedGroupUtil.findPendingByUuid(pendingMembersList, uuid)) .filter(Optional::isPresent) .map(Optional::get) @@ -360,8 +334,7 @@ public class GroupV2Helper { : groupOperations.createGroupJoinDirect(profileKeyCredential); change.setSourceUuid(UuidUtil.toByteString(addressResolver.resolveSignalServiceAddress(selfRecipientId) - .getUuid() - .get())); + .getUuid())); return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword); } @@ -378,9 +351,7 @@ public class GroupV2Helper { final var change = groupOperations.createAcceptInviteChange(profileKeyCredential); final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientId).getUuid(); - if (uuid.isPresent()) { - change.setSourceUuid(UuidUtil.toByteString(uuid.get())); - } + change.setSourceUuid(UuidUtil.toByteString(uuid)); return commitChange(groupInfoV2, change); } @@ -391,7 +362,7 @@ public class GroupV2Helper { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final var address = addressResolver.resolveSignalServiceAddress(recipientId); final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT; - final var change = groupOperations.createChangeMemberRole(address.getUuid().get(), newRole); + final var change = groupOperations.createChangeMemberRole(address.getUuid(), newRole); return commitChange(groupInfoV2, change); } @@ -473,10 +444,7 @@ public class GroupV2Helper { final DecryptedGroup decryptedGroupState; try { - decryptedChange = groupOperations.decryptChange(changeActions, - addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) - .getUuid() - .get()); + decryptedChange = groupOperations.decryptChange(changeActions, getSelfUuid()); decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange); } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { throw new IOException(e); @@ -543,13 +511,15 @@ public class GroupV2Helper { final var credentials = groupsV2Api.getCredentials(today); // TODO cache credentials until they expire var authCredentialResponse = credentials.get(today); - final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId()) - .getUuid() - .get(); + final var uuid = getSelfUuid(); try { return groupsV2Api.getGroupsV2AuthorizationString(uuid, today, groupSecretParams, authCredentialResponse); } catch (VerificationFailedException e) { throw new IOException(e); } } + + private UUID getSelfUuid() { + return addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId()).getUuid(); + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index b0b42545..f28b3638 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -4,6 +4,7 @@ import org.asamk.signal.manager.JobExecutor; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.actions.RenewSessionAction; import org.asamk.signal.manager.actions.RetrieveProfileAction; @@ -34,6 +35,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -48,6 +50,7 @@ public final class IncomingMessageHandler { private final SignalAccount account; private final SignalDependencies dependencies; private final RecipientResolver recipientResolver; + private final SignalServiceAddressResolver addressResolver; private final GroupHelper groupHelper; private final ContactHelper contactHelper; private final AttachmentHelper attachmentHelper; @@ -58,6 +61,7 @@ public final class IncomingMessageHandler { final SignalAccount account, final SignalDependencies dependencies, final RecipientResolver recipientResolver, + final SignalServiceAddressResolver addressResolver, final GroupHelper groupHelper, final ContactHelper contactHelper, final AttachmentHelper attachmentHelper, @@ -67,6 +71,7 @@ public final class IncomingMessageHandler { this.account = account; this.dependencies = dependencies; this.recipientResolver = recipientResolver; + this.addressResolver = addressResolver; this.groupHelper = groupHelper; this.contactHelper = contactHelper; this.attachmentHelper = attachmentHelper; @@ -80,7 +85,7 @@ public final class IncomingMessageHandler { final Manager.ReceiveMessageHandler handler ) { final var actions = new ArrayList(); - if (envelope.hasSource()) { + if (envelope.hasSourceUuid()) { // Store uuid if we don't have it already // address/uuid in envelope is sent by server account.getRecipientStore().resolveRecipientTrusted(envelope.getSourceAddress()); @@ -93,6 +98,8 @@ public final class IncomingMessageHandler { } catch (ProtocolUntrustedIdentityException e) { final var recipientId = account.getRecipientStore().resolveRecipient(e.getSender()); actions.add(new RetrieveProfileAction(recipientId)); + exception = new UntrustedIdentityException(addressResolver.resolveSignalServiceAddress(recipientId), + e.getSenderDevice()); } catch (ProtocolInvalidMessageException e) { final var sender = account.getRecipientStore().resolveRecipient(e.getSender()); logger.debug("Received invalid message, queuing renew session action."); @@ -102,7 +109,7 @@ public final class IncomingMessageHandler { exception = e; } - if (!envelope.hasSource() && content != null) { + if (!envelope.hasSourceUuid() && content != null) { // Store uuid if we don't have it already // address/uuid is validated by unidentified sender certificate account.getRecipientStore().resolveRecipientTrusted(content.getSender()); @@ -113,7 +120,7 @@ public final class IncomingMessageHandler { logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); } else if (isNotAllowedToSendToGroup(envelope, content)) { logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", - (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), + (envelope.hasSourceUuid() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), envelope.getTimestamp()); } else { actions.addAll(handleMessage(envelope, content, ignoreAttachments)); @@ -126,146 +133,153 @@ public final class IncomingMessageHandler { SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments ) { var actions = new ArrayList(); - if (content != null) { - final RecipientId sender; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); - } else { - sender = recipientResolver.resolveRecipient(content.getSender()); + if (content == null) { + return actions; + } + + final RecipientId sender; + if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { + sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); + } else { + sender = recipientResolver.resolveRecipient(content.getSender()); + } + + if (content.getDataMessage().isPresent()) { + var message = content.getDataMessage().get(); + + if (content.isNeedsReceipt()) { + actions.add(new SendReceiptAction(sender, message.getTimestamp())); } - if (content.getDataMessage().isPresent()) { - var message = content.getDataMessage().get(); + actions.addAll(handleSignalServiceDataMessage(message, + false, + sender, + account.getSelfRecipientId(), + ignoreAttachments)); + } - if (content.isNeedsReceipt()) { - actions.add(new SendReceiptAction(sender, message.getTimestamp())); - } + if (content.getSyncMessage().isPresent()) { + var syncMessage = content.getSyncMessage().get(); + actions.addAll(handleSyncMessage(syncMessage, sender, ignoreAttachments)); + } - actions.addAll(handleSignalServiceDataMessage(message, - false, - sender, - account.getSelfRecipientId(), - ignoreAttachments)); + return actions; + } + + private List handleSyncMessage( + final SignalServiceSyncMessage syncMessage, final RecipientId sender, final boolean ignoreAttachments + ) { + var actions = new ArrayList(); + account.setMultiDevice(true); + if (syncMessage.getSent().isPresent()) { + var message = syncMessage.getSent().get(); + final var destination = message.getDestination().orNull(); + actions.addAll(handleSignalServiceDataMessage(message.getMessage(), + true, + sender, + destination == null ? null : recipientResolver.resolveRecipient(destination), + ignoreAttachments)); + } + if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { + var rm = syncMessage.getRequest().get(); + if (rm.isContactsRequest()) { + actions.add(SendSyncContactsAction.create()); } - if (content.getSyncMessage().isPresent()) { - account.setMultiDevice(true); - var syncMessage = content.getSyncMessage().get(); - if (syncMessage.getSent().isPresent()) { - var message = syncMessage.getSent().get(); - final var destination = message.getDestination().orNull(); - actions.addAll(handleSignalServiceDataMessage(message.getMessage(), - true, - sender, - destination == null ? null : recipientResolver.resolveRecipient(destination), - ignoreAttachments)); - } - if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { - var rm = syncMessage.getRequest().get(); - if (rm.isContactsRequest()) { - actions.add(SendSyncContactsAction.create()); - } - if (rm.isGroupsRequest()) { - actions.add(SendSyncGroupsAction.create()); - } - if (rm.isBlockedListRequest()) { - actions.add(SendSyncBlockedListAction.create()); - } - // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); + if (rm.isGroupsRequest()) { + actions.add(SendSyncGroupsAction.create()); + } + if (rm.isBlockedListRequest()) { + actions.add(SendSyncBlockedListAction.create()); + } + // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); + } + if (syncMessage.getGroups().isPresent()) { + logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring."); + } + if (syncMessage.getBlockedList().isPresent()) { + final var blockedListMessage = syncMessage.getBlockedList().get(); + for (var address : blockedListMessage.getAddresses()) { + contactHelper.setContactBlocked(recipientResolver.resolveRecipient(address), true); + } + for (var groupId : blockedListMessage.getGroupIds() + .stream() + .map(GroupId::unknownVersion) + .collect(Collectors.toSet())) { + try { + groupHelper.setGroupBlocked(groupId, true); + } catch (GroupNotFoundException e) { + logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", + groupId.toBase64()); } - if (syncMessage.getGroups().isPresent()) { - try { - final var groupsMessage = syncMessage.getGroups().get(); - attachmentHelper.retrieveAttachment(groupsMessage, syncHelper::handleSyncDeviceGroups); - } catch (Exception e) { - logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage()); - } + } + } + if (syncMessage.getContacts().isPresent()) { + try { + final var contactsMessage = syncMessage.getContacts().get(); + attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(), + syncHelper::handleSyncDeviceContacts); + } catch (Exception e) { + logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); + } + } + if (syncMessage.getVerified().isPresent()) { + final var verifiedMessage = syncMessage.getVerified().get(); + account.getIdentityKeyStore() + .setIdentityTrustLevel(account.getRecipientStore() + .resolveRecipientTrusted(verifiedMessage.getDestination()), + verifiedMessage.getIdentityKey(), + TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + } + if (syncMessage.getStickerPackOperations().isPresent()) { + final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); + for (var m : stickerPackOperationMessages) { + if (!m.getPackId().isPresent()) { + continue; } - if (syncMessage.getBlockedList().isPresent()) { - final var blockedListMessage = syncMessage.getBlockedList().get(); - for (var address : blockedListMessage.getAddresses()) { - contactHelper.setContactBlocked(recipientResolver.resolveRecipient(address), true); + final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); + final var installed = !m.getType().isPresent() + || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; + + var sticker = account.getStickerStore().getSticker(stickerPackId); + if (m.getPackKey().isPresent()) { + if (sticker == null) { + sticker = new Sticker(stickerPackId, m.getPackKey().get()); } - for (var groupId : blockedListMessage.getGroupIds() - .stream() - .map(GroupId::unknownVersion) - .collect(Collectors.toSet())) { - try { - groupHelper.setGroupBlocked(groupId, true); - } catch (GroupNotFoundException e) { - logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", - groupId.toBase64()); - } + if (installed) { + jobExecutor.enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); } } - if (syncMessage.getContacts().isPresent()) { - try { - final var contactsMessage = syncMessage.getContacts().get(); - attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(), - syncHelper::handleSyncDeviceContacts); - } catch (Exception e) { - logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); - } - } - if (syncMessage.getVerified().isPresent()) { - final var verifiedMessage = syncMessage.getVerified().get(); - account.getIdentityKeyStore() - .setIdentityTrustLevel(account.getRecipientStore() - .resolveRecipientTrusted(verifiedMessage.getDestination()), - verifiedMessage.getIdentityKey(), - TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); - } - if (syncMessage.getStickerPackOperations().isPresent()) { - final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); - for (var m : stickerPackOperationMessages) { - if (!m.getPackId().isPresent()) { - continue; - } - final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); - final var installed = !m.getType().isPresent() - || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; - - var sticker = account.getStickerStore().getSticker(stickerPackId); - if (m.getPackKey().isPresent()) { - if (sticker == null) { - sticker = new Sticker(stickerPackId, m.getPackKey().get()); - } - if (installed) { - jobExecutor.enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); - } - } - if (sticker != null) { - sticker.setInstalled(installed); - account.getStickerStore().updateSticker(sticker); - } - } - } - if (syncMessage.getFetchType().isPresent()) { - switch (syncMessage.getFetchType().get()) { - case LOCAL_PROFILE: - actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); - case STORAGE_MANIFEST: - // TODO - } - } - if (syncMessage.getKeys().isPresent()) { - final var keysMessage = syncMessage.getKeys().get(); - if (keysMessage.getStorageService().isPresent()) { - final var storageKey = keysMessage.getStorageService().get(); - account.setStorageKey(storageKey); - } + if (sticker != null) { + sticker.setInstalled(installed); + account.getStickerStore().updateSticker(sticker); } - if (syncMessage.getConfiguration().isPresent()) { + } + } + if (syncMessage.getFetchType().isPresent()) { + switch (syncMessage.getFetchType().get()) { + case LOCAL_PROFILE: + actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); + case STORAGE_MANIFEST: // TODO - } } } + if (syncMessage.getKeys().isPresent()) { + final var keysMessage = syncMessage.getKeys().get(); + if (keysMessage.getStorageService().isPresent()) { + final var storageKey = keysMessage.getStorageService().get(); + account.setStorageKey(storageKey); + } + } + if (syncMessage.getConfiguration().isPresent()) { + // TODO + } return actions; } private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) { SignalServiceAddress source; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { + if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { source = envelope.getSourceAddress(); } else if (content != null) { source = content.getSender(); @@ -290,7 +304,7 @@ public final class IncomingMessageHandler { private boolean isNotAllowedToSendToGroup(SignalServiceEnvelope envelope, SignalServiceContent content) { SignalServiceAddress source; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { + if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { source = envelope.getSourceAddress(); } else if (content != null) { source = content.getSender(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index ac75a573..52154798 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -273,7 +273,7 @@ public final class ProfileHelper { private Single retrieveProfile( RecipientId recipientId, SignalServiceProfile.RequestType requestType - ) throws IOException { + ) { var unidentifiedAccess = getUnidentifiedAccess(recipientId); var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId)); @@ -286,7 +286,7 @@ public final class ProfileHelper { Optional profileKey, Optional unidentifiedAccess, SignalServiceProfile.RequestType requestType - ) throws IOException { + ) { var profileService = profileServiceProvider.getProfileService(); Single> responseSingle; @@ -294,11 +294,7 @@ public final class ProfileHelper { responseSingle = profileService.getProfile(address, profileKey, unidentifiedAccess, requestType); } catch (NoClassDefFoundError e) { // Native zkgroup lib not available for ProfileKey - if (!address.getNumber().isPresent()) { - throw new NotFoundException("Can't request profile without number"); - } - var addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber()); - responseSingle = profileService.getProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType); + responseSingle = profileService.getProfile(address, Optional.absent(), unidentifiedAccess, requestType); } return responseSingle.map(pair -> { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 058a04f2..da901a3d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -1,6 +1,7 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; @@ -13,8 +14,8 @@ import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.ContentHint; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; @@ -43,6 +44,20 @@ public class SendHelper { private final GroupProvider groupProvider; private final RecipientRegistrationRefresher recipientRegistrationRefresher; + private final SignalServiceMessageSender.IndividualSendEvents sendEvents = new SignalServiceMessageSender.IndividualSendEvents() { + @Override + public void onMessageEncrypted() { + } + + @Override + public void onMessageSent() { + } + + @Override + public void onSyncMessageSent() { + } + }; + public SendHelper( final SignalAccount account, final SignalDependencies dependencies, @@ -145,9 +160,12 @@ public class SendHelper { final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId ) throws IOException, UntrustedIdentityException { final var messageSender = dependencies.getMessageSender(); - messageSender.sendReceipt(addressResolver.resolveSignalServiceAddress(recipientId), - unidentifiedAccessHelper.getAccessFor(recipientId), - receiptMessage); + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + try { + messageSender.sendReceipt(address, unidentifiedAccessHelper.getAccessFor(recipientId), receiptMessage); + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { + throw new UntrustedIdentityException(address); + } } public SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException { @@ -162,7 +180,7 @@ public class SendHelper { final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); return messageSender.sendNullMessage(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId)); } - } catch (UntrustedIdentityException e) { + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); } } @@ -183,7 +201,7 @@ public class SendHelper { var messageSender = dependencies.getMessageSender(); try { return messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync()); - } catch (UntrustedIdentityException e) { + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId()); return SendMessageResult.identityFailure(address, e.getIdentityKey()); } @@ -195,11 +213,15 @@ public class SendHelper { var messageSender = dependencies.getMessageSender(); final var address = addressResolver.resolveSignalServiceAddress(recipientId); try { - messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); - } catch (UnregisteredUserException e) { - final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); - final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); - messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message); + try { + messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); + } catch (UnregisteredUserException e) { + final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); + final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); + messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message); + } + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { + throw new UntrustedIdentityException(address); } } @@ -247,7 +269,7 @@ public class SendHelper { message, sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), () -> false); - } catch (UntrustedIdentityException e) { + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return List.of(); } } @@ -263,15 +285,17 @@ public class SendHelper { return messageSender.sendDataMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId), ContentHint.DEFAULT, - message); + message, + sendEvents); } catch (UnregisteredUserException e) { final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); return messageSender.sendDataMessage(addressResolver.resolveSignalServiceAddress(newRecipientId), unidentifiedAccessHelper.getAccessFor(newRecipientId), ContentHint.DEFAULT, - message); + message, + sendEvents); } - } catch (UntrustedIdentityException e) { + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 48dc206e..461706f0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -2,11 +2,9 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.AvatarStore; import org.asamk.signal.manager.TrustLevel; -import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.recipients.Contact; -import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.IOUtils; import org.slf4j.Logger; @@ -21,7 +19,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; @@ -36,7 +33,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.util.ArrayList; -import java.util.List; import java.util.stream.Collectors; public class SyncHelper { @@ -49,7 +45,6 @@ public class SyncHelper { private final GroupHelper groupHelper; private final AvatarStore avatarStore; private final SignalServiceAddressResolver addressResolver; - private final RecipientResolver recipientResolver; public SyncHelper( final SignalAccount account, @@ -57,8 +52,7 @@ public class SyncHelper { final SendHelper sendHelper, final GroupHelper groupHelper, final AvatarStore avatarStore, - final SignalServiceAddressResolver addressResolver, - final RecipientResolver recipientResolver + final SignalServiceAddressResolver addressResolver ) { this.account = account; this.attachmentHelper = attachmentHelper; @@ -66,7 +60,6 @@ public class SyncHelper { this.groupHelper = groupHelper; this.avatarStore = avatarStore; this.addressResolver = addressResolver; - this.recipientResolver = recipientResolver; } public void requestAllSyncData() throws IOException { @@ -222,48 +215,6 @@ public class SyncHelper { sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } - public void handleSyncDeviceGroups(final InputStream input) { - final var s = new DeviceGroupsInputStream(input); - DeviceGroup g; - while (true) { - try { - g = s.read(); - } catch (IOException e) { - logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage()); - continue; - } - if (g == null) { - break; - } - var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId())); - if (syncGroup != null) { - if (g.getName().isPresent()) { - syncGroup.name = g.getName().get(); - } - syncGroup.addMembers(g.getMembers() - .stream() - .map(recipientResolver::resolveRecipient) - .collect(Collectors.toSet())); - if (!g.isActive()) { - syncGroup.removeMember(account.getSelfRecipientId()); - } else { - // Add ourself to the member set as it's marked as active - syncGroup.addMembers(List.of(account.getSelfRecipientId())); - } - syncGroup.blocked = g.isBlocked(); - if (g.getColor().isPresent()) { - syncGroup.color = g.getColor().get(); - } - - if (g.getAvatar().isPresent()) { - groupHelper.downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); - } - syncGroup.archived = g.isArchived(); - account.getGroupStore().updateGroup(syncGroup); - } - } - } - public void handleSyncDeviceContacts(final InputStream input) { final var s = new DeviceContactsInputStream(input); DeviceContact c; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 477e02dc..c972c2c7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -152,7 +152,7 @@ public class SignalAccount implements Closeable { account.initStores(dataPath, identityKey, registrationId, trustNewIdentity); account.groupStore = new GroupStore(getGroupCachePath(dataPath, username), - account.recipientStore::resolveRecipient, + account.recipientStore, account::saveGroupStore); account.stickerStore = new StickerStore(account::saveStickerStore); @@ -174,9 +174,9 @@ public class SignalAccount implements Closeable { preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username)); signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username)); - sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient); + sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore); identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username), - recipientStore::resolveRecipient, + recipientStore, identityKey, registrationId, trustNewIdentity); @@ -254,7 +254,7 @@ public class SignalAccount implements Closeable { account.initStores(dataPath, identityKey, registrationId, trustNewIdentity); account.groupStore = new GroupStore(getGroupCachePath(dataPath, username), - account.recipientStore::resolveRecipient, + account.recipientStore, account::saveGroupStore); account.stickerStore = new StickerStore(account::saveStickerStore); @@ -453,12 +453,10 @@ public class SignalAccount implements Closeable { groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class); groupStore = GroupStore.fromStorage(groupStoreStorage, getGroupCachePath(dataPath, username), - recipientStore::resolveRecipient, + recipientStore, this::saveGroupStore); } else { - groupStore = new GroupStore(getGroupCachePath(dataPath, username), - recipientStore::resolveRecipient, - this::saveGroupStore); + groupStore = new GroupStore(getGroupCachePath(dataPath, username), recipientStore, this::saveGroupStore); } if (rootNode.hasNonNull("stickerStore")) { @@ -572,7 +570,7 @@ public class SignalAccount implements Closeable { var profileStoreNode = rootNode.get("profileStore"); final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class); for (var profileEntry : legacyProfileStore.getProfileEntries()) { - var recipientId = recipientStore.resolveRecipient(profileEntry.getServiceAddress()); + var recipientId = recipientStore.resolveRecipient(profileEntry.getAddress()); recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential()); recipientStore.storeProfileKey(recipientId, profileEntry.getProfileKey()); final var profile = profileEntry.getProfile(); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java b/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java index e542f4e3..e4b639a2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java @@ -9,7 +9,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; + import java.io.InvalidObjectException; +import java.util.Optional; public class Utils { @@ -37,4 +41,12 @@ public class Utils { return node; } + + public static RecipientAddress getRecipientAddressFromIdentifier(final String identifier) { + if (UuidUtil.isUuid(identifier)) { + return new RecipientAddress(UuidUtil.parseOrThrow(identifier)); + } else { + return new RecipientAddress(Optional.empty(), Optional.of(identifier)); + } + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java index a90f87e0..6c6bd7ea 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java @@ -3,7 +3,7 @@ package org.asamk.signal.manager.storage.contacts; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import java.util.UUID; @@ -42,7 +42,7 @@ public class LegacyContactInfo { } @JsonIgnore - public SignalServiceAddress getAddress() { - return new SignalServiceAddress(uuid, number); + public RecipientAddress getAddress() { + return new RecipientAddress(uuid, number); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index 59cfedb5..f86dcb04 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -9,7 +9,6 @@ import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.EnabledState; import org.signal.zkgroup.groups.GroupMasterKey; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.Set; @@ -89,7 +88,7 @@ public class GroupInfoV2 extends GroupInfo { } return group.getMembersList() .stream() - .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) + .map(m -> UuidUtil.parseOrThrow(m.getUuid().toByteArray())) .map(recipientResolver::resolveRecipient) .collect(Collectors.toSet()); } @@ -101,7 +100,7 @@ public class GroupInfoV2 extends GroupInfo { } return group.getPendingMembersList() .stream() - .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) + .map(m -> UuidUtil.parseOrThrow(m.getUuid().toByteArray())) .map(recipientResolver::resolveRecipient) .collect(Collectors.toSet()); } @@ -113,7 +112,7 @@ public class GroupInfoV2 extends GroupInfo { } return group.getRequestingMembersList() .stream() - .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) + .map(m -> UuidUtil.parseOrThrow(m.getUuid().toByteArray())) .map(recipientResolver::resolveRecipient) .collect(Collectors.toSet()); } @@ -126,7 +125,7 @@ public class GroupInfoV2 extends GroupInfo { return group.getMembersList() .stream() .filter(m -> m.getRole() == Member.Role.ADMINISTRATOR) - .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) + .map(m -> UuidUtil.parseOrThrow(m.getUuid().toByteArray())) .map(recipientResolver::resolveRecipient) .collect(Collectors.toSet()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java index 86459c2a..4adc413a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -14,6 +14,7 @@ import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.IOUtils; @@ -22,7 +23,6 @@ import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.util.Hex; @@ -78,7 +78,7 @@ public class GroupStore { final var g1 = (Storage.GroupV1) g; final var members = g1.members.stream().map(m -> { if (m.recipientId == null) { - return recipientResolver.resolveRecipient(new SignalServiceAddress(UuidUtil.parseOrNull(m.uuid), + return recipientResolver.resolveRecipient(new RecipientAddress(UuidUtil.parseOrNull(m.uuid), m.number)); } @@ -343,17 +343,17 @@ public class GroupStore { } } - private static final class JsonSignalServiceAddress { + private static final class JsonRecipientAddress { public String uuid; public String number; // For deserialization - public JsonSignalServiceAddress() { + public JsonRecipientAddress() { } - JsonSignalServiceAddress(final String uuid, final String number) { + JsonRecipientAddress(final String uuid, final String number) { this.uuid = uuid; this.number = number; } @@ -370,7 +370,7 @@ public class GroupStore { if (address.recipientId != null) { jgen.writeNumber(address.recipientId); } else if (address.uuid != null) { - jgen.writeObject(new JsonSignalServiceAddress(address.uuid, address.number)); + jgen.writeObject(new JsonRecipientAddress(address.uuid, address.number)); } else { jgen.writeString(address.number); } @@ -393,7 +393,7 @@ public class GroupStore { } else if (n.isNumber()) { addresses.add(new Member(n.numberValue().longValue(), null, null)); } else { - var address = jsonParser.getCodec().treeToValue(n, JsonSignalServiceAddress.class); + var address = jsonParser.getCodec().treeToValue(n, JsonRecipientAddress.class); addresses.add(new Member(null, address.uuid, address.number)); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java index 0cbdf347..f24e77b1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java @@ -6,7 +6,6 @@ import org.asamk.signal.manager.TrustLevel; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.IOUtils; -import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; @@ -177,7 +176,7 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden * @param identifier can be either a serialized uuid or a e164 phone number */ private RecipientId resolveRecipient(String identifier) { - return resolver.resolveRecipient(Utils.getSignalServiceAddressFromIdentifier(identifier)); + return resolver.resolveRecipient(identifier); } private File getIdentityFile(final RecipientId recipientId) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java index 8e1d5c88..1c6369f0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java @@ -8,10 +8,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; @@ -45,7 +45,7 @@ public class LegacyProfileStore { for (var entry : node) { var name = entry.hasNonNull("name") ? entry.get("name").asText() : null; var uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null; - final var serviceAddress = new SignalServiceAddress(uuid, name); + final var address = new RecipientAddress(uuid, name); ProfileKey profileKey = null; try { profileKey = new ProfileKey(Base64.getDecoder().decode(entry.get("profileKey").asText())); @@ -61,7 +61,7 @@ public class LegacyProfileStore { } var lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong(); var profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class); - profileEntries.add(new LegacySignalProfileEntry(serviceAddress, + profileEntries.add(new LegacySignalProfileEntry(address, profileKey, lastUpdateTimestamp, profile, diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java index 1e2f7ec8..03b11bcb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java @@ -1,12 +1,12 @@ package org.asamk.signal.manager.storage.profiles; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; public class LegacySignalProfileEntry { - private final SignalServiceAddress serviceAddress; + private final RecipientAddress address; private final ProfileKey profileKey; @@ -17,21 +17,21 @@ public class LegacySignalProfileEntry { private final ProfileKeyCredential profileKeyCredential; public LegacySignalProfileEntry( - final SignalServiceAddress serviceAddress, + final RecipientAddress address, final ProfileKey profileKey, final long lastUpdateTimestamp, final SignalProfile profile, final ProfileKeyCredential profileKeyCredential ) { - this.serviceAddress = serviceAddress; + this.address = address; this.profileKey = profileKey; this.lastUpdateTimestamp = lastUpdateTimestamp; this.profile = profile; this.profileKeyCredential = profileKeyCredential; } - public SignalServiceAddress getServiceAddress() { - return serviceAddress; + public RecipientAddress getAddress() { + return address; } public ProfileKey getProfileKey() { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java index eb66b3e5..2fa19f42 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java @@ -1,30 +1,30 @@ package org.asamk.signal.manager.storage.protocol; import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Date; public class LegacyIdentityInfo { - SignalServiceAddress address; + RecipientAddress address; IdentityKey identityKey; TrustLevel trustLevel; Date added; - LegacyIdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) { + LegacyIdentityInfo(RecipientAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) { this.address = address; this.identityKey = identityKey; this.trustLevel = trustLevel; this.added = added; } - public SignalServiceAddress getAddress() { + public RecipientAddress getAddress() { return address; } - public void setAddress(final SignalServiceAddress address) { + public void setAddress(final RecipientAddress address) { this.address = address; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java index 781b6f96..5e11ab7a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java @@ -6,13 +6,13 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import org.asamk.signal.manager.TrustLevel; -import org.asamk.signal.manager.util.Utils; +import org.asamk.signal.manager.storage.Utils; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; @@ -55,11 +55,11 @@ public class LegacyJsonIdentityKeyStore { return localRegistrationId; } - private LegacyIdentityInfo getIdentity(SignalServiceAddress serviceAddress) { + private LegacyIdentityInfo getIdentity(RecipientAddress address) { long maxDate = 0; LegacyIdentityInfo maxIdentity = null; for (var id : this.identities) { - if (!id.address.matches(serviceAddress)) { + if (!id.getAddress().matches(address)) { continue; } @@ -98,16 +98,16 @@ public class LegacyJsonIdentityKeyStore { var uuid = trustedKey.hasNonNull("uuid") ? UuidUtil.parseOrNull(trustedKey.get("uuid").asText()) : null; - final var serviceAddress = uuid == null - ? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName) - : new SignalServiceAddress(uuid, trustedKeyName); + final var address = uuid == null + ? Utils.getRecipientAddressFromIdentifier(trustedKeyName) + : new RecipientAddress(uuid, trustedKeyName); try { var id = new IdentityKey(Base64.getDecoder().decode(trustedKey.get("identityKey").asText()), 0); var trustLevel = trustedKey.hasNonNull("trustLevel") ? TrustLevel.fromInt(trustedKey.get( "trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED; var added = trustedKey.hasNonNull("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp") .asLong()) : new Date(); - identities.add(new LegacyIdentityInfo(serviceAddress, id, trustLevel, added)); + identities.add(new LegacyIdentityInfo(address, id, trustLevel, added)); } catch (InvalidKeyException e) { logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java index 5f301aeb..9ee0cf87 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java @@ -5,8 +5,8 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; -import org.asamk.signal.manager.util.Utils; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.asamk.signal.manager.storage.Utils; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; @@ -45,12 +45,12 @@ public class LegacyJsonSessionStore { } var uuid = session.hasNonNull("uuid") ? UuidUtil.parseOrNull(session.get("uuid").asText()) : null; - final var serviceAddress = uuid == null - ? Utils.getSignalServiceAddressFromIdentifier(sessionName) - : new SignalServiceAddress(uuid, sessionName); + final var address = uuid == null + ? Utils.getRecipientAddressFromIdentifier(sessionName) + : new RecipientAddress(uuid, sessionName); final var deviceId = session.get("deviceId").asInt(); final var record = Base64.getDecoder().decode(session.get("record").asText()); - var sessionInfo = new LegacySessionInfo(serviceAddress, deviceId, record); + var sessionInfo = new LegacySessionInfo(address, deviceId, record); sessions.add(sessionInfo); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java index a19bbd86..2cb984fb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java @@ -1,16 +1,16 @@ package org.asamk.signal.manager.storage.protocol; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; public class LegacySessionInfo { - public SignalServiceAddress address; + public RecipientAddress address; public int deviceId; public byte[] sessionRecord; - LegacySessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) { + LegacySessionInfo(final RecipientAddress address, final int deviceId, final byte[] sessionRecord) { this.address = address; this.deviceId = deviceId; this.sessionRecord = sessionRecord; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java index 84923423..77eb764a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java @@ -129,6 +129,11 @@ public class SignalProtocolStore implements SignalServiceDataStore { sessionStore.archiveSession(address); } + @Override + public Set getAllAddressesWithActiveSessions(final List addressNames) { + return sessionStore.getAllAddressesWithActiveSessions(addressNames); + } + @Override public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { return signedPreKeyStore.loadSignedPreKey(signedPreKeyId); @@ -189,4 +194,11 @@ public class SignalProtocolStore implements SignalServiceDataStore { public boolean isMultiDevice() { return isMultiDevice.get(); } + + @Override + public Transaction beginTransaction() { + return () -> { + // No-op transaction should be safe, as it's only a performance improvement + }; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java index 49317157..2aeb349f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; @@ -18,28 +17,27 @@ public class LegacyRecipientStore { @JsonProperty("recipientStore") @JsonDeserialize(using = RecipientStoreDeserializer.class) - private final List addresses = new ArrayList<>(); + private final List addresses = new ArrayList<>(); - public List getAddresses() { + public List getAddresses() { return addresses; } - public static class RecipientStoreDeserializer extends JsonDeserializer> { + public static class RecipientStoreDeserializer extends JsonDeserializer> { @Override - public List deserialize( + public List deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - var addresses = new ArrayList(); + var addresses = new ArrayList(); if (node.isArray()) { for (var recipient : node) { var recipientName = recipient.get("name").asText(); var uuid = UuidUtil.parseOrThrow(recipient.get("uuid").asText()); - final var serviceAddress = new SignalServiceAddress(uuid, recipientName); - addresses.add(serviceAddress); + addresses.add(new RecipientAddress(uuid, recipientName)); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java index 3ccf8210..2d2950dc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java @@ -2,13 +2,12 @@ package org.asamk.signal.manager.storage.recipients; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; public class Recipient { private final RecipientId recipientId; - private final SignalServiceAddress address; + private final RecipientAddress address; private final Contact contact; @@ -20,7 +19,7 @@ public class Recipient { public Recipient( final RecipientId recipientId, - final SignalServiceAddress address, + final RecipientAddress address, final Contact contact, final ProfileKey profileKey, final ProfileKeyCredential profileKeyCredential, @@ -62,7 +61,7 @@ public class Recipient { return recipientId; } - public SignalServiceAddress getAddress() { + public RecipientAddress getAddress() { return address; } @@ -85,7 +84,7 @@ public class Recipient { public static final class Builder { private RecipientId recipientId; - private SignalServiceAddress address; + private RecipientAddress address; private Contact contact; private ProfileKey profileKey; private ProfileKeyCredential profileKeyCredential; @@ -99,7 +98,7 @@ public class Recipient { return this; } - public Builder withAddress(final SignalServiceAddress val) { + public Builder withAddress(final RecipientAddress val) { address = val; return this; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java new file mode 100644 index 00000000..29e964b0 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java @@ -0,0 +1,89 @@ +package org.asamk.signal.manager.storage.recipients; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.Optional; +import java.util.UUID; + +public class RecipientAddress { + + private final Optional uuid; + private final Optional e164; + + /** + * Construct a RecipientAddress. + * + * @param uuid The UUID of the user, if available. + * @param e164 The phone number of the user, if available. + */ + public RecipientAddress(Optional uuid, Optional e164) { + if (!uuid.isPresent() && !e164.isPresent()) { + throw new AssertionError("Must have either a UUID or E164 number!"); + } + + this.uuid = uuid; + this.e164 = e164; + } + + public RecipientAddress(UUID uuid, String e164) { + this(Optional.ofNullable(uuid), Optional.ofNullable(e164)); + } + + public RecipientAddress(SignalServiceAddress address) { + this.uuid = Optional.of(address.getUuid()); + this.e164 = Optional.ofNullable(address.getNumber().orNull()); + } + + public RecipientAddress(UUID uuid) { + this.uuid = Optional.of(uuid); + this.e164 = Optional.empty(); + } + + public Optional getNumber() { + return e164; + } + + public Optional getUuid() { + return uuid; + } + + public String getIdentifier() { + if (uuid.isPresent()) { + return uuid.get().toString(); + } else if (e164.isPresent()) { + return e164.get(); + } else { + throw new AssertionError("Given the checks in the constructor, this should not be possible."); + } + } + + public boolean matches(RecipientAddress other) { + return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) || ( + e164.isPresent() && other.e164.isPresent() && e164.get().equals(other.e164.get()) + ); + } + + public SignalServiceAddress toSignalServiceAddress() { + return new SignalServiceAddress(uuid.orElse(UuidUtil.UNKNOWN_UUID), + org.whispersystems.libsignal.util.guava.Optional.fromNullable(e164.orElse(null))); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final RecipientAddress that = (RecipientAddress) o; + + if (!uuid.equals(that.uuid)) return false; + return e164.equals(that.e164); + } + + @Override + public int hashCode() { + int result = uuid.hashCode(); + result = 31 * result + e164.hashCode(); + return result; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientResolver.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientResolver.java index c6d06d23..a76e5b50 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientResolver.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientResolver.java @@ -2,7 +2,15 @@ package org.asamk.signal.manager.storage.recipients; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.util.UUID; + public interface RecipientResolver { + RecipientId resolveRecipient(String identifier); + + RecipientId resolveRecipient(RecipientAddress address); + RecipientId resolveRecipient(SignalServiceAddress address); + + RecipientId resolveRecipient(UUID uuid); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index c8a11340..86164d58 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -12,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.ByteArrayInputStream; @@ -30,9 +31,10 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.function.Supplier; import java.util.stream.Collectors; -public class RecipientStore implements ContactsStore, ProfileStore { +public class RecipientStore implements RecipientResolver, ContactsStore, ProfileStore { private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class); @@ -51,9 +53,8 @@ public class RecipientStore implements ContactsStore, ProfileStore { final var storage = objectMapper.readValue(inputStream, Storage.class); final var recipients = storage.recipients.stream().map(r -> { final var recipientId = new RecipientId(r.id); - final var address = new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable( - r.uuid).transform(UuidUtil::parseOrThrow), - org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.number)); + final var address = new RecipientAddress(Optional.ofNullable(r.uuid).map(UuidUtil::parseOrThrow), + Optional.ofNullable(r.number)); Contact contact = null; if (r.contact != null) { @@ -119,7 +120,7 @@ public class RecipientStore implements ContactsStore, ProfileStore { this.lastId = lastId; } - public SignalServiceAddress resolveServiceAddress(RecipientId recipientId) { + public RecipientAddress resolveRecipientAddress(RecipientId recipientId) { synchronized (recipients) { return getRecipient(recipientId).getAddress(); } @@ -134,24 +135,52 @@ public class RecipientStore implements ContactsStore, ProfileStore { } } - @Deprecated - public SignalServiceAddress resolveServiceAddress(SignalServiceAddress address) { - return resolveServiceAddress(resolveRecipient(address, false)); + @Override + public RecipientId resolveRecipient(UUID uuid) { + return resolveRecipient(new RecipientAddress(uuid), false); } - public RecipientId resolveRecipient(UUID uuid) { - return resolveRecipient(new SignalServiceAddress(uuid, null), false); + @Override + public RecipientId resolveRecipient(final String identifier) { + return resolveRecipient(Utils.getRecipientAddressFromIdentifier(identifier), false); } - public RecipientId resolveRecipient(String number) { - return resolveRecipient(new SignalServiceAddress(null, number), false); + public RecipientId resolveRecipient( + final String number, Supplier uuidSupplier + ) throws UnregisteredUserException { + final Optional byNumber; + synchronized (recipients) { + byNumber = findByNumberLocked(number); + } + if (byNumber.isEmpty() || byNumber.get().getAddress().getUuid().isEmpty()) { + final var uuid = uuidSupplier.get(); + if (uuid == null) { + throw new UnregisteredUserException(number, null); + } + + return resolveRecipient(new RecipientAddress(uuid, number), false); + } + return byNumber.get().getRecipientId(); } - public RecipientId resolveRecipientTrusted(SignalServiceAddress address) { + public RecipientId resolveRecipient(RecipientAddress address) { + return resolveRecipient(address, false); + } + + @Override + public RecipientId resolveRecipient(final SignalServiceAddress address) { + return resolveRecipient(new RecipientAddress(address), false); + } + + public RecipientId resolveRecipientTrusted(RecipientAddress address) { return resolveRecipient(address, true); } - public List resolveRecipientsTrusted(List addresses) { + public RecipientId resolveRecipientTrusted(SignalServiceAddress address) { + return resolveRecipient(new RecipientAddress(address), true); + } + + public List resolveRecipientsTrusted(List addresses) { final List recipientIds; final List> toBeMerged = new ArrayList<>(); synchronized (recipients) { @@ -169,10 +198,6 @@ public class RecipientStore implements ContactsStore, ProfileStore { return recipientIds; } - public RecipientId resolveRecipient(SignalServiceAddress address) { - return resolveRecipient(address, false); - } - @Override public void storeContact(final RecipientId recipientId, final Contact contact) { synchronized (recipients) { @@ -262,7 +287,7 @@ public class RecipientStore implements ContactsStore, ProfileStore { * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source. * Has no effect, if the address contains only a number or a uuid. */ - private RecipientId resolveRecipient(SignalServiceAddress address, boolean isHighTrust) { + private RecipientId resolveRecipient(RecipientAddress address, boolean isHighTrust) { final Pair> pair; synchronized (recipients) { pair = resolveRecipientLocked(address, isHighTrust); @@ -278,30 +303,26 @@ public class RecipientStore implements ContactsStore, ProfileStore { } private Pair> resolveRecipientLocked( - SignalServiceAddress address, boolean isHighTrust + RecipientAddress address, boolean isHighTrust ) { - final var byNumber = !address.getNumber().isPresent() + final var byNumber = address.getNumber().isEmpty() ? Optional.empty() - : findByNameLocked(address.getNumber().get()); - final var byUuid = !address.getUuid().isPresent() + : findByNumberLocked(address.getNumber().get()); + final var byUuid = address.getUuid().isEmpty() || address.getUuid().get().equals(UuidUtil.UNKNOWN_UUID) ? Optional.empty() : findByUuidLocked(address.getUuid().get()); if (byNumber.isEmpty() && byUuid.isEmpty()) { logger.debug("Got new recipient, both uuid and number are unknown"); - if (isHighTrust || !address.getUuid().isPresent() || !address.getNumber().isPresent()) { + if (isHighTrust || address.getUuid().isEmpty() || address.getNumber().isEmpty()) { return new Pair<>(addNewRecipientLocked(address), Optional.empty()); } - return new Pair<>(addNewRecipientLocked(new SignalServiceAddress(address.getUuid().get(), null)), - Optional.empty()); + return new Pair<>(addNewRecipientLocked(new RecipientAddress(address.getUuid().get())), Optional.empty()); } - if (!isHighTrust - || !address.getUuid().isPresent() - || !address.getNumber().isPresent() - || byNumber.equals(byUuid)) { + if (!isHighTrust || address.getUuid().isEmpty() || address.getNumber().isEmpty() || byNumber.equals(byUuid)) { return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty()); } @@ -317,7 +338,7 @@ public class RecipientStore implements ContactsStore, ProfileStore { "Got recipient existing with number, but different uuid, so stripping its number and adding new recipient"); updateRecipientAddressLocked(byNumber.get().getRecipientId(), - new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null)); + new RecipientAddress(byNumber.get().getAddress().getUuid().get())); return new Pair<>(addNewRecipientLocked(address), Optional.empty()); } @@ -331,7 +352,7 @@ public class RecipientStore implements ContactsStore, ProfileStore { "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number"); updateRecipientAddressLocked(byNumber.get().getRecipientId(), - new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null)); + new RecipientAddress(byNumber.get().getAddress().getUuid().get())); updateRecipientAddressLocked(byUuid.get().getRecipientId(), address); return new Pair<>(byUuid.get().getRecipientId(), Optional.empty()); } @@ -342,14 +363,14 @@ public class RecipientStore implements ContactsStore, ProfileStore { return new Pair<>(byUuid.get().getRecipientId(), byNumber.map(Recipient::getRecipientId)); } - private RecipientId addNewRecipientLocked(final SignalServiceAddress serviceAddress) { + private RecipientId addNewRecipientLocked(final RecipientAddress address) { final var nextRecipientId = nextIdLocked(); - storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, serviceAddress, null, null, null, null)); + storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, address, null, null, null, null)); return nextRecipientId; } private void updateRecipientAddressLocked( - final RecipientId recipientId, final SignalServiceAddress address + final RecipientId recipientId, final RecipientAddress address ) { final var recipient = recipients.get(recipientId); storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withAddress(address).build()); @@ -380,7 +401,7 @@ public class RecipientStore implements ContactsStore, ProfileStore { saveLocked(); } - private Optional findByNameLocked(final String number) { + private Optional findByNumberLocked(final String number) { return recipients.entrySet() .stream() .filter(entry -> entry.getValue().getAddress().getNumber().isPresent() && number.equals(entry.getValue() @@ -431,8 +452,8 @@ public class RecipientStore implements ContactsStore, ProfileStore { .map(Enum::name) .collect(Collectors.toSet())); return new Storage.Recipient(pair.getKey().getId(), - recipient.getAddress().getNumber().orNull(), - recipient.getAddress().getUuid().transform(UUID::toString).orNull(), + recipient.getAddress().getNumber().orElse(null), + recipient.getAddress().getUuid().map(UUID::toString).orElse(null), recipient.getProfileKey() == null ? null : base64.encodeToString(recipient.getProfileKey().serialize()), diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index ef0a055b..5738408d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -3,7 +3,6 @@ package org.asamk.signal.manager.storage.sessions; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.IOUtils; -import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.NoSessionException; @@ -23,6 +22,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -150,6 +150,19 @@ public class SessionStore implements SignalServiceSessionStore { } } + @Override + public Set getAllAddressesWithActiveSessions(final List addressNames) { + final var recipientIdToNameMap = addressNames.stream() + .collect(Collectors.toMap(this::resolveRecipient, name -> name)); + synchronized (cachedSessions) { + return recipientIdToNameMap.keySet() + .stream() + .flatMap(recipientId -> getKeysLocked(recipientId).stream()) + .map(key -> new SignalProtocolAddress(recipientIdToNameMap.get(key.recipientId), key.getDeviceId())) + .collect(Collectors.toSet()); + } + } + public void archiveAllSessions() { synchronized (cachedSessions) { final var keys = getKeysLocked(); @@ -198,7 +211,7 @@ public class SessionStore implements SignalServiceSessionStore { * @param identifier can be either a serialized uuid or a e164 phone number */ private RecipientId resolveRecipient(String identifier) { - return resolver.resolveRecipient(Utils.getSignalServiceAddressFromIdentifier(identifier)); + return resolver.resolveRecipient(identifier); } private Key getKey(final SignalProtocolAddress address) { diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index a2466311..3530d6ad 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -48,11 +48,11 @@ public class Utils { byte[] ownId; byte[] theirId; - if (isUuidCapable && ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) { + if (isUuidCapable) { // Version 2: UUID user version = 2; - ownId = UuidUtil.toByteArray(ownAddress.getUuid().get()); - theirId = UuidUtil.toByteArray(theirAddress.getUuid().get()); + ownId = UuidUtil.toByteArray(ownAddress.getUuid()); + theirId = UuidUtil.toByteArray(theirAddress.getUuid()); } else { // Version 1: E164 user version = 1; @@ -69,12 +69,4 @@ public class Utils { theirId, theirIdentityKey); } - - public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) { - if (UuidUtil.isUuid(identifier)) { - return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null); - } else { - return new SignalServiceAddress(null, identifier); - } - } } diff --git a/run_tests.sh b/run_tests.sh index 4ee46845..5978eed9 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -136,8 +136,8 @@ run_main -u "$NUMBER_2" listGroups -d run_main -u "$NUMBER_2" --output=json listGroups -d run_main -u "$NUMBER_1" receive run_main -u "$NUMBER_1" updateGroup -g "$GROUP_ID" -m "$NUMBER_2" -run_main -u "$NUMBER_1" block "$GROUP_ID" -run_main -u "$NUMBER_1" unblock "$GROUP_ID" +run_main -u "$NUMBER_1" --verbose block -g "$GROUP_ID" +run_main -u "$NUMBER_1" --verbose unblock -g "$GROUP_ID" ## Identities run_main -u "$NUMBER_1" listIdentities diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 5ed6af00..9433d209 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -45,7 +45,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { e.printStackTrace(); } } else if (content != null) { - final var sender = !envelope.isUnidentifiedSender() && envelope.hasSource() + final var sender = !envelope.isUnidentifiedSender() && envelope.hasSourceUuid() ? envelope.getSourceAddress() : content.getSender(); if (content.getReceiptMessage().isPresent()) { diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 0d8f312f..96603b76 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -1,12 +1,12 @@ package org.asamk.signal; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.Util; -import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.slf4j.helpers.MessageFormatter; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceContent; @@ -38,12 +38,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { @Override public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { - if (envelope.hasSource()) { + if (envelope.hasSourceUuid()) { var source = envelope.getSourceAddress(); writer.println("Envelope from: {} (device: {})", formatContact(source), envelope.getSourceDevice()); - if (source.getRelay().isPresent()) { - writer.println("Relayed by: {}", source.getRelay().get()); - } } else { writer.println("Envelope from: unknown source"); } @@ -56,8 +53,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { writer.println("Got receipt."); } else if (envelope.isSignalMessage() || envelope.isPreKeySignalMessage() || envelope.isUnidentifiedSender()) { if (exception != null) { - if (exception instanceof ProtocolUntrustedIdentityException) { - var e = (ProtocolUntrustedIdentityException) exception; + if (exception instanceof UntrustedIdentityException) { + var e = (UntrustedIdentityException) exception; writer.println( "The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message."); final var recipientName = getLegacyIdentifier(m.resolveSignalServiceAddress(e.getSender())); @@ -630,7 +627,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { private void printMention( PlainTextWriter writer, SignalServiceDataMessage.Mention mention ) { - final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null)); + final var address = m.resolveSignalServiceAddress(mention.getUuid()); writer.println("- {}: {} (length: {})", formatContact(address), mention.getStart(), mention.getLength()); } diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index 8d651592..8c1b9fb2 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -74,9 +74,14 @@ public class JoinGroupCommand implements JsonRpcLocalCommand { } catch (GroupPatchNotAcceptedException e) { throw new UserErrorException("Failed to join group, maybe already a member"); } catch (IOException e) { - throw new IOErrorException("Failed to send message: " + e.getMessage()); + throw new IOErrorException("Failed to send message: " + + e.getMessage() + + " (" + + e.getClass().getSimpleName() + + ")"); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (GroupLinkNotActiveException e) { throw new UserErrorException("Group link is not valid: " + e.getMessage()); } diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index b39f9ec9..5e609a48 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -8,7 +8,6 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.manager.Manager; -import java.util.UUID; import java.util.stream.Collectors; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -47,7 +46,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand { final var address = m.resolveSignalServiceAddress(contactPair.first()); final var contact = contactPair.second(); return new JsonContact(address.getNumber().orNull(), - address.getUuid().transform(UUID::toString).orNull(), + address.getUuid().toString(), contact.getName(), contact.isBlocked(), contact.getMessageExpirationTime()); diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index a6a9a2f1..b53577be 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -16,7 +16,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Set; -import java.util.UUID; import java.util.stream.Collectors; public class ListGroupsCommand implements JsonRpcLocalCommand { @@ -46,8 +45,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { private static Set resolveJsonMembers(Manager m, Set addresses) { return addresses.stream() .map(m::resolveSignalServiceAddress) - .map(address -> new JsonGroupMember(address.getNumber().orNull(), - address.getUuid().transform(UUID::toString).orNull())) + .map(address -> new JsonGroupMember(address.getNumber().orNull(), address.getUuid().toString())) .collect(Collectors.toSet()); } diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index c859996e..02cd1d9f 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -18,7 +18,6 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Base64; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; public class ListIdentitiesCommand implements JsonRpcLocalCommand { @@ -72,7 +71,7 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(address, id.getIdentityKey())); var scannableSafetyNumber = m.computeSafetyNumberForScanning(address, id.getIdentityKey()); return new JsonIdentity(address.getNumber().orNull(), - address.getUuid().transform(UUID::toString).orNull(), + address.getUuid().toString(), Hex.toString(id.getFingerprint()), safetyNumber, scannableSafetyNumber == null diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 03bf232b..c64d19cc 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -66,7 +66,11 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { m.deleteGroup(groupId); } } catch (IOException e) { - throw new IOErrorException("Failed to send message: " + e.getMessage()); + throw new IOErrorException("Failed to send message: " + + e.getMessage() + + " (" + + e.getClass().getSimpleName() + + ")"); } catch (GroupNotFoundException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } catch (LastGroupAdminException e) { diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index e482dd58..6e1e92f7 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -64,7 +64,8 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } @@ -104,7 +105,8 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } catch (Signal.Error.GroupNotFound e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index c29d0268..7ab445fc 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -85,7 +85,8 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { m.sendEndSessionMessage(singleRecipients); return; } catch (IOException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } @@ -108,7 +109,8 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { outputResult(outputWriter, results.getTimestamp()); ErrorUtils.handleSendMessageResults(results.getResults()); } catch (AttachmentInvalidException | IOException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } @@ -141,9 +143,11 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { signal.sendEndSessionMessage(recipients); return; } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage()); + throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } @@ -182,7 +186,8 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { outputResult(outputWriter, timestamp); return; } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage()); + throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (DBusExecutionException e) { throw new UnexpectedErrorException("Failed to send note to self message: " + e.getMessage()); } @@ -194,9 +199,11 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { } catch (UnknownObject e) { throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage()); + throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 98e5f5ec..11a16b2e 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -80,7 +80,8 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } @@ -127,7 +128,8 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } catch (Signal.Error.GroupNotFound e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } diff --git a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java index 70e2f015..0d5772ec 100644 --- a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java @@ -7,8 +7,8 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.util.CommandUtil; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import java.io.IOException; @@ -51,7 +51,8 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { throw new UserErrorException("Unknown receipt type: " + type); } } catch (IOException | UntrustedIdentityException e) { - throw new UserErrorException("Failed to send message: " + e.getMessage()); + throw new UserErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } } diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index ace4da85..3a965e47 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -8,13 +8,13 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import java.io.IOException; import java.util.HashSet; @@ -59,7 +59,8 @@ public class SendTypingCommand implements JsonRpcLocalCommand { try { m.sendTypingMessage(action, recipientIdentifiers); } catch (IOException | UntrustedIdentityException e) { - throw new UserErrorException("Failed to send message: " + e.getMessage()); + throw new UserErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index a8d556f3..6df70ac2 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -174,7 +174,8 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } @@ -210,7 +211,8 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } catch (Signal.Error.AttachmentInvalid e) { throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 6a7cc764..0bb0c435 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -21,6 +21,7 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.File; @@ -244,6 +245,8 @@ public class DbusSignalImpl implements Signal { m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); + } catch (UnregisteredUserException e) { + throw new Error.Failure("Contact is not registered."); } } diff --git a/src/main/java/org/asamk/signal/json/JsonMention.java b/src/main/java/org/asamk/signal/json/JsonMention.java index f0c66d00..b24768b7 100644 --- a/src/main/java/org/asamk/signal/json/JsonMention.java +++ b/src/main/java/org/asamk/signal/json/JsonMention.java @@ -4,9 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; - -import java.util.UUID; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -29,10 +26,10 @@ public class JsonMention { final int length; JsonMention(SignalServiceDataMessage.Mention mention, Manager m) { - final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null)); + final var address = m.resolveSignalServiceAddress(mention.getUuid()); this.name = getLegacyIdentifier(address); this.number = address.getNumber().orNull(); - this.uuid = address.getUuid().transform(UUID::toString).orNull(); + this.uuid = address.getUuid().toString(); this.start = mention.getStart(); this.length = mention.getLength(); } diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index 814952aa..7b884b0e 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -5,14 +5,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.Signal; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.RecipientIdentifier; -import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.List; -import java.util.UUID; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -34,10 +33,6 @@ public class JsonMessageEnvelope { @JsonProperty final Integer sourceDevice; - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - final String relay; - @JsonProperty final long timestamp; @@ -64,34 +59,30 @@ public class JsonMessageEnvelope { public JsonMessageEnvelope( SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception, Manager m ) { - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - var source = envelope.getSourceAddress(); + if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { + var source = m.resolveSignalServiceAddress(envelope.getSourceAddress()); this.source = getLegacyIdentifier(source); this.sourceNumber = source.getNumber().orNull(); - this.sourceUuid = source.getUuid().transform(UUID::toString).orNull(); + this.sourceUuid = source.getUuid().toString(); this.sourceDevice = envelope.getSourceDevice(); - this.relay = source.getRelay().orNull(); } else if (envelope.isUnidentifiedSender() && content != null) { - final var source = content.getSender(); + final var source = m.resolveSignalServiceAddress(content.getSender()); this.source = getLegacyIdentifier(source); this.sourceNumber = source.getNumber().orNull(); - this.sourceUuid = source.getUuid().transform(UUID::toString).orNull(); + this.sourceUuid = source.getUuid().toString(); this.sourceDevice = content.getSenderDevice(); - this.relay = null; - } else if (exception instanceof ProtocolUntrustedIdentityException) { - var e = (ProtocolUntrustedIdentityException) exception; + } else if (exception instanceof UntrustedIdentityException) { + var e = (UntrustedIdentityException) exception; final var source = m.resolveSignalServiceAddress(e.getSender()); this.source = getLegacyIdentifier(source); this.sourceNumber = source.getNumber().orNull(); - this.sourceUuid = source.getUuid().transform(UUID::toString).orNull(); + this.sourceUuid = source.getUuid().toString(); this.sourceDevice = e.getSenderDevice(); - this.relay = null; } else { this.source = null; this.sourceNumber = null; this.sourceUuid = null; this.sourceDevice = null; - this.relay = null; } String name; try { @@ -129,7 +120,6 @@ public class JsonMessageEnvelope { sourceUuid = null; sourceName = null; sourceDevice = null; - relay = null; timestamp = messageReceived.getTimestamp(); receiptMessage = null; dataMessage = new JsonDataMessage(messageReceived); @@ -144,7 +134,6 @@ public class JsonMessageEnvelope { sourceUuid = null; sourceName = null; sourceDevice = null; - relay = null; timestamp = receiptReceived.getTimestamp(); receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp)); dataMessage = null; @@ -159,7 +148,6 @@ public class JsonMessageEnvelope { sourceUuid = null; sourceName = null; sourceDevice = null; - relay = null; timestamp = messageReceived.getTimestamp(); receiptMessage = null; dataMessage = null; diff --git a/src/main/java/org/asamk/signal/json/JsonQuote.java b/src/main/java/org/asamk/signal/json/JsonQuote.java index ecd31c1a..73af895a 100644 --- a/src/main/java/org/asamk/signal/json/JsonQuote.java +++ b/src/main/java/org/asamk/signal/json/JsonQuote.java @@ -8,7 +8,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import java.util.ArrayList; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -43,7 +42,7 @@ public class JsonQuote { final var address = m.resolveSignalServiceAddress(quote.getAuthor()); this.author = getLegacyIdentifier(address); this.authorNumber = address.getNumber().orNull(); - this.authorUuid = address.getUuid().transform(UUID::toString).orNull(); + this.authorUuid = address.getUuid().toString(); this.text = quote.getText(); if (quote.getMentions() != null && quote.getMentions().size() > 0) { diff --git a/src/main/java/org/asamk/signal/json/JsonReaction.java b/src/main/java/org/asamk/signal/json/JsonReaction.java index ecea15fe..cc80ee84 100644 --- a/src/main/java/org/asamk/signal/json/JsonReaction.java +++ b/src/main/java/org/asamk/signal/json/JsonReaction.java @@ -5,8 +5,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction; -import java.util.UUID; - import static org.asamk.signal.util.Util.getLegacyIdentifier; public class JsonReaction { @@ -35,7 +33,7 @@ public class JsonReaction { final var address = m.resolveSignalServiceAddress(reaction.getTargetAuthor()); this.targetAuthor = getLegacyIdentifier(address); this.targetAuthorNumber = address.getNumber().orNull(); - this.targetAuthorUuid = address.getUuid().transform(UUID::toString).orNull(); + this.targetAuthorUuid = address.getUuid().toString(); this.targetSentTimestamp = reaction.getTargetSentTimestamp(); this.isRemove = reaction.isRemove(); } diff --git a/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java index 28c9d936..e2c92bac 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java @@ -6,8 +6,6 @@ import org.asamk.Signal; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; -import java.util.UUID; - import static org.asamk.signal.util.Util.getLegacyIdentifier; class JsonSyncDataMessage extends JsonDataMessage { @@ -29,7 +27,7 @@ class JsonSyncDataMessage extends JsonDataMessage { final var address = transcriptMessage.getDestination().get(); this.destination = getLegacyIdentifier(address); this.destinationNumber = address.getNumber().orNull(); - this.destinationUuid = address.getUuid().transform(UUID::toString).orNull(); + this.destinationUuid = address.getUuid().toString(); } else { this.destination = null; this.destinationNumber = null; diff --git a/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java index df307b45..042ed7e4 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java @@ -4,8 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; -import java.util.UUID; - import static org.asamk.signal.util.Util.getLegacyIdentifier; class JsonSyncReadMessage { @@ -27,7 +25,7 @@ class JsonSyncReadMessage { final var sender = readMessage.getSender(); this.sender = getLegacyIdentifier(sender); this.senderNumber = sender.getNumber().orNull(); - this.senderUuid = sender.getUuid().transform(UUID::toString).orNull(); + this.senderUuid = sender.getUuid().toString(); this.timestamp = readMessage.getTimestamp(); } } diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 01a79dd1..b4954bf3 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -60,7 +60,7 @@ public class Util { } public static String getLegacyIdentifier(final SignalServiceAddress address) { - return address.getNumber().or(() -> address.getUuid().get().toString()); + return address.getNumber().or(() -> address.getUuid().toString()); } public static ObjectMapper createJsonObjectMapper() { -- 2.51.0 From 5743cf4455d98ac16231866c43c877c10c016a89 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 30 Aug 2021 13:33:54 +0200 Subject: [PATCH 14/16] Improve dbus register error message if called with invalid number --- .../java/org/asamk/signal/manager/storage/SignalAccount.java | 3 ++- src/main/java/org/asamk/signal/BaseConfig.java | 2 +- .../java/org/asamk/signal/dbus/DbusSignalControlImpl.java | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index c972c2c7..4e240887 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -21,6 +21,7 @@ import org.asamk.signal.manager.storage.protocol.SignalProtocolStore; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore; import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientStore; import org.asamk.signal.manager.storage.sessions.SessionStore; @@ -789,7 +790,7 @@ public class SignalAccount implements Closeable { } public RecipientId getSelfRecipientId() { - return recipientStore.resolveRecipientTrusted(getSelfAddress()); + return recipientStore.resolveRecipientTrusted(new RecipientAddress(uuid, username)); } public String getEncryptedDeviceName() { diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java index bb8db7d2..04c1ac8a 100644 --- a/src/main/java/org/asamk/signal/BaseConfig.java +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -5,7 +5,7 @@ public class BaseConfig { public final static String PROJECT_NAME = BaseConfig.class.getPackage().getImplementationTitle(); public final static String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); - final static String USER_AGENT_SIGNAL_ANDROID = "Signal-Android/5.12.4"; + final static String USER_AGENT_SIGNAL_ANDROID = "Signal-Android/5.22.3"; final static String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + "/" + PROJECT_VERSION; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java index 35f530b0..6ec8d964 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -13,6 +13,7 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.KeyBackupServicePinException; import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.IOException; import java.net.URI; @@ -99,6 +100,10 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { public void registerWithCaptcha( final String number, final boolean voiceVerification, final String captcha ) throws Error.Failure, Error.InvalidNumber { + if (!PhoneNumberFormatter.isValidNumber(number, null)) { + throw new SignalControl.Error.InvalidNumber( + "Invalid username (phone number), make sure you include the country code."); + } try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) { registrationManager.register(voiceVerification, captcha); } catch (CaptchaRequiredException e) { -- 2.51.0 From 32150b1aaa32888c5179d664a9a497a13d3f4bfa Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 30 Aug 2021 13:39:27 +0200 Subject: [PATCH 15/16] Move all message decryption to IncomingMessageHandler --- .../org/asamk/signal/manager/Manager.java | 47 +++++++-------- .../helper/IncomingMessageHandler.java | 59 +++++++++++++++---- 2 files changed, 68 insertions(+), 38 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 9e38853b..87b89913 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -58,7 +58,6 @@ import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; -import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; @@ -818,37 +817,33 @@ public class Manager implements Closeable { ) { var envelope = cachedMessage.loadEnvelope(); if (envelope == null) { + cachedMessage.delete(); return null; } - SignalServiceContent content = null; - List actions = null; - if (!envelope.isReceipt()) { - try { - content = dependencies.getCipher().decrypt(envelope); - } catch (ProtocolUntrustedIdentityException e) { - if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { - // Envelope is more than a month old, cleaning up. - cachedMessage.delete(); - return null; - } - if (!envelope.hasSourceUuid()) { - final var identifier = e.getSender(); - final var recipientId = account.getRecipientStore().resolveRecipient(identifier); - try { - account.getMessageCache().replaceSender(cachedMessage, recipientId); - } catch (IOException ioException) { - logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage()); - } - } - return null; - } catch (Exception er) { - // All other errors are not recoverable, so delete the cached message + + final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler); + final var actions = result.first(); + final var exception = result.second(); + + if (exception instanceof UntrustedIdentityException) { + if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { + // Envelope is more than a month old, cleaning up. cachedMessage.delete(); return null; } - actions = incomingMessageHandler.handleMessage(envelope, content, ignoreAttachments); + if (!envelope.hasSourceUuid()) { + final var identifier = ((UntrustedIdentityException) exception).getSender(); + final var recipientId = account.getRecipientStore().resolveRecipient(identifier); + try { + account.getMessageCache().replaceSender(cachedMessage, recipientId); + } catch (IOException ioException) { + logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage()); + } + } + return null; } - handler.handleMessage(envelope, content, null); + + // If successful and for all other errors that are not recoverable, delete the cached message cachedMessage.delete(); return actions; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index f28b3638..57b71ee1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -79,6 +79,28 @@ public final class IncomingMessageHandler { this.jobExecutor = jobExecutor; } + public Pair, Exception> handleRetryEnvelope( + final SignalServiceEnvelope envelope, + final boolean ignoreAttachments, + final Manager.ReceiveMessageHandler handler + ) { + SignalServiceContent content = null; + if (!envelope.isReceipt()) { + try { + content = dependencies.getCipher().decrypt(envelope); + } catch (ProtocolUntrustedIdentityException e) { + final var recipientId = account.getRecipientStore().resolveRecipient(e.getSender()); + final var exception = new UntrustedIdentityException(addressResolver.resolveSignalServiceAddress( + recipientId), e.getSenderDevice()); + return new Pair<>(List.of(), exception); + } catch (Exception e) { + return new Pair<>(List.of(), e); + } + } + final var actions = checkAndHandleMessage(envelope, content, ignoreAttachments, handler, null); + return new Pair<>(actions, null); + } + public Pair, Exception> handleEnvelope( final SignalServiceEnvelope envelope, final boolean ignoreAttachments, @@ -108,35 +130,48 @@ public final class IncomingMessageHandler { } catch (Exception e) { exception = e; } - - if (!envelope.hasSourceUuid() && content != null) { - // Store uuid if we don't have it already - // address/uuid is validated by unidentified sender certificate - account.getRecipientStore().resolveRecipientTrusted(content.getSender()); - } } + actions.addAll(checkAndHandleMessage(envelope, content, ignoreAttachments, handler, exception)); + return new Pair<>(actions, exception); + } + + private List checkAndHandleMessage( + final SignalServiceEnvelope envelope, + final SignalServiceContent content, + final boolean ignoreAttachments, + final Manager.ReceiveMessageHandler handler, + final Exception exception + ) { + if (!envelope.hasSourceUuid() && content != null) { + // Store uuid if we don't have it already + // address/uuid is validated by unidentified sender certificate + account.getRecipientStore().resolveRecipientTrusted(content.getSender()); + } if (isMessageBlocked(envelope, content)) { logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); + return List.of(); } else if (isNotAllowedToSendToGroup(envelope, content)) { logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", (envelope.hasSourceUuid() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), envelope.getTimestamp()); + return List.of(); } else { - actions.addAll(handleMessage(envelope, content, ignoreAttachments)); + List actions; + if (content != null) { + actions = handleMessage(envelope, content, ignoreAttachments); + } else { + actions = List.of(); + } handler.handleMessage(envelope, content, exception); + return actions; } - return new Pair<>(actions, exception); } public List handleMessage( SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments ) { var actions = new ArrayList(); - if (content == null) { - return actions; - } - final RecipientId sender; if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); -- 2.51.0 From 7a3522dc010fd15601b040547807c617276ec6c2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 30 Aug 2021 13:55:25 +0200 Subject: [PATCH 16/16] Prevent endless loop when receiving contact sync message --- .../java/org/asamk/signal/manager/helper/SyncHelper.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 461706f0..3cc76b28 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -222,8 +222,13 @@ public class SyncHelper { try { c = s.read(); } catch (IOException e) { - logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage()); - continue; + if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) { + logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage()); + continue; + } else { + logger.warn("Failed to read sync contacts", e); + break; + } } if (c == null) { break; -- 2.51.0