]> nmode's Git Repositories - signal-cli/blobdiff - src/main/java/org/asamk/signal/manager/Manager.java
Implement accepting and declining group invitations
[signal-cli] / src / main / java / org / asamk / signal / manager / Manager.java
index 000455ac169740d0ec9ad6b9099030f3dc708b92..24bc0ebd8a04be61b2eb1c60e8f15815cbcc45e0 100644 (file)
@@ -43,6 +43,7 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
 import org.signal.libsignal.metadata.ProtocolNoSessionException;
 import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
 import org.signal.libsignal.metadata.SelfSendException;
+import org.signal.storageservice.protos.groups.GroupChange;
 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
 import org.signal.storageservice.protos.groups.local.DecryptedMember;
 import org.signal.zkgroup.InvalidInputException;
@@ -119,6 +120,9 @@ import org.whispersystems.signalservice.api.util.StreamDetails;
 import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
 import org.whispersystems.signalservice.api.util.UuidUtil;
 import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
+import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
 import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
 import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
@@ -141,6 +145,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
+import java.security.SignatureException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -150,6 +155,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
@@ -160,7 +166,9 @@ import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
+import static org.asamk.signal.manager.ServiceConfig.CDS_MRENCLAVE;
 import static org.asamk.signal.manager.ServiceConfig.capabilities;
+import static org.asamk.signal.manager.ServiceConfig.getIasKeyStore;
 
 public class Manager implements Closeable {
 
@@ -213,7 +221,9 @@ public class Manager implements Closeable {
         this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
                 this::getRecipientProfile,
                 account::getSelfAddress,
-                groupsV2Operations);
+                groupsV2Operations,
+                groupsV2Api,
+                this::getGroupAuthForToday);
     }
 
     public String getUsername() {
@@ -312,6 +322,8 @@ public class Manager implements Closeable {
             contact.profileKey = null;
             account.getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
         }
+        // Ensure our profile key is stored in profile store
+        account.getProfileStore().storeProfileKey(getSelfAddress(), account.getProfileKey());
     }
 
     public void checkAccountState() throws IOException {
@@ -695,6 +707,17 @@ public class Manager implements Closeable {
         return g;
     }
 
+    private GroupInfo getGroupForUpdating(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException {
+        GroupInfo g = account.getGroupStore().getGroup(groupId);
+        if (g == null) {
+            throw new GroupNotFoundException(groupId);
+        }
+        if (!g.isMember(account.getSelfAddress()) && !g.isPendingMember(account.getSelfAddress())) {
+            throw new NotAGroupMemberException(groupId, g.getTitle());
+        }
+        return g;
+    }
+
     public List<GroupInfo> getGroups() {
         return account.getGroupStore().getGroups();
     }
@@ -736,52 +759,29 @@ public class Manager implements Closeable {
     }
 
     public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
-        SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId).build();
 
-        SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
+        SignalServiceDataMessage.Builder messageBuilder;
 
-        final GroupInfo g = getGroupForSending(groupId);
+        final GroupInfo g = getGroupForUpdating(groupId);
         if (g instanceof GroupInfoV1) {
             GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
+            SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
+                    .withId(groupId)
+                    .build();
+            messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
             groupInfoV1.removeMember(account.getSelfAddress());
             account.getGroupStore().updateGroup(groupInfoV1);
         } else {
-            throw new RuntimeException("TODO Not implemented!");
+            final GroupInfoV2 groupInfoV2 = (GroupInfoV2) g;
+            final Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2);
+            groupInfoV2.setGroup(groupGroupChangePair.first());
+            messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
+            account.getGroupStore().updateGroup(groupInfoV2);
         }
 
         return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
     }
 
-    private GroupInfoV2 createGroupV2(
-            String name, Collection<SignalServiceAddress> members, InputStream avatar
-    ) throws IOException {
-        byte[] avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
-        final GroupsV2Operations.NewGroup newGroup = groupHelper.createGroupV2(name, members, avatarBytes);
-        final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
-
-        final GroupsV2AuthorizationString groupAuthForToday;
-        final DecryptedGroup decryptedGroup;
-        try {
-            groupAuthForToday = getGroupAuthForToday(groupSecretParams);
-            groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
-            decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
-        } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
-            System.err.println("Failed to create V2 group: " + e.getMessage());
-            return null;
-        }
-        if (decryptedGroup == null) {
-            System.err.println("Failed to create V2 group!");
-            return null;
-        }
-
-        final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
-        final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
-        GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
-        g.setGroup(decryptedGroup);
-
-        return g;
-    }
-
     private Pair<byte[], List<SendMessageResult>> sendUpdateGroupMessage(
             byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile
     ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
@@ -789,8 +789,7 @@ public class Manager implements Closeable {
         SignalServiceDataMessage.Builder messageBuilder;
         if (groupId == null) {
             // Create new group
-            InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile);
-            GroupInfoV2 gv2 = createGroupV2(name, members, avatar);
+            GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile);
             if (gv2 == null) {
                 GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId());
                 gv1.addMembers(Collections.singleton(account.getSelfAddress()));
@@ -798,27 +797,68 @@ public class Manager implements Closeable {
                 messageBuilder = getGroupUpdateMessageBuilder(gv1);
                 g = gv1;
             } else {
-                messageBuilder = getGroupUpdateMessageBuilder(gv2);
+                messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
                 g = gv2;
             }
         } else {
-            GroupInfo group = getGroupForSending(groupId);
-            if (!(group instanceof GroupInfoV1)) {
-                throw new RuntimeException("TODO Not implemented!");
+            GroupInfo group = getGroupForUpdating(groupId);
+            if (group instanceof GroupInfoV2) {
+                final GroupInfoV2 groupInfoV2 = (GroupInfoV2) group;
+
+                Pair<Long, List<SendMessageResult>> result = null;
+                if (groupInfoV2.isPendingMember(getSelfAddress())) {
+                    Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
+                    result = sendUpdateGroupMessage(groupInfoV2,
+                            groupGroupChangePair.first(),
+                            groupGroupChangePair.second());
+                }
+
+                if (members != null) {
+                    final Set<SignalServiceAddress> newMembers = new HashSet<>(members);
+                    newMembers.removeAll(group.getMembers());
+                    if (newMembers.size() > 0) {
+                        Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2,
+                                newMembers);
+                        result = sendUpdateGroupMessage(groupInfoV2,
+                                groupGroupChangePair.first(),
+                                groupGroupChangePair.second());
+                    }
+                }
+                if (result == null || name != null || avatarFile != null) {
+                    Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2,
+                            name,
+                            avatarFile);
+                    result = sendUpdateGroupMessage(groupInfoV2,
+                            groupGroupChangePair.first(),
+                            groupGroupChangePair.second());
+                }
+
+                return new Pair<>(group.groupId, result.second());
+            } else {
+                GroupInfoV1 gv1 = (GroupInfoV1) group;
+                updateGroupV1(gv1, name, members, avatarFile);
+                messageBuilder = getGroupUpdateMessageBuilder(gv1);
+                g = gv1;
             }
-            GroupInfoV1 gv1 = (GroupInfoV1) group;
-            updateGroupV1(gv1, name, members, avatarFile);
-            messageBuilder = getGroupUpdateMessageBuilder(gv1);
-            g = gv1;
         }
 
         account.getGroupStore().updateGroup(g);
 
         final Pair<Long, List<SendMessageResult>> result = sendMessage(messageBuilder,
-                g.getMembersWithout(account.getSelfAddress()));
+                g.getMembersIncludingPendingWithout(account.getSelfAddress()));
         return new Pair<>(g.groupId, result.second());
     }
 
+    private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
+            GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
+    ) throws IOException {
+        group.setGroup(newDecryptedGroup);
+        final SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(group,
+                groupChange.toByteArray());
+        account.getGroupStore().updateGroup(group);
+        return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress()));
+    }
+
     private void updateGroupV1(
             final GroupInfoV1 g,
             final String name,
@@ -899,11 +939,10 @@ public class Manager implements Closeable {
                 .withExpiration(g.getMessageExpirationTime());
     }
 
-    private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g) {
+    private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
         SignalServiceGroupV2.Builder group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
                 .withRevision(g.getGroup().getRevision())
-//                .withSignedGroupChange() // TODO
-                ;
+                .withSignedGroupChange(signedGroupChange);
         return SignalServiceDataMessage.newBuilder()
                 .asGroupMessage(group.build())
                 .withExpiration(g.getMessageExpirationTime());
@@ -1278,10 +1317,39 @@ public class Manager implements Closeable {
 
     private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
         final Set<SignalServiceAddress> signalServiceAddresses = new HashSet<>(numbers.size());
+        final Set<SignalServiceAddress> missingUuids = new HashSet<>();
 
         for (String number : numbers) {
-            signalServiceAddresses.add(canonicalizeAndResolveSignalServiceAddress(number));
+            final SignalServiceAddress resolvedAddress = canonicalizeAndResolveSignalServiceAddress(number);
+            if (resolvedAddress.getUuid().isPresent()) {
+                signalServiceAddresses.add(resolvedAddress);
+            } else {
+                missingUuids.add(resolvedAddress);
+            }
+        }
+
+        Map<String, UUID> registeredUsers;
+        try {
+            registeredUsers = accountManager.getRegisteredUsers(getIasKeyStore(),
+                    missingUuids.stream().map(a -> a.getNumber().get()).collect(Collectors.toSet()),
+                    CDS_MRENCLAVE);
+        } catch (IOException | Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException e) {
+            System.err.println("Failed to resolve uuids from server: " + e.getMessage());
+            registeredUsers = new HashMap<>();
+        }
+
+        for (SignalServiceAddress address : missingUuids) {
+            final String number = address.getNumber().get();
+            if (registeredUsers.containsKey(number)) {
+                final SignalServiceAddress newAddress = resolveSignalServiceAddress(new SignalServiceAddress(
+                        registeredUsers.get(number),
+                        number));
+                signalServiceAddresses.add(newAddress);
+            } else {
+                signalServiceAddresses.add(address);
+            }
         }
+
         return signalServiceAddresses;
     }
 
@@ -1427,16 +1495,20 @@ public class Manager implements Closeable {
 
     private GroupsV2AuthorizationString getGroupAuthForToday(
             final GroupSecretParams groupSecretParams
-    ) throws IOException, VerificationFailedException {
+    ) throws IOException {
         final int today = currentTimeDays();
         // Returns credentials for the next 7 days
         final HashMap<Integer, AuthCredentialResponse> credentials = groupsV2Api.getCredentials(today);
         // TODO cache credentials until they expire
         AuthCredentialResponse authCredentialResponse = credentials.get(today);
-        return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
-                today,
-                groupSecretParams,
-                authCredentialResponse);
+        try {
+            return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
+                    today,
+                    groupSecretParams,
+                    authCredentialResponse);
+        } catch (VerificationFailedException e) {
+            throw new IOException(e);
+        }
     }
 
     private List<HandleAction> handleSignalServiceDataMessage(
@@ -1541,6 +1613,9 @@ public class Manager implements Closeable {
                             group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(),
                                     groupContext.getSignedGroupChange(),
                                     groupMasterKey);
+                            if (group != null) {
+                                storeProfileKeysFromMembers(group);
+                            }
                         }
                         if (group == null) {
                             group = getDecryptedGroup(groupSecretParams);
@@ -1637,15 +1712,7 @@ public class Manager implements Closeable {
         try {
             final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
             DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
-            for (DecryptedMember member : group.getMembersList()) {
-                final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(
-                        member.getUuid().toByteArray()), null));
-                try {
-                    account.getProfileStore()
-                            .storeProfileKey(address, new ProfileKey(member.getProfileKey().toByteArray()));
-                } catch (InvalidInputException ignored) {
-                }
-            }
+            storeProfileKeysFromMembers(group);
             return group;
         } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
             System.err.println("Failed to retrieve Group V2 info, ignoring ...");
@@ -1653,6 +1720,18 @@ public class Manager implements Closeable {
         }
     }
 
+    private void storeProfileKeysFromMembers(final DecryptedGroup group) {
+        for (DecryptedMember member : group.getMembersList()) {
+            final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(
+                    member.getUuid().toByteArray()), null));
+            try {
+                account.getProfileStore()
+                        .storeProfileKey(address, new ProfileKey(member.getProfileKey().toByteArray()));
+            } catch (InvalidInputException ignored) {
+            }
+        }
+    }
+
     private void retryFailedReceivedMessages(
             ReceiveMessageHandler handler, boolean ignoreAttachments
     ) {
@@ -1863,7 +1942,7 @@ public class Manager implements Closeable {
     ) {
         List<HandleAction> actions = new ArrayList<>();
         if (content != null) {
-            SignalServiceAddress sender;
+            final SignalServiceAddress sender;
             if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
                 sender = envelope.getSourceAddress();
             } else {
@@ -1890,11 +1969,14 @@ public class Manager implements Closeable {
                 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
                 if (syncMessage.getSent().isPresent()) {
                     SentTranscriptMessage message = syncMessage.getSent().get();
-                    actions.addAll(handleSignalServiceDataMessage(message.getMessage(),
-                            true,
-                            sender,
-                            message.getDestination().orNull(),
-                            ignoreAttachments));
+                    final SignalServiceAddress destination = message.getDestination().orNull();
+                    if (destination != null) {
+                        actions.addAll(handleSignalServiceDataMessage(message.getMessage(),
+                                true,
+                                sender,
+                                destination,
+                                ignoreAttachments));
+                    }
                 }
                 if (syncMessage.getRequest().isPresent()) {
                     RequestMessage rm = syncMessage.getRequest().get();