X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/27274879029459ea5bf0a59af1183e12040fec56..a91e3f762e0e5d6a9cf9e208fe85207d7829e22d:/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 c619acbc..9d73ac81 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -83,6 +83,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; @@ -161,6 +162,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import java.util.stream.Collectors; @@ -193,6 +195,15 @@ public class Manager implements Closeable { private final PinHelper pinHelper; private final AvatarStore avatarStore; private final AttachmentStore attachmentStore; + private final SignalSessionLock sessionLock = new SignalSessionLock() { + private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); + + @Override + public Lock acquire() { + LEGACY_LOCK.lock(); + return LEGACY_LOCK::unlock; + } + }; Manager( SignalAccount account, @@ -389,6 +400,7 @@ public class Manager implements Closeable { newProfile.getInternalServiceName(), newProfile.getAbout() == null ? "" : newProfile.getAbout(), newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), + Optional.absent(), streamDetails); } @@ -534,6 +546,7 @@ public class Manager implements Closeable { account.getPassword(), account.getDeviceId(), account.getSignalProtocolStore(), + sessionLock, userAgent, account.isMultiDevice(), Optional.fromNullable(messagePipe), @@ -769,94 +782,94 @@ public class Manager implements Closeable { return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId())); } - public Pair> updateGroup( - GroupId groupId, String name, List members, File avatarFile + public Pair> createGroup( + String name, List members, File avatarFile + ) throws IOException, AttachmentInvalidException, InvalidNumberException { + return createGroup(name, members == null ? null : getSignalServiceAddresses(members), avatarFile); + } + + private 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 = groupHelper.createGroupV2(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 = sendMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); + return new Pair<>(gv2.getGroupId(), result.second()); + } + + public Pair> updateGroup( + GroupId groupId, + String name, + String description, + List members, + List removeMembers, + File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { - return sendUpdateGroupMessage(groupId, + return updateGroup(groupId, name, + description, members == null ? null : getSignalServiceAddresses(members), + removeMembers == null ? null : getSignalServiceAddresses(removeMembers), avatarFile); } - private Pair> sendUpdateGroupMessage( - GroupId groupId, String name, Set members, File avatarFile + private Pair> updateGroup( + GroupId groupId, + String name, + String description, + Set members, + final Set removeMembers, + File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - GroupInfo g; - SignalServiceDataMessage.Builder messageBuilder; - if (groupId == null) { - // Create new group - var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name, - members == null ? Set.of() : members, - avatarFile); - if (gv2Pair == null) { - var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); - gv1.addMembers(List.of(account.getSelfRecipientId())); - updateGroupV1(gv1, name, members, avatarFile); - messageBuilder = getGroupUpdateMessageBuilder(gv1); - g = gv1; - } else { - final var gv2 = gv2Pair.first(); - final var decryptedGroup = gv2Pair.second(); + var group = getGroupForUpdating(groupId); - gv2.setGroup(decryptedGroup, this::resolveRecipient); - if (avatarFile != null) { - avatarStore.storeGroupAvatar(gv2.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - messageBuilder = getGroupUpdateMessageBuilder(gv2, null); - g = gv2; - } - } else { - var group = getGroupForUpdating(groupId); - if (group instanceof GroupInfoV2) { - final var groupInfoV2 = (GroupInfoV2) group; - - Pair> result = null; - if (groupInfoV2.isPendingMember(account.getSelfRecipientId())) { - var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2); - result = sendUpdateGroupMessage(groupInfoV2, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } + if (group instanceof GroupInfoV2) { + return updateGroupV2((GroupInfoV2) group, name, description, members, removeMembers, avatarFile); + } - if (members != null) { - final var newMembers = new HashSet<>(members); - newMembers.removeAll(group.getMembers()); - if (newMembers.size() > 0) { - var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers); - result = sendUpdateGroupMessage(groupInfoV2, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - } - if (result == null || name != null || avatarFile != null) { - var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, name, avatarFile); - if (avatarFile != null) { - avatarStore.storeGroupAvatar(groupInfoV2.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - result = sendUpdateGroupMessage(groupInfoV2, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } + return updateGroupV1((GroupInfoV1) group, name, members, avatarFile); + } - return new Pair<>(group.getGroupId(), result.second()); - } else { - var gv1 = (GroupInfoV1) group; - updateGroupV1(gv1, name, members, avatarFile); - messageBuilder = getGroupUpdateMessageBuilder(gv1); - g = gv1; - } - } + 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(g); + account.getGroupStore().updateGroup(gv1); - final var result = sendMessage(messageBuilder, - g.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - return new Pair<>(g.getGroupId(), result.second()); + return sendMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } - private void updateGroupV1( + private void updateGroupV1Details( final GroupInfoV1 g, final String name, final Collection members, final File avatarFile ) throws IOException { if (name != null) { @@ -894,13 +907,59 @@ public class Manager implements Closeable { } } - public Pair> joinGroup( - GroupInviteLinkUrl inviteLinkUrl - ) throws IOException, GroupLinkNotActiveException { - return sendJoinGroupMessage(inviteLinkUrl); + private Pair> updateGroupV2( + final GroupInfoV2 group, + final String name, + final String description, + final Set members, + final Set removeMembers, + final File avatarFile + ) throws IOException { + Pair> result = null; + if (group.isPendingMember(account.getSelfRecipientId())) { + var groupGroupChangePair = groupHelper.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 = groupHelper.updateGroupV2(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 = groupHelper.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 = groupHelper.revokeInvitedMembers(group, pendingRemoveMembers); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + } + + if (result == null || name != null || description != null || avatarFile != null) { + var groupGroupChangePair = groupHelper.updateGroupV2(group, name, description, avatarFile); + if (avatarFile != null) { + avatarStore.storeGroupAvatar(group.getGroupId(), + outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); + } + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + return result; } - private Pair> sendJoinGroupMessage( + public Pair> joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { final var groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), @@ -917,11 +976,24 @@ public class Manager implements Closeable { return new Pair<>(group.getGroupId(), List.of()); } - final var result = sendUpdateGroupMessage(group, group.getGroup(), groupChange); + 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 sendMessage(messageBuilder, members); + } + private static int currentTimeDays() { return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()); } @@ -944,15 +1016,6 @@ public class Manager implements Closeable { } } - private Pair> sendUpdateGroupMessage( - GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange - ) throws IOException { - group.setGroup(newDecryptedGroup, this::resolveRecipient); - final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); - account.getGroupStore().updateGroup(group); - return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - } - Pair> sendGroupInfoMessage( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { @@ -1510,18 +1573,12 @@ public class Manager implements Closeable { } } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException { + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException { var cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), + sessionLock, certificateValidator); - try { - return cipher.decrypt(envelope); - } catch (ProtocolUntrustedIdentityException e) { - if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) { - throw (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause(); - } - throw new AssertionError(e); - } + return cipher.decrypt(envelope); } private void handleEndSession(RecipientId recipientId) { @@ -1765,9 +1822,9 @@ public class Manager implements Closeable { if (!envelope.isReceipt()) { try { content = decryptMessage(envelope); - } catch (org.whispersystems.libsignal.UntrustedIdentityException e) { + } catch (ProtocolUntrustedIdentityException e) { if (!envelope.hasSource()) { - final var identifier = ((org.whispersystems.libsignal.UntrustedIdentityException) e).getName(); + final var identifier = e.getSender(); final var recipientId = resolveRecipient(identifier); try { account.getMessageCache().replaceSender(cachedMessage, recipientId); @@ -1888,8 +1945,8 @@ public class Manager implements Closeable { handler.handleMessage(envelope, content, exception); } if (cachedMessage[0] != null) { - if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) { - final var identifier = ((org.whispersystems.libsignal.UntrustedIdentityException) exception).getName(); + if (exception instanceof ProtocolUntrustedIdentityException) { + final var identifier = ((ProtocolUntrustedIdentityException) exception).getSender(); final var recipientId = resolveRecipient(identifier); queuedActions.add(new RetrieveProfileAction(recipientId)); if (!envelope.hasSource()) {