X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/11c90fa0324347bf2b7ba56855ccd45898141569..ca52c0103136bc1f0cb140c33e81ff17d47aec2e:/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..577badc8 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; @@ -43,6 +47,7 @@ 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; @@ -81,6 +86,9 @@ 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; @@ -151,6 +159,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; @@ -267,7 +276,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 +288,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 +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); @@ -327,16 +340,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 { @@ -672,33 +698,9 @@ public class Manager implements Closeable { 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 { + public SendGroupMessageResults sendQuitGroupMessage( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { var group = getGroupForUpdating(groupId); if (group instanceof GroupInfoV1) { return quitGroupV1((GroupInfoV1) group); @@ -714,19 +716,19 @@ public class Manager implements Closeable { } } - private Pair> quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { + private SendGroupMessageResults quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupInfoV1.getGroupId().serialize()) .build(); - var messageBuilder = createMessageBuilder().asGroupMessage(group); + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); groupInfoV1.removeMember(account.getSelfRecipientId()); account.getGroupStore().updateGroup(groupInfoV1); - return sendHelper.sendGroupMessage(messageBuilder.build(), + return sendGroupMessage(messageBuilder, groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } - private Pair> quitGroupV2( + private SendGroupMessageResults quitGroupV2( final GroupInfoV2 groupInfoV2, final Set newAdmins ) throws LastGroupAdminException, IOException { final var currentAdmins = groupInfoV2.getAdminMembers(); @@ -741,9 +743,10 @@ public class Manager implements Closeable { } 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(), + + var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); + return sendGroupMessage(messageBuilder, groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } @@ -752,13 +755,13 @@ 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); + public Pair createGroup( + String name, Set members, File avatarFile + ) throws IOException, AttachmentInvalidException { + return createGroupInternal(name, members == null ? null : getRecipientIds(members), avatarFile); } - private Pair> createGroup( + private Pair createGroupInternal( String name, Set members, File avatarFile ) throws IOException, AttachmentInvalidException { final var selfRecipientId = account.getSelfRecipientId(); @@ -771,13 +774,12 @@ public class Manager implements Closeable { 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()); + return new Pair<>(gv1.getGroupId(), result); } final var gv2 = gv2Pair.first(); @@ -788,30 +790,32 @@ public class Manager implements Closeable { 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()); + final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null); + + final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); + return new Pair<>(gv2.getGroupId(), result); } - 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 updateGroupInternal(groupId, name, description, members == null ? null : getRecipientIds(members), @@ -823,10 +827,11 @@ public class Manager implements Closeable { addMemberPermission, editDetailsPermission, avatarFile, - expirationTimer); + expirationTimer, + isAnnouncementGroup); } - private Pair> updateGroup( + private SendGroupMessageResults updateGroupInternal( final GroupId groupId, final String name, final String description, @@ -839,7 +844,8 @@ public class Manager implements Closeable { final GroupPermission addMemberPermission, final GroupPermission editDetailsPermission, final File avatarFile, - final Integer expirationTimer + final Integer expirationTimer, + final Boolean isAnnouncementGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { var group = getGroupForUpdating(groupId); @@ -857,7 +863,8 @@ public class Manager implements Closeable { addMemberPermission, editDetailsPermission, avatarFile, - expirationTimer); + expirationTimer, + isAnnouncementGroup); } catch (ConflictException e) { // Detected conflicting update, refreshing group and trying again group = getGroup(groupId, true); @@ -873,7 +880,8 @@ public class Manager implements Closeable { addMemberPermission, editDetailsPermission, avatarFile, - expirationTimer); + expirationTimer, + isAnnouncementGroup); } } @@ -885,16 +893,15 @@ public class Manager implements Closeable { return result; } - private Pair> updateGroupV1( + private SendGroupMessageResults 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())); + var messageBuilder = getGroupUpdateMessageBuilder(gv1); + return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } private void updateGroupV1Details( @@ -935,7 +942,7 @@ public class Manager implements Closeable { } } - private Pair> updateGroupV2( + private SendGroupMessageResults updateGroupV2( final GroupInfoV2 group, final String name, final String description, @@ -948,9 +955,10 @@ public class Manager implements Closeable { final GroupPermission addMemberPermission, final GroupPermission editDetailsPermission, final File avatarFile, - Integer expirationTimer + final Integer expirationTimer, + final Boolean isAnnouncementGroup ) throws IOException { - Pair> result = null; + SendGroupMessageResults result = null; if (group.isPendingMember(account.getSelfRecipientId())) { var groupGroupChangePair = groupV2Helper.acceptInvite(group); result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); @@ -1034,6 +1042,11 @@ public class Manager implements Closeable { 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) { @@ -1046,7 +1059,7 @@ public class Manager implements Closeable { return result; } - public Pair> joinGroup( + public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), @@ -1060,25 +1073,74 @@ public class Manager implements Closeable { if (group.getGroup() == null) { // Only requested member, can't send update to group members - return new Pair<>(group.getGroupId(), List.of()); + return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of())); } final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); - return new Pair<>(group.getGroupId(), result.second()); + return new Pair<>(group.getGroupId(), result); } - private Pair> sendUpdateGroupV2Message( + 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()); - account.getGroupStore().updateGroup(group); - return sendHelper.sendGroupMessage(messageBuilder.build(), members); + return sendGroupMessage(messageBuilder, members); + } + + 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); + } + } + return new SendMessageResults(timestamp, results); + } + + 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); + } + } + } + + 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() { @@ -1104,7 +1166,7 @@ public class Manager implements Closeable { } } - Pair> sendGroupInfoMessage( + SendGroupMessageResults sendGroupInfoMessage( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { GroupInfoV1 g; @@ -1122,7 +1184,7 @@ public class Manager implements Closeable { var messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(recipientId)); + return sendGroupMessage(messageBuilder, Set.of(recipientId)); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { @@ -1143,43 +1205,75 @@ public class Manager implements Closeable { throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); } - return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); + 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 createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.getMessageExpirationTime()); } - Pair> sendGroupInfoRequest( + SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); - var messageBuilder = createMessageBuilder().asGroupMessage(group.build()); + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); // 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))); + return sendGroupMessage(messageBuilder, Set.of(resolveRecipient(recipient))); } - void sendReceipt( - SignalServiceAddress remoteAddress, long messageId + public void sendReadReceipt( + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, + messageIds, + System.currentTimeMillis()); + + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + } + + public void sendViewedReceipt( + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, + messageIds, + System.currentTimeMillis()); + + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + } + + 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 +1288,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,23 +1337,25 @@ 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) { @@ -1300,20 +1384,20 @@ 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); + RecipientIdentifier.Single recipient, int messageExpirationTimer + ) throws IOException { + var recipientId = resolveRecipient(recipient); setExpirationTimer(recipientId, messageExpirationTimer); - sendExpirationTimerUpdate(recipientId); + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + try { + sendMessage(messageBuilder, Set.of(recipient)); + } catch (NotAGroupMemberException | GroupNotFoundException e) { + throw new AssertionError(e); + } } /** @@ -1328,7 +1412,7 @@ public class Manager implements Closeable { } private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { - final var messageBuilder = createMessageBuilder().asExpirationUpdate(); + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); sendHelper.sendAsGroupMessage(messageBuilder, groupId); } @@ -1338,7 +1422,7 @@ public class Manager implements Closeable { * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file * @return if successful, returns the URL to install the sticker pack in the signal app */ - public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException { + public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); var messageSender = dependencies.getMessageSender(); @@ -1357,7 +1441,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 +1511,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,40 +1561,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)); - } + // Store numbers as recipients so we have the number/uuid association + registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); - 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); + return registeredUsers; } - private SignalServiceDataMessage.Builder createMessageBuilder() { - final var timestamp = System.currentTimeMillis(); - - var messageBuilder = SignalServiceDataMessage.newBuilder(); - messageBuilder.withTimestamp(timestamp); - return messageBuilder; + public void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + sendTypingMessage(action.toSignalService(), recipients); } private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException { @@ -1740,16 +1810,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( @@ -1793,10 +1854,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 +1886,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,7 +1913,6 @@ 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); @@ -1893,16 +1942,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 +1973,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 +2018,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 +2028,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 +2040,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( @@ -2548,8 +2620,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; @@ -2584,19 +2656,19 @@ public class Manager implements Closeable { 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 +2677,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 +2756,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 +2795,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 +2805,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); }