Handle message deletion

This commit is contained in:
SessionHero01 2024-09-25 11:00:57 +10:00
parent 57c95ea7d6
commit b13b7e647f
No known key found for this signature in database
6 changed files with 139 additions and 112 deletions

View File

@ -1339,80 +1339,6 @@ open class Storage(
} }
} }
override fun handleMemberLeftNotification(message: GroupUpdated, closedGroupId: AccountId) {
insertGroupInfoChange(message, closedGroupId)
}
override fun sendGroupUpdateDeleteMessage(groupSessionId: String, messageHashes: List<String>): Promise<Unit, Exception> {
val closedGroup = configFactory.userGroups?.getClosedGroup(groupSessionId)
?: return Promise.ofFail(NullPointerException("No group found"))
val keys = configFactory.getGroupKeysConfig(AccountId(groupSessionId))
?: return Promise.ofFail(NullPointerException("No group keys found"))
val adminKey = if (closedGroup.hasAdminKey()) closedGroup.adminKey else null
val authData = closedGroup.authData
val auth = if (adminKey != null) {
OwnedSwarmAuth.ofClosedGroup(AccountId(groupSessionId), adminKey)
} else if (authData != null) {
GroupSubAccountSwarmAuth(keys, AccountId(groupSessionId), authData)
} else {
return Promise.ofFail(IllegalStateException("No auth data nor admin key found"))
}
val groupDestination = Destination.ClosedGroup(groupSessionId)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(groupDestination)
val timestamp = SnodeAPI.nowWithOffset
val signature = adminKey?.let { key ->
SodiumUtilities.sign(
buildDeleteMemberContentSignature(memberIds = emptyList(), messageHashes, timestamp),
key
)
}
val message = GroupUpdated(
GroupUpdateMessage.newBuilder()
.setDeleteMemberContent(
GroupUpdateDeleteMemberContentMessage.newBuilder()
.addAllMessageHashes(messageHashes)
.let {
if (signature != null) it.setAdminSignature(ByteString.copyFrom(signature))
else it
}
)
.build()
).apply {
sentTimestamp = timestamp
}
// Delete might need fake hash?
val authenticatedDelete = if (adminKey == null) null else buildAuthenticatedDeleteBatchInfo(auth, messageHashes, required = true)
val authenticatedStore = buildAuthenticatedStoreBatchInfo(
namespace = Namespace.CLOSED_GROUP_MESSAGES(),
message = MessageSender.buildWrappedMessageToSnode(Destination.ClosedGroup(groupSessionId), message, false),
auth = auth
)
keys.free()
// delete only present when admin
val storeIndex = if (adminKey != null) 1 else 0
return SnodeAPI.getSingleTargetSnode(groupSessionId).bind { snode ->
SnodeAPI.getRawBatchResponse(
snode,
groupSessionId,
listOfNotNull(authenticatedDelete, authenticatedStore),
sequence = true
)
}.map { rawResponse ->
val results = (rawResponse["results"] as ArrayList<Any>)[storeIndex] as Map<String,Any>
val hash = results["hash"] as? String
message.serverHash = hash
MessageSender.handleSuccessfulMessageSend(message, groupDestination, false)
}
}
override fun setServerCapabilities(server: String, capabilities: List<String>) { override fun setServerCapabilities(server: String, capabilities: List<String>) {
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities) return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
} }

View File

@ -1042,6 +1042,111 @@ class GroupManagerV2Impl @Inject constructor(
storage.insertGroupInfoChange(message, groupId) storage.insertGroupInfoChange(message, groupId)
} }
override suspend fun requestMessageDeletion(
groupId: AccountId,
messageHashes: List<String>
): Unit = withContext(dispatcher) {
// To delete messages from a group, there are a few considerations:
// 1. Messages are stored on every member's device, we need a way to ask them to delete their stored messages
// 2. Messages are also stored on the group swarm, only the group admin can delete them
// So we will send a group message to ask members to delete the messages,
// meanwhile, if we are admin we can just delete those messages from the group swarm, and otherwise
// the admins can pick up the group message and delete the messages on our behalf.
val userGroups = requireNotNull(configFactory.userGroups) { "User groups config is not available" }
val group = requireNotNull(userGroups.getClosedGroup(groupId.hexString)) {
"Group doesn't exist"
}
val userPubKey = requireNotNull(storage.getUserPublicKey()) { "No current user available" }
// Check if we can actually delete these messages
check(
group.hasAdminKey() ||
storage.ensureMessageHashesAreSender(messageHashes.toSet(), userPubKey, groupId.hexString)
) {
"Cannot delete messages that are not sent by us"
}
// If we are admin, we can delete the messages from the group swarm
group.adminKey?.let { adminKey ->
deleteMessageFromGroupSwarm(groupId, OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), messageHashes)
}
// Construct a message to ask members to delete the messages, sign if we are admin, then send
val timestamp = SnodeAPI.nowWithOffset
val signature = group.adminKey?.let { key ->
SodiumUtilities.sign(
buildDeleteMemberContentSignature(memberIds = emptyList(), messageHashes, timestamp),
key
)
}
val message = GroupUpdated(
GroupUpdateMessage.newBuilder()
.setDeleteMemberContent(
GroupUpdateDeleteMemberContentMessage.newBuilder()
.addAllMessageHashes(messageHashes)
.let {
if (signature != null) it.setAdminSignature(ByteString.copyFrom(signature))
else it
}
)
.build()
).apply {
sentTimestamp = timestamp
}
val groupAddress = Address.fromSerialized(groupId.hexString)
MessageSender.sendNonDurably(message, groupAddress, false).await()
}
override suspend fun handleDeleteMemberContent(
groupId: AccountId,
deleteMemberContent: GroupUpdateDeleteMemberContentMessage,
sender: AccountId,
senderIsVerifiedAdmin: Boolean,
): Unit = withContext(dispatcher) {
val threadId = requireNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) {
"No thread ID found for the group"
}
val hashes = deleteMemberContent.messageHashesList
val memberIds = deleteMemberContent.memberSessionIdsList
if (hashes.isNotEmpty()) {
if (senderIsVerifiedAdmin) {
// We'll delete everything the admin says
storage.deleteMessagesByHash(threadId, hashes)
} else if (storage.ensureMessageHashesAreSender(hashes.toSet(), sender.hexString, groupId.hexString)) {
// ensure that all message hashes belong to user
// storage delete
storage.deleteMessagesByHash(threadId, hashes)
}
}
if (memberIds.isNotEmpty() && senderIsVerifiedAdmin) {
for (member in memberIds) {
storage.deleteMessagesByUser(threadId, member)
}
}
val adminKey = configFactory.userGroups?.getClosedGroup(groupId.hexString)?.adminKey
if (!senderIsVerifiedAdmin && adminKey != null) {
// If the deletion request comes from a non-admin, and we as an admin, will also delete
// the content from the swarm, provided that the messages are actually sent by that user
if (storage.ensureMessageHashesAreSender(hashes.toSet(), sender.hexString, groupId.hexString)) {
deleteMessageFromGroupSwarm(groupId, OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), hashes)
}
// The non-admin user shouldn't be able to delete other user's messages so we will
// ignore the memberIds in the message
}
}
private suspend fun deleteMessageFromGroupSwarm(groupId: AccountId, auth: OwnedSwarmAuth, hashes: List<String>) {
SnodeAPI.sendBatchRequest(
groupId, SnodeAPI.buildAuthenticatedDeleteBatchInfo(auth, hashes)
)
}
private fun BatchResponse.requireAllRequestsSuccessful(errorMessage: String) { private fun BatchResponse.requireAllRequestsSuccessful(errorMessage: String) {
val firstError = this.results.firstOrNull { it.code != 200 } val firstError = this.results.firstOrNull { it.code != 200 }

View File

@ -242,10 +242,10 @@ class DefaultConversationRepository @Inject constructor(
if (recipient.isClosedGroupV2Recipient) { if (recipient.isClosedGroupV2Recipient) {
// admin check internally, assume either admin or all belong to user // admin check internally, assume either admin or all belong to user
storage.sendGroupUpdateDeleteMessage( groupManager.requestMessageDeletion(
groupSessionId = recipient.address.serialize(), groupId = AccountId(publicKey),
messageHashes = listOf(serverHash) messageHashes = listOf(serverHash)
).await() )
} else { } else {
SnodeAPI.deleteMessage( SnodeAPI.deleteMessage(
publicKey = publicKey, publicKey = publicKey,

View File

@ -177,8 +177,6 @@ interface StorageProtocol {
fun insertGroupInfoLeaving(closedGroup: AccountId): Long? fun insertGroupInfoLeaving(closedGroup: AccountId): Long?
fun insertGroupInviteControlMessage(sentTimestamp: Long, senderPublicKey: String, closedGroup: AccountId, groupName: String): Long? fun insertGroupInviteControlMessage(sentTimestamp: Long, senderPublicKey: String, closedGroup: AccountId, groupName: String): Long?
fun updateGroupInfoChange(messageId: Long, newType: UpdateMessageData.Kind) fun updateGroupInfoChange(messageId: Long, newType: UpdateMessageData.Kind)
fun handleMemberLeftNotification(message: GroupUpdated, closedGroupId: AccountId)
fun sendGroupUpdateDeleteMessage(groupSessionId: String, messageHashes: List<String>): Promise<Unit, Exception>
// Groups // Groups
fun getAllGroups(includeInactive: Boolean): List<GroupRecord> fun getAllGroups(includeInactive: Boolean): List<GroupRecord>

View File

@ -3,6 +3,8 @@ package org.session.libsession.messaging.groups
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.messages.control.GroupUpdated import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
/** /**
@ -57,4 +59,13 @@ interface GroupManagerV2 {
suspend fun handleKicked(groupId: AccountId) suspend fun handleKicked(groupId: AccountId)
suspend fun setName(groupId: AccountId, newName: String) suspend fun setName(groupId: AccountId, newName: String)
suspend fun requestMessageDeletion(groupId: AccountId, messageHashes: List<String>)
suspend fun handleDeleteMemberContent(
groupId: AccountId,
deleteMemberContent: GroupUpdateDeleteMemberContentMessage,
sender: AccountId,
senderIsVerifiedAdmin: Boolean,
)
} }

View File

@ -6,6 +6,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.Sodium import network.loki.messenger.libsession_util.util.Sodium
import network.loki.messenger.libsession_util.util.afterSend
import org.session.libsession.avatars.AvatarHelper import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.database.userAuth import org.session.libsession.database.userAuth
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
@ -585,45 +586,31 @@ private fun MessageReceiver.handleGroupUpdated(message: GroupUpdated, closedGrou
} }
private fun handleDeleteMemberContent(message: GroupUpdated, closedGroup: AccountId) { private fun handleDeleteMemberContent(message: GroupUpdated, closedGroup: AccountId) {
val storage = MessagingModuleConfiguration.shared.storage
val deleteMemberContent = message.inner.deleteMemberContent val deleteMemberContent = message.inner.deleteMemberContent
val adminSig = if (deleteMemberContent.hasAdminSignature()) deleteMemberContent.adminSignature.toByteArray()!! else byteArrayOf() val adminSig = if (deleteMemberContent.hasAdminSignature()) deleteMemberContent.adminSignature.toByteArray()!! else byteArrayOf()
val memberIds = deleteMemberContent.memberSessionIdsList val hasValidAdminSignature = adminSig.isNotEmpty() && runCatching {
val hashes = deleteMemberContent.messageHashesList
val threadId = storage.getThreadId(Address.fromSerialized(closedGroup.hexString))!!
val messageToValidate = buildDeleteMemberContentSignature(
memberIds = memberIds.asSequence().map(::AccountId).asIterable(),
messageHashes = hashes,
timestamp = message.sentTimestamp!!
)
if (hashes.isNotEmpty()) {
// Delete all hashes conditionally
if (storage.ensureMessageHashesAreSender(hashes.toSet(), message.sender!!, closedGroup.hexString)) {
// ensure that all message hashes belong to user
// storage delete
storage.deleteMessagesByHash(threadId, hashes)
} else {
// otherwise assert a valid admin sig exists
verifyAdminSignature( verifyAdminSignature(
closedGroup, closedGroup,
adminSig, adminSig,
messageToValidate buildDeleteMemberContentSignature(
memberIds = deleteMemberContent.memberSessionIdsList.asSequence().map(::AccountId).asIterable(),
messageHashes = deleteMemberContent.messageHashesList,
timestamp = message.sentTimestamp!!,
) )
// storage delete
storage.deleteMessagesByHash(threadId, hashes)
}
} else if (memberIds.isNotEmpty()) {
// Delete all from member Ids, and require admin sig?
verifyAdminSignature(
closedGroup,
adminSig,
messageToValidate
) )
for (member in memberIds) { }.isSuccess
storage.deleteMessagesByUser(threadId, member)
GlobalScope.launch {
try {
MessagingModuleConfiguration.shared.groupManagerV2.handleDeleteMemberContent(
groupId = closedGroup,
deleteMemberContent = deleteMemberContent,
sender = AccountId(message.sender!!),
senderIsVerifiedAdmin = hasValidAdminSignature
)
} catch (e: Exception) {
Log.e("GroupUpdated", "Failed to handle delete member content", e)
} }
} }
} }
@ -649,7 +636,7 @@ private fun handleMemberLeft(message: GroupUpdated, closedGroup: AccountId) {
} }
private fun handleMemberLeftNotification(message: GroupUpdated, closedGroup: AccountId) { private fun handleMemberLeftNotification(message: GroupUpdated, closedGroup: AccountId) {
MessagingModuleConfiguration.shared.storage.handleMemberLeftNotification(message, closedGroup) MessagingModuleConfiguration.shared.storage.insertGroupInfoChange(message, closedGroup)
} }
private fun handleGroupInfoChange(message: GroupUpdated, closedGroup: AccountId) { private fun handleGroupInfoChange(message: GroupUpdated, closedGroup: AccountId) {