Migrate GV1 to GV2 on to server. Allow query of group status.

This commit is contained in:
Alan Evans 2020-10-20 17:43:17 -03:00
parent 31e137cf6d
commit 985a220fca
5 changed files with 160 additions and 63 deletions

View file

@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import java.io.IOException;
@ -80,6 +79,14 @@ public final class GroupManager {
}
}
@WorkerThread
public static void migrateGroupToServer(@NonNull Context context,
@NonNull GroupId.V1 groupIdV1)
throws IOException, GroupChangeFailedException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
{
new GroupManagerV2(context).migrateGroupOnToServer(groupIdV1);
}
private static Set<RecipientId> getMemberIds(Collection<Recipient> recipients) {
Set<RecipientId> results = new HashSet<>(recipients.size());
@ -164,6 +171,21 @@ public final class GroupManager {
}
}
@WorkerThread
public static V2GroupServerStatus v2GroupStatus(@NonNull Context context,
@NonNull GroupMasterKey groupMasterKey)
throws IOException
{
try {
new GroupManagerV2(context).groupServerQuery(groupMasterKey);
return V2GroupServerStatus.FULL_OR_PENDING_MEMBER;
} catch (GroupNotAMemberException e) {
return V2GroupServerStatus.NOT_A_MEMBER;
} catch (GroupDoesNotExistException e) {
return V2GroupServerStatus.DOES_NOT_EXIST;
}
}
@WorkerThread
public static void setMemberAdmin(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@ -303,7 +325,7 @@ public final class GroupManager {
} else {
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).requireGroup(groupId);
List<RecipientId> members = groupRecord.getMembers();
byte[] avatar = groupRecord.hasAvatar() ? Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId())) : null;
byte[] avatar = groupRecord.hasAvatar() ? AvatarHelper.getAvatarBytes(context, groupRecord.getRecipientId()) : null;
Set<RecipientId> recipientIds = new HashSet<>(members);
int originalSize = recipientIds.size();
@ -388,4 +410,13 @@ public final class GroupManager {
ENABLED,
ENABLED_WITH_APPROVAL
}
public enum V2GroupServerStatus {
/** The group does not exist. The expected pre-migration state for V1 groups. */
DOES_NOT_EXIST,
/** Group exists but self is not in the group. */
NOT_A_MEMBER,
/** Self is a full or pending member of the group. */
FULL_OR_PENDING_MEMBER
}
}

View file

@ -138,6 +138,31 @@ final class GroupManagerV2 {
return new GroupUpdater(groupId, GroupsV2ProcessingLock.acquireGroupProcessingLock());
}
@WorkerThread
void groupServerQuery(@NonNull GroupMasterKey groupMasterKey)
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
{
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
.getCurrentGroupStateFromServer();
}
@WorkerThread
void migrateGroupOnToServer(@NonNull GroupId.V1 groupIdV1)
throws IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException, GroupChangeFailedException
{
GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupIdV1);
String name = groupRecord.getTitle();
byte[] avatar = groupRecord.hasAvatar() ? AvatarHelper.getAvatarBytes(context, groupRecord.getRecipientId()) : null;
int messageTimer = Recipient.resolved(groupRecord.getRecipientId()).getExpireMessages();
Set<RecipientId> memberIds = Stream.of(groupRecord.getMembers())
.filterNot(m -> m.equals(Recipient.self().getId()))
.collect(Collectors.toSet());
createGroupOnServer(groupSecretParams, name, avatar, memberIds, Member.Role.ADMINISTRATOR, messageTimer);
}
final class GroupCreator extends LockOwner {
GroupCreator(@NonNull Closeable lock) {
@ -150,59 +175,43 @@ final class GroupManagerV2 {
@Nullable byte[] avatar)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
{
if (!GroupsV2CapabilityChecker.allAndSelfSupportGroupsV2AndUuid(members)) {
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities");
}
return createGroup(name, avatar, members);
}
GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
Set<GroupCandidate> candidates = new HashSet<>(groupCandidateHelper.recipientIdsToCandidates(members));
if (SignalStore.internalValues().gv2ForceInvites()) {
candidates = GroupCandidate.withoutProfileKeyCredentials(candidates);
}
if (!self.hasProfileKeyCredential()) {
Log.w(TAG, "Cannot create a V2 group as self does not have a versioned profile");
throw new MembershipNotSuitableForV2Exception("Cannot create a V2 group as self does not have a versioned profile");
}
GroupsV2Operations.NewGroup newGroup = groupsV2Operations.createNewGroup(name,
Optional.fromNullable(avatar),
self,
candidates);
GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
GroupMasterKey masterKey = groupSecretParams.getMasterKey();
@WorkerThread
private @NonNull GroupManager.GroupActionResult createGroup(@Nullable String name,
@Nullable byte[] avatar,
@NonNull Collection<RecipientId> members)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
{
GroupSecretParams groupSecretParams = GroupSecretParams.generate();
DecryptedGroup decryptedGroup;
try {
groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, ApplicationDependencies.getGroupsV2Authorization().getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
if (decryptedGroup == null) {
throw new GroupChangeFailedException();
}
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup);
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
AvatarHelper.setAvatar(context, groupRecipientId, avatar != null ? new ByteArrayInputStream(avatar) : null);
groupDatabase.onAvatarUpdated(groupId, avatar != null);
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder(GroupChangeReconstruct.reconstructGroupChange(DecryptedGroup.newBuilder().build(), decryptedGroup))
.setEditor(UuidUtil.toByteString(selfUuid))
.build();
RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, new GroupMutation(null, groupChange, decryptedGroup), null);
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient,
recipientAndThread.threadId,
decryptedGroup.getMembersCount() - 1,
getPendingMemberRecipientIds(decryptedGroup.getPendingMembersList()));
} catch (VerificationFailedException | InvalidGroupStateException | GroupExistsException e) {
decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, Member.Role.DEFAULT, 0);
} catch (GroupAlreadyExistsException e) {
throw new GroupChangeFailedException(e);
}
GroupMasterKey masterKey = groupSecretParams.getMasterKey();
GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup);
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
AvatarHelper.setAvatar(context, groupRecipientId, avatar != null ? new ByteArrayInputStream(avatar) : null);
groupDatabase.onAvatarUpdated(groupId, avatar != null);
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder(GroupChangeReconstruct.reconstructGroupChange(DecryptedGroup.newBuilder().build(), decryptedGroup))
.setEditor(UuidUtil.toByteString(selfUuid))
.build();
RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, new GroupMutation(null, groupChange, decryptedGroup), null);
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient,
recipientAndThread.threadId,
decryptedGroup.getMembersCount() - 1,
getPendingMemberRecipientIds(decryptedGroup.getPendingMembersList()));
}
}
@ -229,7 +238,7 @@ final class GroupManagerV2 {
@NonNull GroupManager.GroupActionResult addMembers(@NonNull Collection<RecipientId> newMembers)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, MembershipNotSuitableForV2Exception
{
if (!GroupsV2CapabilityChecker.allSupportGroupsV2AndUuid(newMembers)) {
if (!GroupsV2CapabilityChecker.allHaveUuidAndSupportGroupsV2(newMembers)) {
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities");
}
@ -586,6 +595,56 @@ final class GroupManagerV2 {
}
}
@WorkerThread
private @NonNull DecryptedGroup createGroupOnServer(@NonNull GroupSecretParams groupSecretParams,
@Nullable String name,
@Nullable byte[] avatar,
@NonNull Collection<RecipientId> members,
@NonNull Member.Role memberRole,
int disappearingMessageTimerSeconds)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
{
if (!GroupsV2CapabilityChecker.allAndSelfHaveUuidAndSupportGroupsV2(members)) {
throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 capability or we don't have their UUID");
}
GroupCandidate self = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId());
Set<GroupCandidate> candidates = new HashSet<>(groupCandidateHelper.recipientIdsToCandidates(members));
if (SignalStore.internalValues().gv2ForceInvites()) {
Log.w(TAG, "Forcing GV2 invites due to internal setting");
candidates = GroupCandidate.withoutProfileKeyCredentials(candidates);
}
if (!self.hasProfileKeyCredential()) {
Log.w(TAG, "Cannot create a V2 group as self does not have a versioned profile");
throw new MembershipNotSuitableForV2Exception("Cannot create a V2 group as self does not have a versioned profile");
}
GroupsV2Operations.NewGroup newGroup = groupsV2Operations.createNewGroup(groupSecretParams,
name,
Optional.fromNullable(avatar),
self,
candidates,
memberRole,
disappearingMessageTimerSeconds);
try {
groupsV2Api.putNewGroup(newGroup, authorization.getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
DecryptedGroup decryptedGroup = groupsV2Api.getGroup(groupSecretParams, ApplicationDependencies.getGroupsV2Authorization().getAuthorizationForToday(Recipient.self().requireUuid(), groupSecretParams));
if (decryptedGroup == null) {
throw new GroupChangeFailedException();
}
return decryptedGroup;
} catch (VerificationFailedException | InvalidGroupStateException e) {
throw new GroupChangeFailedException(e);
} catch (GroupExistsException e) {
throw new GroupAlreadyExistsException(e);
}
}
final class GroupJoiner extends LockOwner {
private final GroupId.V2 groupId;
private final GroupLinkPassword password;
@ -777,7 +836,7 @@ final class GroupManagerV2 {
private @NonNull GroupChange joinGroupOnServer(boolean requestToJoin, int currentRevision)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupLinkNotActiveException, GroupJoinAlreadyAMemberException
{
if (!GroupsV2CapabilityChecker.allAndSelfSupportGroupsV2AndUuid(Collections.singleton(Recipient.self().getId()))) {
if (!GroupsV2CapabilityChecker.allAndSelfHaveUuidAndSupportGroupsV2(Collections.singleton(Recipient.self().getId()))) {
throw new MembershipNotSuitableForV2Exception("Self does not support GV2 or UUID capabilities");
}

View file

@ -52,18 +52,18 @@ public final class GroupsV2CapabilityChecker {
}
@WorkerThread
static boolean allAndSelfSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
static boolean allAndSelfHaveUuidAndSupportGroupsV2(@NonNull Collection<RecipientId> recipientIds)
throws IOException
{
HashSet<RecipientId> recipientIdsSet = new HashSet<>(recipientIds);
recipientIdsSet.add(Recipient.self().getId());
return allSupportGroupsV2AndUuid(recipientIdsSet);
return allHaveUuidAndSupportGroupsV2(recipientIdsSet);
}
@WorkerThread
static boolean allSupportGroupsV2AndUuid(@NonNull Collection<RecipientId> recipientIds)
static boolean allHaveUuidAndSupportGroupsV2(@NonNull Collection<RecipientId> recipientIds)
throws IOException
{
Set<RecipientId> recipientIdsSet = new HashSet<>(recipientIds);

View file

@ -96,6 +96,11 @@ public class AvatarHelper {
return ModernDecryptingPartInputStream.createFor(attachmentSecret, avatarFile, 0);
}
public static byte[] getAvatarBytes(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException {
return hasAvatar(context, recipientId) ? Util.readFully(getAvatar(context, recipientId))
: null;
}
/**
* Returns the size of the avatar on disk.
*/

View file

@ -79,23 +79,26 @@ public final class GroupsV2Operations {
* @param self You will be member 0 and the only admin.
* @param members Members must not contain self. Members will be non-admin members of the group.
*/
public NewGroup createNewGroup(final String title,
public NewGroup createNewGroup(final GroupSecretParams groupSecretParams,
final String title,
final Optional<byte[]> avatar,
final GroupCandidate self,
final Set<GroupCandidate> members) {
final Set<GroupCandidate> members,
final Member.Role memberRole,
final int disappearingMessageTimerSeconds)
{
if (members.contains(self)) {
throw new IllegalArgumentException("Members must not contain self");
}
final GroupSecretParams groupSecretParams = GroupSecretParams.generate(random);
final GroupOperations groupOperations = forGroup(groupSecretParams);
final GroupOperations groupOperations = forGroup(groupSecretParams);
Group.Builder group = Group.newBuilder()
.setRevision(0)
.setPublicKey(ByteString.copyFrom(groupSecretParams.getPublicParams().serialize()))
.setTitle(groupOperations.encryptTitle(title))
.setDisappearingMessagesTimer(groupOperations.encryptTimer(0))
.setDisappearingMessagesTimer(groupOperations.encryptTimer(disappearingMessageTimerSeconds))
.setAccessControl(AccessControl.newBuilder()
.setAttributes(AccessControl.AccessRequired.MEMBER)
.setMembers(AccessControl.AccessRequired.MEMBER));
@ -103,13 +106,12 @@ public final class GroupsV2Operations {
group.addMembers(groupOperations.member(self.getProfileKeyCredential().get(), Member.Role.ADMINISTRATOR));
for (GroupCandidate credential : members) {
Member.Role newMemberRole = Member.Role.DEFAULT;
ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orNull();
if (profileKeyCredential != null) {
group.addMembers(groupOperations.member(profileKeyCredential, newMemberRole));
group.addMembers(groupOperations.member(profileKeyCredential, memberRole));
} else {
group.addPendingMembers(groupOperations.invitee(credential.getUuid(), newMemberRole));
group.addPendingMembers(groupOperations.invitee(credential.getUuid(), memberRole));
}
}