mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00: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()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user