mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Persist group state in backup.
This commit is contained in:
@@ -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<BackupRecipient
|
||||
val extras = RecipientTableCursorUtil.getExtras(cursor)
|
||||
val showAsStoryState: GroupTable.ShowAsStoryState = GroupTable.ShowAsStoryState.deserialize(cursor.requireInt(GroupTable.SHOW_AS_STORY_STATE))
|
||||
|
||||
val decryptedGroup: DecryptedGroup = DecryptedGroup.ADAPTER.decode(cursor.requireBlob(GroupTable.V2_DECRYPTED_GROUP)!!)
|
||||
|
||||
return BackupRecipient(
|
||||
id = cursor.requireLong(RecipientTable.ID),
|
||||
group = BackupGroup(
|
||||
@@ -338,7 +479,8 @@ class BackupGroupIterator(private val cursor: Cursor) : Iterator<BackupRecipient
|
||||
whitelisted = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
|
||||
hideStory = extras?.hideStory() ?: false,
|
||||
storySendMode = showAsStoryState.toGroupStorySendMode(),
|
||||
name = cursor.requireString(GroupTable.TITLE) ?: ""
|
||||
name = cursor.requireString(GroupTable.TITLE) ?: "",
|
||||
snapshot = decryptedGroup.toSnapshot()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user