X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/11c90fa0324347bf2b7ba56855ccd45898141569..944c3327ee4ea552d968fca1db89fa3318cec2d9:/lib/src/main/java/org/asamk/signal/manager/Manager.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 e2306dec..3857d803 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -17,6 +17,10 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.SendGroupMessageResults; +import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; @@ -30,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; @@ -41,8 +46,8 @@ 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; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; @@ -55,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; @@ -81,15 +72,16 @@ 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; +import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; 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; @@ -99,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; @@ -117,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; @@ -151,6 +139,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -178,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; @@ -226,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()); @@ -244,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() { @@ -267,7 +262,11 @@ public class Manager implements Closeable { } public static Manager init( - String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent + String username, + File settingsPath, + ServiceEnvironment serviceEnvironment, + String userAgent, + final TrustNewIdentity trustNewIdentity ) throws IOException, NotRegisteredException { var pathConfig = PathConfig.createDefault(settingsPath); @@ -275,7 +274,7 @@ public class Manager implements Closeable { throw new NotRegisteredException(); } - var account = SignalAccount.load(pathConfig.getDataPath(), username, true); + var account = SignalAccount.load(pathConfig.getDataPath(), username, true, trustNewIdentity); if (!account.isRegistered()) { throw new NotRegisteredException(); @@ -304,7 +303,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); @@ -327,16 +326,29 @@ public class Manager implements Closeable { * This is used for checking a set of phone numbers for registration on Signal * * @param numbers The set of phone number in question - * @return A map of numbers to booleans. True if registered, false otherwise. Should never be null + * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. * @throws IOException if its unable to get the contacts to check if they're registered */ - public Map areUsersRegistered(Set numbers) throws IOException { - // Note "contactDetails" has no optionals. It only gives us info on users who are registered - var contactDetails = getRegisteredUsers(numbers); + public Map> areUsersRegistered(Set numbers) throws IOException { + Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { + try { + return canonicalizePhoneNumber(n); + } catch (InvalidNumberException e) { + return ""; + } + })); - var registeredUsers = contactDetails.keySet(); + // Note "contactDetails" has no optionals. It only gives us info on users who are registered + var contactDetails = getRegisteredUsers(canonicalizedNumbers.values() + .stream() + .filter(s -> !s.isEmpty()) + .collect(Collectors.toSet())); - return numbers.stream().collect(Collectors.toMap(x -> x, registeredUsers::contains)); + return numbers.stream().collect(Collectors.toMap(n -> n, n -> { + final var number = canonicalizedNumbers.get(n); + final var uuid = contactDetails.get(number); + return new Pair<>(number.isEmpty() ? null : number, uuid); + })); } public void updateAccountAttributes() throws IOException { @@ -639,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) { @@ -657,94 +660,15 @@ 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(); } - public Pair> sendGroupMessage( - String messageText, List attachments, GroupId groupId - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - final var messageBuilder = createMessageBuilder().withBody(messageText); - if (attachments != null) { - messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); - } - - return sendHelper.sendAsGroupMessage(messageBuilder, groupId); - } - - public Pair> sendGroupMessageReaction( - String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId - ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException { - var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor); - var reaction = new SignalServiceDataMessage.Reaction(emoji, - remove, - resolveSignalServiceAddress(targetAuthorRecipientId), - targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withReaction(reaction); - - return sendHelper.sendAsGroupMessage(messageBuilder, groupId); - } - - public Pair> sendQuitGroupMessage( - GroupId groupId, Set groupAdmins - ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException { - var group = getGroupForUpdating(groupId); - if (group instanceof GroupInfoV1) { - return quitGroupV1((GroupInfoV1) group); - } - + public SendGroupMessageResults sendQuitGroupMessage( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { 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 Pair> quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) - .withId(groupInfoV1.getGroupId().serialize()) - .build(); - - var messageBuilder = createMessageBuilder().asGroupMessage(group); - groupInfoV1.removeMember(account.getSelfRecipientId()); - account.getGroupStore().updateGroup(groupInfoV1); - return sendHelper.sendGroupMessage(messageBuilder.build(), - groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - } - - private Pair> 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); - var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); - account.getGroupStore().updateGroup(groupInfoV2); - return sendHelper.sendGroupMessage(messageBuilder.build(), - groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + return groupHelper.quitGroup(groupId, newAdmins); } public void deleteGroup(GroupId groupId) throws IOException { @@ -752,66 +676,29 @@ public class Manager implements Closeable { avatarStore.deleteGroupAvatar(groupId); } - public Pair> createGroup( - String name, List members, File avatarFile - ) throws IOException, AttachmentInvalidException, InvalidNumberException { - return createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); - } - - private Pair> createGroup( - String name, Set members, File avatarFile + 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); - - SignalServiceDataMessage.Builder messageBuilder; - 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.second()); - } - - 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)); - } - messageBuilder = getGroupUpdateMessageBuilder(gv2, null); - account.getGroupStore().updateGroup(gv2); - - final var result = sendHelper.sendGroupMessage(messageBuilder.build(), - gv2.getMembersIncludingPendingWithout(selfRecipientId)); - return new Pair<>(gv2.getGroupId(), result.second()); + return groupHelper.createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); } - public Pair> updateGroup( + public SendGroupMessageResults updateGroup( GroupId groupId, String name, String description, - List members, - List removeMembers, - List admins, - List removeAdmins, + Set members, + Set removeMembers, + Set admins, + Set removeAdmins, boolean resetGroupLink, GroupLinkState groupLinkState, GroupPermission addMemberPermission, GroupPermission editDetailsPermission, File avatarFile, - Integer expirationTimer - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { - return updateGroup(groupId, + Integer expirationTimer, + Boolean isAnnouncementGroup + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { + return groupHelper.updateGroup(groupId, name, description, members == null ? null : getRecipientIds(members), @@ -823,363 +710,114 @@ public class Manager implements Closeable { addMemberPermission, editDetailsPermission, avatarFile, - expirationTimer); - } - - private Pair> 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 - ) 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); - } 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); - } - } - - final var gv1 = (GroupInfoV1) group; - final var result = updateGroupV1(gv1, name, members, avatarFile); - if (expirationTimer != null) { - setExpirationTimer(gv1, expirationTimer); - } - return result; + expirationTimer, + isAnnouncementGroup); } - private Pair> updateGroupV1( - final GroupInfoV1 gv1, final String name, final Set members, final File avatarFile - ) throws IOException, AttachmentInvalidException { - updateGroupV1Details(gv1, name, members, avatarFile); - var messageBuilder = getGroupUpdateMessageBuilder(gv1); - - account.getGroupStore().updateGroup(gv1); - - return sendHelper.sendGroupMessage(messageBuilder.build(), - gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + public Pair joinGroup( + GroupInviteLinkUrl inviteLinkUrl + ) throws IOException, GroupLinkNotActiveException { + return groupHelper.joinGroup(inviteLinkUrl); } - 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"); + public SendMessageResults sendMessage( + SignalServiceDataMessage.Builder messageBuilder, Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + var results = new HashMap>(); + long timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); + for (final var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + final var result = sendHelper.sendMessage(messageBuilder, recipientId); + results.put(recipient, List.of(result)); + } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { + final var result = sendHelper.sendSelfMessage(messageBuilder); + results.put(recipient, List.of(result)); + } else if (recipient instanceof RecipientIdentifier.Group) { + final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId); + results.put(recipient, result); } - - g.addMembers(members); - } - - if (avatarFile != null) { - avatarStore.storeGroupAvatar(g.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); } + return new SendMessageResults(timestamp, results); } - private Pair> 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, - Integer expirationTimer - ) throws IOException { - Pair> 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 (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)); + public void sendTypingMessage( + SignalServiceTypingMessage.Action action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + final var timestamp = System.currentTimeMillis(); + for (var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + sendHelper.sendTypingMessage(message, recipientId); + } else if (recipient instanceof RecipientIdentifier.Group) { + final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); + sendHelper.sendGroupTypingMessage(message, groupId); } - 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(), List.of()); } - - final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); - - return new Pair<>(group.getGroupId(), result.second()); } - private Pair> 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)); - - final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); - account.getGroupStore().updateGroup(group); - return sendHelper.sendGroupMessage(messageBuilder.build(), members); - } - - 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); - } - } - - Pair> sendGroupInfoMessage( + 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 sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(recipientId)); + return groupHelper.sendGroupInfoMessage(groupId, 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 createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); + SendGroupMessageResults sendGroupInfoRequest( + GroupIdV1 groupId, SignalServiceAddress recipient + ) throws IOException { + final var recipientId = resolveRecipient(recipient); + return groupHelper.sendGroupInfoRequest(groupId, recipientId); } - private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { - var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) - .withRevision(g.getGroup().getRevision()) - .withSignedGroupChange(signedGroupChange); - return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); - } + public void sendReadReceipt( + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, + messageIds, + System.currentTimeMillis()); - Pair> sendGroupInfoRequest( - GroupIdV1 groupId, SignalServiceAddress recipient - ) throws IOException { - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + } - var messageBuilder = createMessageBuilder().asGroupMessage(group.build()); + public void sendViewedReceipt( + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, + messageIds, + System.currentTimeMillis()); - // Send group info request message to the recipient who sent us a message with this groupId - return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(resolveRecipient(recipient))); + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); } - void sendReceipt( - SignalServiceAddress remoteAddress, long messageId + void sendDeliveryReceipt( + SignalServiceAddress remoteAddress, List messageIds ) throws IOException, UntrustedIdentityException { var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, - List.of(messageId), + messageIds, System.currentTimeMillis()); sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); } - public Pair> sendMessage( - String messageText, List attachments, List recipients - ) throws IOException, AttachmentInvalidException, InvalidNumberException { - final var messageBuilder = createMessageBuilder().withBody(messageText); - if (attachments != null) { - var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); + public SendMessageResults sendMessage( + Message message, Set recipients + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException { + final var messageBuilder = SignalServiceDataMessage.newBuilder(); + applyMessage(messageBuilder, message); + return sendMessage(messageBuilder, recipients); + } + + private void applyMessage( + 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(); @@ -1194,55 +832,43 @@ public class Manager implements Closeable { messageBuilder.withAttachments(attachmentPointers); } - return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); } - public Pair sendSelfMessage( - String messageText, List attachments - ) throws IOException, AttachmentInvalidException { - final var messageBuilder = createMessageBuilder().withBody(messageText); - if (attachments != null) { - messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); - } - return sendHelper.sendSelfMessage(messageBuilder); - } - - public Pair> sendRemoteDeleteMessage( - long targetSentTimestamp, List recipients - ) throws IOException, InvalidNumberException { - var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); - return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); - } - - public Pair> sendGroupRemoteDeleteMessage( - long targetSentTimestamp, GroupId groupId + public SendMessageResults sendRemoteDeleteMessage( + long targetSentTimestamp, Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); - return sendHelper.sendAsGroupMessage(messageBuilder, groupId); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); + return sendMessage(messageBuilder, recipients); } - public Pair> sendMessageReaction( - String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients - ) throws IOException, InvalidNumberException { - var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor); + public SendMessageResults sendMessageReaction( + String emoji, + boolean remove, + RecipientIdentifier.Single targetAuthor, + long targetSentTimestamp, + Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + var targetAuthorRecipientId = resolveRecipient(targetAuthor); var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withReaction(reaction); - return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); + return sendMessage(messageBuilder, recipients); } - public Pair> sendEndSessionMessage(List recipients) throws IOException, InvalidNumberException { - var messageBuilder = createMessageBuilder().asEndSessionMessage(); + public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { + var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); - final var recipientIds = getRecipientIds(recipients); try { - return sendHelper.sendMessage(messageBuilder, recipientIds); + return sendMessage(messageBuilder, + recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new AssertionError(e); } finally { - for (var recipientId : recipientIds) { + for (var recipient : recipients) { + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); handleEndSession(recipientId); } } @@ -1255,28 +881,31 @@ public class Manager implements Closeable { } } - public void setContactName(String number, String name) throws InvalidNumberException, NotMasterDeviceException { + public void setContactName( + RecipientIdentifier.Single recipient, String name + ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - final var recipientId = canonicalizeAndResolveRecipient(number); + 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()); } public void setContactBlocked( - String number, boolean blocked - ) throws InvalidNumberException, NotMasterDeviceException { + RecipientIdentifier.Single recipient, boolean blocked + ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - setContactBlocked(canonicalizeAndResolveRecipient(number), blocked); + 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); + // TODO cycle our profile key account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); } @@ -1287,9 +916,26 @@ public class Manager implements Closeable { } group.setBlocked(blocked); + // TODO cycle our profile key account.getGroupStore().updateGroup(group); } + /** + * Change the expiration timer for a contact + */ + public void setExpirationTimer( + RecipientIdentifier.Single recipient, int messageExpirationTimer + ) throws IOException { + var recipientId = resolveRecipient(recipient); + setExpirationTimer(recipientId, messageExpirationTimer); + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + try { + sendMessage(messageBuilder, Set.of(recipient)); + } catch (NotAGroupMemberException | GroupNotFoundException e) { + throw new AssertionError(e); + } + } + private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { var contact = account.getContactStore().getContact(recipientId); if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { @@ -1300,45 +946,13 @@ public class Manager implements Closeable { .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); } - private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException { - final var messageBuilder = createMessageBuilder().asExpirationUpdate(); - sendHelper.sendMessage(messageBuilder, Set.of(recipientId)); - } - - /** - * Change the expiration timer for a contact - */ - public void setExpirationTimer( - String number, int messageExpirationTimer - ) throws IOException, InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(number); - setExpirationTimer(recipientId, messageExpirationTimer); - sendExpirationTimerUpdate(recipientId); - } - - /** - * 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 = createMessageBuilder().asExpirationUpdate(); - sendHelper.sendAsGroupMessage(messageBuilder, groupId); - } - /** * Upload the sticker pack from path. * * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file * @return if successful, returns the URL to install the sticker pack in the signal app */ - public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException { + public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); var messageSender = dependencies.getMessageSender(); @@ -1357,7 +971,7 @@ public class Manager implements Closeable { "pack_id=" + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) + "&pack_key=" - + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)).toString(); + + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)); } catch (URISyntaxException e) { throw new AssertionError(e); } @@ -1427,12 +1041,12 @@ public class Manager implements Closeable { return certificate; } - private Set getRecipientIds(Collection numbers) throws InvalidNumberException { - final var signalServiceAddresses = new HashSet(numbers.size()); + private Set getRecipientIds(Collection recipients) { + final var signalServiceAddresses = new HashSet(recipients.size()); final var addressesMissingUuid = new HashSet(); - for (var number : numbers) { - final var resolvedAddress = resolveSignalServiceAddress(canonicalizeAndResolveRecipient(number)); + for (var number : recipients) { + final var resolvedAddress = resolveSignalServiceAddress(resolveRecipient(number)); if (resolvedAddress.getUuid().isPresent()) { signalServiceAddresses.add(resolvedAddress); } else { @@ -1477,44 +1091,26 @@ public class Manager implements Closeable { } private Map getRegisteredUsers(final Set numbers) throws IOException { + final Map registeredUsers; try { - return dependencies.getAccountManager() + registeredUsers = dependencies.getAccountManager() .getRegisteredUsers(ServiceConfig.getIasKeyStore(), numbers, serviceEnvironmentConfig.getCdsMrenclave()); } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { throw new IOException(e); } - } - - public void sendTypingMessage( - TypingAction action, Set recipients - ) throws IOException, UntrustedIdentityException, InvalidNumberException { - final var timestamp = System.currentTimeMillis(); - var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.absent()); - sendHelper.sendTypingMessage(message, getRecipientIds(recipients)); - } - - public void sendGroupTypingMessage( - TypingAction action, GroupId groupId - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { - final var timestamp = System.currentTimeMillis(); - final var message = new SignalServiceTypingMessage(action.toSignalService(), - timestamp, - Optional.of(groupId.serialize())); - sendHelper.sendGroupTypingMessage(message, groupId); - } - private SignalServiceDataMessage.Builder createMessageBuilder() { - final var timestamp = System.currentTimeMillis(); + // Store numbers as recipients so we have the number/uuid association + registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); - var messageBuilder = SignalServiceDataMessage.newBuilder(); - messageBuilder.withTimestamp(timestamp); - return messageBuilder; + return registeredUsers; } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException { - return dependencies.getCipher().decrypt(envelope); + public void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + sendTypingMessage(action.toSignalService(), recipients); } private void handleEndSession(RecipientId recipientId) { @@ -1544,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()) { @@ -1588,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); } @@ -1673,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()) { @@ -1740,16 +1277,7 @@ public class Manager implements Closeable { queuedActions.addAll(actions); } } - for (var action : queuedActions) { - try { - action.execute(this); - } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - logger.warn("Message action failed.", e); - } - } + handleQueuedActions(queuedActions); } private List retryFailedReceivedMessage( @@ -1763,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(); @@ -1793,10 +1321,10 @@ public class Manager implements Closeable { boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler - ) throws IOException, InterruptedException { + ) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); - Set queuedActions = null; + Set queuedActions = new HashSet<>(); final var signalWebSocket = dependencies.getSignalWebSocket(); signalWebSocket.connect(); @@ -1825,27 +1353,16 @@ public class Manager implements Closeable { // Received indicator that server queue is empty hasCaughtUpWithOldMessages = true; - if (queuedActions != null) { - for (var action : queuedActions) { - try { - action.execute(this); - } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - logger.warn("Message action failed.", e); - } - } - queuedActions.clear(); - queuedActions = null; - } + handleQueuedActions(queuedActions); + queuedActions.clear(); // Continue to wait another timeout for new messages continue; } } catch (AssertionError e) { if (e.getCause() instanceof InterruptedException) { - throw (InterruptedException) e.getCause(); + Thread.currentThread().interrupt(); + break; } else { throw e; } @@ -1863,10 +1380,9 @@ public class Manager implements Closeable { // address/uuid in envelope is sent by server resolveRecipientTrusted(envelope.getSourceAddress()); } - final var notAGroupMember = isNotAGroupMember(envelope, content); if (!envelope.isReceipt()) { try { - content = decryptMessage(envelope); + content = dependencies.getCipher().decrypt(envelope); } catch (Exception e) { exception = e; } @@ -1893,16 +1409,16 @@ public class Manager implements Closeable { } } } else { - if (queuedActions == null) { - queuedActions = new HashSet<>(); - } 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 (notAGroupMember) { - logger.info("Ignoring a message from a non group member: {}", 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); } @@ -1924,6 +1440,20 @@ public class Manager implements Closeable { } } } + handleQueuedActions(queuedActions); + } + + private void handleQueuedActions(final Set queuedActions) { + for (var action : queuedActions) { + try { + action.execute(this); + } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + logger.warn("Message action failed.", e); + } + } } private boolean isMessageBlocked( @@ -1955,8 +1485,8 @@ public class Manager implements Closeable { return false; } - public boolean isContactBlocked(final String identifier) throws InvalidNumberException { - final var recipientId = canonicalizeAndResolveRecipient(identifier); + public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { + final var recipientId = resolveRecipient(recipient); return isContactBlocked(recipientId); } @@ -1965,7 +1495,7 @@ public class Manager implements Closeable { return sourceContact != null && sourceContact.isBlocked(); } - private boolean isNotAGroupMember( + private boolean isNotAllowedToSendToGroup( SignalServiceEnvelope envelope, SignalServiceContent content ) { SignalServiceAddress source; @@ -1977,23 +1507,32 @@ public class Manager implements Closeable { return false; } - if (content != null && content.getDataMessage().isPresent()) { - var message = content.getDataMessage().get(); - if (message.getGroupContext().isPresent()) { - 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 && !group.isMember(resolveRecipient(source))) { - return true; - } + 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; } } - return false; + + var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + var group = getGroup(groupId); + if (group == null) { + return false; + } + + final var recipientId = resolveRecipient(source); + return !group.isMember(recipientId) || ( + group.isAnnouncementGroup() && !group.isAdmin(recipientId) + ); } private List handleMessage( @@ -2085,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); @@ -2261,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) { @@ -2269,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 ) { @@ -2320,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 { @@ -2418,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), @@ -2548,8 +2055,8 @@ public class Manager implements Closeable { return account.getContactStore().getContacts(); } - public String getContactOrProfileName(String number) throws InvalidNumberException { - final var recipientId = canonicalizeAndResolveRecipient(number); + public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { + final var recipientId = resolveRecipient(recipientIdentifier); final var recipient = account.getRecipientStore().getRecipient(recipientId); if (recipient == null) { return null; @@ -2567,36 +2074,26 @@ 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() { return account.getIdentityKeyStore().getIdentities(); } - public List getIdentities(String number) throws InvalidNumberException { - final var identity = account.getIdentityKeyStore().getIdentity(canonicalizeAndResolveRecipient(number)); + public List getIdentities(RecipientIdentifier.Single recipient) { + final var identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); return identity == null ? List.of() : List.of(identity); } /** * Trust this the identity with this fingerprint * - * @param name username of the identity + * @param recipient username of the identity * @param fingerprint Fingerprint */ - public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { + var recipientId = resolveRecipient(recipient); return trustIdentity(recipientId, identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), TrustLevel.TRUSTED_VERIFIED); @@ -2605,24 +2102,43 @@ public class Manager implements Closeable { /** * Trust this the identity with this safety number * - * @param name username of the identity + * @param recipient username of the identity * @param safetyNumber Safety number */ - public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { + var recipientId = resolveRecipient(recipient); var address = account.getRecipientStore().resolveServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), TrustLevel.TRUSTED_VERIFIED); } + /** + * Trust this the identity with this scannable safety number + * + * @param recipient username of the identity + * @param safetyNumber Scannable safety number + */ + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { + var recipientId = resolveRecipient(recipient); + var address = account.getRecipientStore().resolveServiceAddress(recipientId); + return trustIdentity(recipientId, identityKey -> { + final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); + try { + return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber); + } catch (FingerprintVersionMismatchException | FingerprintParsingException e) { + return false; + } + }, TrustLevel.TRUSTED_VERIFIED); + } + /** * Trust all keys of this identity without verification * - * @param name username of the identity + * @param recipient username of the identity */ - public boolean trustIdentityAllKeys(String name) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { + var recipientId = resolveRecipient(recipient); return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); } @@ -2665,21 +2181,23 @@ public class Manager implements Closeable { } public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final var fingerprint = Utils.computeSafetyNumber(capabilities.isUuid(), - account.getSelfAddress(), - getIdentityKeyPair().getPublicKey(), - theirAddress, - theirIdentityKey); + final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); } public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final var fingerprint = Utils.computeSafetyNumber(capabilities.isUuid(), + final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); + } + + private Fingerprint computeSafetyNumberFingerprint( + final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey + ) { + return Utils.computeSafetyNumber(capabilities.isUuid(), account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); } @Deprecated @@ -2702,12 +2220,8 @@ public class Manager implements Closeable { return account.getRecipientStore().resolveServiceAddress(recipientId); } - public RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException { - var canonicalizedNumber = UuidUtil.isUuid(identifier) - ? identifier - : PhoneNumberFormatter.formatNumber(identifier, account.getUsername()); - - return resolveRecipient(canonicalizedNumber); + private String canonicalizePhoneNumber(final String number) throws InvalidNumberException { + return PhoneNumberFormatter.formatNumber(number, account.getUsername()); } private RecipientId resolveRecipient(final String identifier) { @@ -2716,6 +2230,17 @@ public class Manager implements Closeable { return resolveRecipient(address); } + private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) { + final SignalServiceAddress address; + if (recipient instanceof RecipientIdentifier.Uuid) { + address = new SignalServiceAddress(((RecipientIdentifier.Uuid) recipient).uuid, null); + } else { + address = new SignalServiceAddress(null, ((RecipientIdentifier.Number) recipient).number); + } + + return resolveRecipient(address); + } + public RecipientId resolveRecipient(SignalServiceAddress address) { return account.getRecipientStore().resolveRecipient(address); }