Persist group state in backup.

This commit is contained in:
Clark
2024-04-25 09:23:36 -04:00
committed by Greyson Parrelli
parent e60b32202e
commit d983265e08
2 changed files with 226 additions and 16 deletions

View File

@@ -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()
)
)
}

View File

@@ -121,6 +121,74 @@ message Group {
bool hideStory = 3;
StorySendMode storySendMode = 4;
string name = 5;
GroupSnapshot snapshot = 6;
// These are simply plaintext copies of the groups proto from Groups.proto.
// They should be kept completely in-sync with Groups.proto.
// These exist to allow us to have the latest snapshot of a group during restoration without having to hit the network.
// We would use Groups.proto if we could, but we want a plaintext version to improve export readability.
// For documentation, defer to Groups.proto. The only name change is Group -> 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 {}