]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/Manager.java
Skip invalid contact records
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / Manager.java
index a1a4b278f8160ac9d7bb9cf899b08913ffc81f33..08789d9581589c5a7ada04be362e8fc17f395361 100644 (file)
  */
 package org.asamk.signal.manager;
 
+import org.asamk.signal.manager.api.Device;
 import org.asamk.signal.manager.config.ServiceConfig;
 import org.asamk.signal.manager.config.ServiceEnvironment;
 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
 import org.asamk.signal.manager.groups.GroupId;
 import org.asamk.signal.manager.groups.GroupIdV1;
 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupLinkState;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
+import org.asamk.signal.manager.groups.GroupPermission;
 import org.asamk.signal.manager.groups.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;
 import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
@@ -82,6 +86,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;
@@ -108,7 +113,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsO
 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream;
 import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
-import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
 import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
@@ -117,8 +121,10 @@ 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.push.exceptions.UnregisteredUserException;
+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.SleepTimer;
@@ -160,6 +166,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;
 
@@ -188,10 +195,19 @@ public class Manager implements Closeable {
 
     private final UnidentifiedAccessHelper unidentifiedAccessHelper;
     private final ProfileHelper profileHelper;
-    private final GroupHelper groupHelper;
+    private final GroupV2Helper groupV2Helper;
     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,
@@ -247,7 +263,7 @@ public class Manager implements Closeable {
                 unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
                 () -> messageReceiver,
                 this::resolveSignalServiceAddress);
-        this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
+        this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential,
                 this::getRecipientProfile,
                 account::getSelfRecipientId,
                 groupsV2Operations,
@@ -287,7 +303,7 @@ public class Manager implements Closeable {
             throw new NotRegisteredException();
         }
 
-        var account = SignalAccount.load(pathConfig.getDataPath(), username);
+        var account = SignalAccount.load(pathConfig.getDataPath(), username, true);
 
         if (!account.isRegistered()) {
             throw new NotRegisteredException();
@@ -315,6 +331,17 @@ 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.");
+        } else {
+            var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
+            long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
+            if (days > 7) {
+                logger.warn(
+                        "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
+                        days);
+            }
+        }
         if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) {
             refreshPreKeys();
         }
@@ -341,7 +368,7 @@ public class Manager implements Closeable {
     }
 
     public void updateAccountAttributes() throws IOException {
-        accountManager.setAccountAttributes(account.getDeviceName(),
+        accountManager.setAccountAttributes(account.getEncryptedDeviceName(),
                 null,
                 account.getLocalRegistrationId(),
                 true,
@@ -388,6 +415,7 @@ public class Manager implements Closeable {
                     newProfile.getInternalServiceName(),
                     newProfile.getAbout() == null ? "" : newProfile.getAbout(),
                     newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
+                    Optional.absent(),
                     streamDetails);
         }
 
@@ -422,10 +450,21 @@ public class Manager implements Closeable {
         account.setRegistered(false);
     }
 
-    public List<DeviceInfo> getLinkedDevices() throws IOException {
+    public List<Device> getLinkedDevices() throws IOException {
         var devices = accountManager.getDevices();
         account.setMultiDevice(devices.size() > 1);
-        return devices;
+        var identityKey = account.getIdentityKeyPair().getPrivateKey();
+        return devices.stream().map(d -> {
+            String deviceName = d.getName();
+            if (deviceName != null) {
+                try {
+                    deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
+                } catch (IOException e) {
+                    logger.debug("Failed to decrypt device name, maybe plain text?", e);
+                }
+            }
+            return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen());
+        }).collect(Collectors.toList());
     }
 
     public void removeLinkedDevices(int deviceId) throws IOException {
@@ -522,6 +561,7 @@ public class Manager implements Closeable {
                 account.getPassword(),
                 account.getDeviceId(),
                 account.getSignalProtocolStore(),
+                sessionLock,
                 userAgent,
                 account.isMultiDevice(),
                 Optional.fromNullable(messagePipe),
@@ -544,17 +584,9 @@ public class Manager implements Closeable {
     Profile getRecipientProfile(
             RecipientId recipientId, boolean force
     ) {
-        var profileKey = account.getProfileStore().getProfileKey(recipientId);
-        if (profileKey == null) {
-            if (force) {
-                // retrieve profile to get identity key
-                retrieveEncryptedProfile(recipientId);
-            }
-            return null;
-        }
         var profile = account.getProfileStore().getProfile(recipientId);
 
-        var now = new Date().getTime();
+        var now = System.currentTimeMillis();
         // Profiles are cached for 24h before retrieving them again, unless forced
         if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
             return profile;
@@ -578,7 +610,18 @@ public class Manager implements Closeable {
             return null;
         }
 
-        profile = decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
+        var profileKey = account.getProfileStore().getProfileKey(recipientId);
+        if (profileKey == null) {
+            profile = new Profile(System.currentTimeMillis(),
+                    null,
+                    null,
+                    null,
+                    null,
+                    ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
+                    ProfileUtils.getCapabilities(encryptedProfile));
+        } else {
+            profile = decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
+        }
         account.getProfileStore().storeProfile(recipientId, profile);
 
         return profile;
@@ -610,7 +653,7 @@ public class Manager implements Closeable {
             }
         } catch (InvalidKeyException ignored) {
             logger.warn("Got invalid identity key in profile for {}",
-                    resolveSignalServiceAddress(recipientId).getLegacyIdentifier());
+                    resolveSignalServiceAddress(recipientId).getIdentifier());
         }
         return profileAndCredential;
     }
@@ -733,7 +776,9 @@ public class Manager implements Closeable {
         return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
     }
 
-    public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
+    public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(
+            GroupId groupId, Set<String> groupAdmins
+    ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException {
         SignalServiceDataMessage.Builder messageBuilder;
 
         final var g = getGroupForUpdating(groupId);
@@ -745,7 +790,18 @@ public class Manager implements Closeable {
             account.getGroupStore().updateGroup(groupInfoV1);
         } else {
             final var groupInfoV2 = (GroupInfoV2) g;
-            final var groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2);
+            final var currentAdmins = g.getAdminMembers();
+            final var newAdmins = getSignalServiceAddresses(groupAdmins);
+            newAdmins.removeAll(currentAdmins);
+            newAdmins.retainAll(g.getMembers());
+            if (currentAdmins.contains(getSelfRecipientId())
+                    && currentAdmins.size() == 1
+                    && g.getMembers().size() > 1
+                    && newAdmins.size() == 0) {
+                // Last admin can't leave the group, unless she's also the last member
+                throw new LastGroupAdminException(g.getGroupId(), g.getTitle());
+            }
+            final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins);
             groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
             messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
             account.getGroupStore().updateGroup(groupInfoV2);
@@ -754,94 +810,150 @@ public class Manager implements Closeable {
         return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
     }
 
-    public Pair<GroupId, List<SendMessageResult>> updateGroup(
-            GroupId groupId, String name, List<String> members, File avatarFile
-    ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
-        return sendUpdateGroupMessage(groupId,
-                name,
-                members == null ? null : getSignalServiceAddresses(members),
-                avatarFile);
+    public Pair<GroupId, List<SendMessageResult>> createGroup(
+            String name, List<String> members, File avatarFile
+    ) throws IOException, AttachmentInvalidException, InvalidNumberException {
+        return createGroup(name, members == null ? null : getSignalServiceAddresses(members), avatarFile);
     }
 
-    private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
-            GroupId groupId, String name, Set<RecipientId> members, File avatarFile
-    ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
-        GroupInfo g;
+    private Pair<GroupId, List<SendMessageResult>> createGroup(
+            String name, Set<RecipientId> 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 (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();
+        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());
+        }
 
-                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<Long, List<SendMessageResult>> result = null;
-                if (groupInfoV2.isPendingMember(account.getSelfRecipientId())) {
-                    var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
-                    result = sendUpdateGroupMessage(groupInfoV2,
-                            groupGroupChangePair.first(),
-                            groupGroupChangePair.second());
-                }
+        final var gv2 = gv2Pair.first();
+        final var decryptedGroup = gv2Pair.second();
 
-                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());
-                }
+        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<Long, List<SendMessageResult>> updateGroup(
+            GroupId groupId,
+            String name,
+            String description,
+            List<String> members,
+            List<String> removeMembers,
+            List<String> admins,
+            List<String> removeAdmins,
+            boolean resetGroupLink,
+            GroupLinkState groupLinkState,
+            GroupPermission addMemberPermission,
+            GroupPermission editDetailsPermission,
+            File avatarFile,
+            Integer expirationTimer
+    ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
+        return updateGroup(groupId,
+                name,
+                description,
+                members == null ? null : getSignalServiceAddresses(members),
+                removeMembers == null ? null : getSignalServiceAddresses(removeMembers),
+                admins == null ? null : getSignalServiceAddresses(admins),
+                removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins),
+                resetGroupLink,
+                groupLinkState,
+                addMemberPermission,
+                editDetailsPermission,
+                avatarFile,
+                expirationTimer);
+    }
+
+    private Pair<Long, List<SendMessageResult>> updateGroup(
+            final GroupId groupId,
+            final String name,
+            final String description,
+            final Set<RecipientId> members,
+            final Set<RecipientId> removeMembers,
+            final Set<RecipientId> admins,
+            final Set<RecipientId> 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);
 
-                return new Pair<>(group.getGroupId(), result.second());
-            } else {
-                var gv1 = (GroupInfoV1) group;
-                updateGroupV1(gv1, name, members, avatarFile);
-                messageBuilder = getGroupUpdateMessageBuilder(gv1);
-                g = gv1;
+        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);
             }
         }
 
-        account.getGroupStore().updateGroup(g);
+        final var gv1 = (GroupInfoV1) group;
+        final var result = updateGroupV1(gv1, name, members, avatarFile);
+        if (expirationTimer != null) {
+            setExpirationTimer(gv1, expirationTimer);
+        }
+        return result;
+    }
+
+    private Pair<Long, List<SendMessageResult>> updateGroupV1(
+            final GroupInfoV1 gv1, final String name, final Set<RecipientId> members, final File avatarFile
+    ) throws IOException, AttachmentInvalidException {
+        updateGroupV1Details(gv1, name, members, avatarFile);
+        var messageBuilder = getGroupUpdateMessageBuilder(gv1);
 
-        final var result = sendMessage(messageBuilder,
-                g.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
-        return new Pair<>(g.getGroupId(), result.second());
+        account.getGroupStore().updateGroup(gv1);
+
+        return sendMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
     }
 
-    private void updateGroupV1(
+    private void updateGroupV1Details(
             final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final File avatarFile
     ) throws IOException {
         if (name != null) {
@@ -879,18 +991,123 @@ public class Manager implements Closeable {
         }
     }
 
-    public Pair<GroupId, List<SendMessageResult>> joinGroup(
-            GroupInviteLinkUrl inviteLinkUrl
-    ) throws IOException, GroupLinkNotActiveException {
-        return sendJoinGroupMessage(inviteLinkUrl);
+    private Pair<Long, List<SendMessageResult>> updateGroupV2(
+            final GroupInfoV2 group,
+            final String name,
+            final String description,
+            final Set<RecipientId> members,
+            final Set<RecipientId> removeMembers,
+            final Set<RecipientId> admins,
+            final Set<RecipientId> removeAdmins,
+            final boolean resetGroupLink,
+            final GroupLinkState groupLinkState,
+            final GroupPermission addMemberPermission,
+            final GroupPermission editDetailsPermission,
+            final File avatarFile,
+            Integer expirationTimer
+    ) throws IOException {
+        Pair<Long, List<SendMessageResult>> 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));
+            }
+            result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+        }
+
+        return result;
     }
 
-    private Pair<GroupId, List<SendMessageResult>> sendJoinGroupMessage(
+    public Pair<GroupId, List<SendMessageResult>> joinGroup(
             GroupInviteLinkUrl inviteLinkUrl
     ) throws IOException, GroupLinkNotActiveException {
-        final var groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
+        final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
                 inviteLinkUrl.getPassword());
-        final var groupChange = groupHelper.joinGroup(inviteLinkUrl.getGroupMasterKey(),
+        final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(),
                 inviteLinkUrl.getPassword(),
                 groupJoinInfo);
         final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
@@ -902,11 +1119,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<Long, List<SendMessageResult>> 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());
     }
@@ -929,15 +1159,6 @@ public class Manager implements Closeable {
         }
     }
 
-    private Pair<Long, List<SendMessageResult>> 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<Long, List<SendMessageResult>> sendGroupInfoMessage(
             GroupIdV1 groupId, SignalServiceAddress recipient
     ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
@@ -1089,9 +1310,11 @@ public class Manager implements Closeable {
         }
     }
 
-    SendMessageResult renewSession(RecipientId recipientId) throws IOException {
+    void renewSession(RecipientId recipientId) throws IOException {
         account.getSessionStore().archiveSessions(recipientId);
-        return sendNullMessage(recipientId);
+        if (!recipientId.equals(getSelfRecipientId())) {
+            sendNullMessage(recipientId);
+        }
     }
 
     public String getContactName(String number) throws InvalidNumberException {
@@ -1163,15 +1386,17 @@ public class Manager implements Closeable {
     /**
      * Change the expiration timer for a group
      */
-    public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) {
-        var g = getGroup(groupId);
-        if (g instanceof GroupInfoV1) {
-            var groupInfoV1 = (GroupInfoV1) g;
-            groupInfoV1.messageExpirationTime = messageExpirationTimer;
-            account.getGroupStore().updateGroup(groupInfoV1);
-        } else {
-            throw new RuntimeException("TODO Not implemented!");
-        }
+    private void setExpirationTimer(
+            GroupInfoV1 groupInfoV1, int messageExpirationTimer
+    ) throws NotAGroupMemberException, GroupNotFoundException, IOException {
+        groupInfoV1.messageExpirationTime = messageExpirationTimer;
+        account.getGroupStore().updateGroup(groupInfoV1);
+        sendExpirationTimerUpdate(groupInfoV1.getGroupId());
+    }
+
+    private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException {
+        final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
+        sendGroupMessage(messageBuilder, groupId);
     }
 
     /**
@@ -1272,11 +1497,13 @@ public class Manager implements Closeable {
     }
 
     private byte[] getSenderCertificate() {
-        // TODO support UUID capable sender certificates
-        // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
         byte[] certificate;
         try {
-            certificate = accountManager.getSenderCertificate();
+            if (account.isPhoneNumberShared()) {
+                certificate = accountManager.getSenderCertificate();
+            } else {
+                certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
+            }
         } catch (IOException e) {
             logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
             return null;
@@ -1491,18 +1718,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) {
@@ -1686,10 +1907,12 @@ public class Manager implements Closeable {
             if (signedGroupChange != null
                     && groupInfoV2.getGroup() != null
                     && groupInfoV2.getGroup().getRevision() + 1 == revision) {
-                group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey);
+                group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(),
+                        signedGroupChange,
+                        groupMasterKey);
             }
             if (group == null) {
-                group = groupHelper.getDecryptedGroup(groupSecretParams);
+                group = groupV2Helper.getDecryptedGroup(groupSecretParams);
             }
             if (group != null) {
                 storeProfileKeysFromMembers(group);
@@ -1746,9 +1969,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);
@@ -1789,6 +2012,7 @@ public class Manager implements Closeable {
             SignalServiceContent content = null;
             Exception exception = null;
             final CachedMessage[] cachedMessage = {null};
+            account.setLastReceiveTimestamp(System.currentTimeMillis());
             try {
                 var result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> {
                     final var recipientId = envelope1.hasSource()
@@ -1825,6 +2049,7 @@ public class Manager implements Closeable {
 
             if (envelope.hasSource()) {
                 // Store uuid if we don't have it already
+                // address/uuid in envelope is sent by server
                 resolveRecipientTrusted(envelope.getSourceAddress());
             }
             final var notAGroupMember = isNotAGroupMember(envelope, content);
@@ -1834,6 +2059,11 @@ public class Manager implements Closeable {
                 } catch (Exception e) {
                     exception = e;
                 }
+                if (!envelope.hasSource() && content != null) {
+                    // Store uuid if we don't have it already
+                    // address/uuid is validated by unidentified sender certificate
+                    resolveRecipientTrusted(content.getSender());
+                }
                 var actions = handleMessage(envelope, content, ignoreAttachments);
                 if (exception instanceof ProtocolInvalidMessageException) {
                     final var sender = resolveRecipient(((ProtocolInvalidMessageException) exception).getSender());
@@ -1863,8 +2093,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()) {
@@ -2010,7 +2240,16 @@ public class Manager implements Closeable {
                         try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) {
                             var s = new DeviceGroupsInputStream(attachmentAsStream);
                             DeviceGroup g;
-                            while ((g = s.read()) != null) {
+                            while (true) {
+                                try {
+                                    g = s.read();
+                                } catch (IOException e) {
+                                    logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage());
+                                    continue;
+                                }
+                                if (g == null) {
+                                    break;
+                                }
                                 var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId()));
                                 if (syncGroup != null) {
                                     if (g.getName().isPresent()) {
@@ -2081,7 +2320,17 @@ public class Manager implements Closeable {
                                 .asPointer(), tmpFile)) {
                             var s = new DeviceContactsInputStream(attachmentAsStream);
                             DeviceContact c;
-                            while ((c = s.read()) != null) {
+                            while (true) {
+                                try {
+                                    c = s.read();
+                                } catch (IOException e) {
+                                    logger.warn("Sync contacts contained invalid contact, ignoring: {}",
+                                            e.getMessage());
+                                    continue;
+                                }
+                                if (c == null) {
+                                    break;
+                                }
                                 if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
                                     account.setProfileKey(c.getProfileKey().get());
                                 }
@@ -2492,10 +2741,14 @@ 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 && ((GroupInfoV2) group).getGroup() == null) {
+        if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) {
             final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
-            ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
+            ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
             account.getGroupStore().updateGroup(group);
         }
         return group;