diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt index 9a99d9bb86..3aab49c032 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt @@ -13,6 +13,7 @@ import org.signal.core.util.SqlUtil import org.signal.core.util.deleteAll import org.signal.core.util.logging.Log import org.signal.core.util.nullIfBlank +import org.signal.core.util.requireBlob import org.signal.core.util.requireBoolean import org.signal.core.util.requireInt import org.signal.core.util.requireLong @@ -23,7 +24,16 @@ import org.signal.core.util.toInt import org.signal.core.util.update import org.signal.libsignal.zkgroup.InvalidInputException import org.signal.libsignal.zkgroup.groups.GroupMasterKey +import org.signal.libsignal.zkgroup.groups.GroupSecretParams +import org.signal.storageservice.protos.groups.AccessControl +import org.signal.storageservice.protos.groups.Member +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember import org.signal.storageservice.protos.groups.local.DecryptedGroup +import org.signal.storageservice.protos.groups.local.DecryptedMember +import org.signal.storageservice.protos.groups.local.DecryptedPendingMember +import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember +import org.signal.storageservice.protos.groups.local.DecryptedTimer +import org.signal.storageservice.protos.groups.local.EnabledState import org.thoughtcrime.securesms.backup.v2.BackupState import org.thoughtcrime.securesms.backup.v2.proto.AccountData import org.thoughtcrime.securesms.backup.v2.proto.Contact @@ -37,13 +47,14 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.groups.GroupId -import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations +import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.PNI import org.whispersystems.signalservice.api.util.toByteArray @@ -103,7 +114,8 @@ fun RecipientTable.getGroupsForBackup(): BackupGroupIterator { "${RecipientTable.TABLE_NAME}.${RecipientTable.EXTRAS}", "${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY}", "${GroupTable.TABLE_NAME}.${GroupTable.SHOW_AS_STORY_STATE}", - "${GroupTable.TABLE_NAME}.${GroupTable.TITLE}" + "${GroupTable.TABLE_NAME}.${GroupTable.TITLE}", + "${GroupTable.TABLE_NAME}.${GroupTable.V2_DECRYPTED_GROUP}" ) .from( """ @@ -219,9 +231,8 @@ private fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId { val masterKey = GroupMasterKey(group.masterKey.toByteArray()) val groupId = GroupId.v2(masterKey) - val placeholderState = DecryptedGroup.Builder() - .revision(GroupsV2StateProcessor.PLACEHOLDER_REVISION) - .build() + val operations = ApplicationDependencies.getGroupsV2Operations().forGroup(GroupSecretParams.deriveFromMasterKey(masterKey)) + val decryptedState = group.snapshot!!.toDecryptedGroup(operations) val values = ContentValues().apply { put(RecipientTable.GROUP_ID, groupId.toString()) @@ -236,20 +247,148 @@ private fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId { } val recipientId = writableDatabase.insert(RecipientTable.TABLE_NAME, null, values) - val groupValues = ContentValues().apply { - put(GroupTable.RECIPIENT_ID, recipientId) - put(GroupTable.GROUP_ID, groupId.toString()) - put(GroupTable.TITLE, group.name) - put(GroupTable.V2_MASTER_KEY, masterKey.serialize()) - put(GroupTable.V2_DECRYPTED_GROUP, placeholderState.encode()) - put(GroupTable.V2_REVISION, placeholderState.revision) - put(GroupTable.SHOW_AS_STORY_STATE, group.storySendMode.toGroupShowAsStoryState().code) - } - writableDatabase.insert(GroupTable.TABLE_NAME, null, groupValues) + SignalDatabase.groups.create(masterKey, decryptedState) return RecipientId.from(recipientId) } +private fun Group.AccessControl.AccessRequired.toLocal(): AccessControl.AccessRequired { + return when (this) { + Group.AccessControl.AccessRequired.UNKNOWN -> AccessControl.AccessRequired.UNKNOWN + Group.AccessControl.AccessRequired.ANY -> AccessControl.AccessRequired.ANY + Group.AccessControl.AccessRequired.MEMBER -> AccessControl.AccessRequired.MEMBER + Group.AccessControl.AccessRequired.ADMINISTRATOR -> AccessControl.AccessRequired.ADMINISTRATOR + Group.AccessControl.AccessRequired.UNSATISFIABLE -> AccessControl.AccessRequired.UNSATISFIABLE + } +} + +private fun Group.AccessControl.toLocal(): AccessControl { + return AccessControl(members = this.members.toLocal(), attributes = this.attributes.toLocal(), addFromInviteLink = this.addFromInviteLink.toLocal()) +} + +private fun Group.Member.Role.toLocal(): Member.Role { + return when (this) { + Group.Member.Role.UNKNOWN -> Member.Role.UNKNOWN + Group.Member.Role.DEFAULT -> Member.Role.DEFAULT + Group.Member.Role.ADMINISTRATOR -> Member.Role.ADMINISTRATOR + } +} + +private fun AccessControl.AccessRequired.toSnapshot(): Group.AccessControl.AccessRequired { + return when (this) { + AccessControl.AccessRequired.UNKNOWN -> Group.AccessControl.AccessRequired.UNKNOWN + AccessControl.AccessRequired.ANY -> Group.AccessControl.AccessRequired.ANY + AccessControl.AccessRequired.MEMBER -> Group.AccessControl.AccessRequired.MEMBER + AccessControl.AccessRequired.ADMINISTRATOR -> Group.AccessControl.AccessRequired.ADMINISTRATOR + AccessControl.AccessRequired.UNSATISFIABLE -> Group.AccessControl.AccessRequired.UNSATISFIABLE + } +} + +private fun AccessControl.toSnapshot(): Group.AccessControl { + return Group.AccessControl(members = members.toSnapshot(), attributes = attributes.toSnapshot(), addFromInviteLink = addFromInviteLink.toSnapshot()) +} + +private fun Member.Role.toSnapshot(): Group.Member.Role { + return when (this) { + Member.Role.UNKNOWN -> Group.Member.Role.UNKNOWN + Member.Role.DEFAULT -> Group.Member.Role.DEFAULT + Member.Role.ADMINISTRATOR -> Group.Member.Role.ADMINISTRATOR + } +} + +private fun DecryptedGroup.toSnapshot(): Group.GroupSnapshot { + return Group.GroupSnapshot( + title = title, + avatar = avatar, + disappearingMessagesTimer = disappearingMessagesTimer?.duration ?: 0, + accessControl = accessControl?.toSnapshot(), + version = revision, + members = members.map { it.toSnapshot() }, + membersPendingProfileKey = pendingMembers.map { it.toSnapshot() }, + membersPendingAdminApproval = requestingMembers.map { it.toSnapshot() }, + inviteLinkPassword = inviteLinkPassword, + description = description, + announcements_only = isAnnouncementGroup == EnabledState.ENABLED, + members_banned = bannedMembers.map { it.toSnapshot() } + ) +} + +private fun Group.Member.toLocal(): DecryptedMember { + return DecryptedMember(aciBytes = userId, role = role.toLocal(), profileKey = profileKey, joinedAtRevision = joinedAtVersion) +} + +private fun DecryptedMember.toSnapshot(): Group.Member { + return Group.Member(userId = aciBytes, role = role.toSnapshot(), profileKey = profileKey, joinedAtVersion = joinedAtRevision) +} + +private fun Group.MemberPendingProfileKey.toLocal(operations: GroupsV2Operations.GroupOperations): DecryptedPendingMember { + return DecryptedPendingMember( + serviceIdBytes = member!!.userId, + role = member.role.toLocal(), + addedByAci = addedByUserId, + timestamp = timestamp, + serviceIdCipherText = operations.encryptServiceId(ServiceId.Companion.parseOrNull(member.userId)) + ) +} + +private fun DecryptedPendingMember.toSnapshot(): Group.MemberPendingProfileKey { + return Group.MemberPendingProfileKey( + member = Group.Member( + userId = serviceIdBytes, + role = role.toSnapshot() + ), + addedByUserId = addedByAci, + timestamp = timestamp + ) +} + +private fun Group.MemberPendingAdminApproval.toLocal(): DecryptedRequestingMember { + return DecryptedRequestingMember( + aciBytes = userId, + profileKey = profileKey, + timestamp = timestamp + ) +} + +private fun DecryptedRequestingMember.toSnapshot(): Group.MemberPendingAdminApproval { + return Group.MemberPendingAdminApproval( + userId = aciBytes, + profileKey = profileKey, + timestamp = timestamp + ) +} + +private fun Group.MemberBanned.toLocal(): DecryptedBannedMember { + return DecryptedBannedMember( + serviceIdBytes = userId, + timestamp = timestamp + ) +} + +private fun DecryptedBannedMember.toSnapshot(): Group.MemberBanned { + return Group.MemberBanned( + userId = serviceIdBytes, + timestamp = timestamp + ) +} + +private fun Group.GroupSnapshot.toDecryptedGroup(operations: GroupsV2Operations.GroupOperations): DecryptedGroup { + return DecryptedGroup( + title = title, + avatar = avatar, + disappearingMessagesTimer = DecryptedTimer(duration = disappearingMessagesTimer), + accessControl = accessControl?.toLocal(), + revision = version, + members = members.map { member -> member.toLocal() }, + pendingMembers = membersPendingProfileKey.map { pending -> pending.toLocal(operations) }, + requestingMembers = membersPendingAdminApproval.map { requesting -> requesting.toLocal() }, + inviteLinkPassword = inviteLinkPassword, + description = description, + isAnnouncementGroup = if (announcements_only) EnabledState.ENABLED else EnabledState.DISABLED, + bannedMembers = members_banned.map { it.toLocal() } + ) +} + private fun Contact.toLocalExtras(): RecipientExtras { return RecipientExtras( hideStory = this.hideStory @@ -331,6 +470,8 @@ class BackupGroupIterator(private val cursor: Cursor) : Iterator GroupSnapshot to avoid the naming conflict. + message GroupSnapshot { + bytes publicKey = 1; + string title = 2; + string description = 11; + string avatar = 3; + uint32 disappearingMessagesTimer = 4; + AccessControl accessControl = 5; + uint32 version = 6; + repeated Member members = 7; + repeated MemberPendingProfileKey membersPendingProfileKey = 8; + repeated MemberPendingAdminApproval membersPendingAdminApproval = 9; + bytes inviteLinkPassword = 10; + bool announcements_only = 12; + repeated MemberBanned members_banned = 13; + } + + message Member { + enum Role { + UNKNOWN = 0; + DEFAULT = 1; + ADMINISTRATOR = 2; + } + + bytes userId = 1; + Role role = 2; + bytes profileKey = 3; + bytes presentation = 4; + uint32 joinedAtVersion = 5; + } + + message MemberPendingProfileKey { + Member member = 1; + bytes addedByUserId = 2; + uint64 timestamp = 3; + } + + message MemberPendingAdminApproval { + bytes userId = 1; + bytes profileKey = 2; + bytes presentation = 3; + uint64 timestamp = 4; + } + + message MemberBanned { + bytes userId = 1; + uint64 timestamp = 2; + } + + message AccessControl { + enum AccessRequired { + UNKNOWN = 0; + ANY = 1; + MEMBER = 2; + ADMINISTRATOR = 3; + UNSATISFIABLE = 4; + } + + AccessRequired attributes = 1; + AccessRequired members = 2; + AccessRequired addFromInviteLink = 3; + } } message Self {}