From b13b7e647f829d051f4655d3f8d2c852d745de9f Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:00:57 +1000 Subject: [PATCH] Handle message deletion --- .../securesms/database/Storage.kt | 74 ------------ .../securesms/groups/GroupManagerV2Impl.kt | 105 ++++++++++++++++++ .../repository/ConversationRepository.kt | 6 +- .../libsession/database/StorageProtocol.kt | 2 - .../messaging/groups/GroupManagerV2.kt | 11 ++ .../ReceivedMessageHandler.kt | 53 ++++----- 6 files changed, 139 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index edef9c20f2..6520b3c5c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -1339,80 +1339,6 @@ open class Storage( } } - - override fun handleMemberLeftNotification(message: GroupUpdated, closedGroupId: AccountId) { - insertGroupInfoChange(message, closedGroupId) - } - - - override fun sendGroupUpdateDeleteMessage(groupSessionId: String, messageHashes: List): Promise { - 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)[storeIndex] as Map - val hash = results["hash"] as? String - message.serverHash = hash - MessageSender.handleSuccessfulMessageSend(message, groupDestination, false) - } - } - override fun setServerCapabilities(server: String, capabilities: List) { return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index 20ce1349f5..e381db8fed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -1042,6 +1042,111 @@ class GroupManagerV2Impl @Inject constructor( storage.insertGroupInfoChange(message, groupId) } + override suspend fun requestMessageDeletion( + groupId: AccountId, + messageHashes: List + ): 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) { + SnodeAPI.sendBatchRequest( + groupId, SnodeAPI.buildAuthenticatedDeleteBatchInfo(auth, hashes) + ) + } private fun BatchResponse.requireAllRequestsSuccessful(errorMessage: String) { val firstError = this.results.firstOrNull { it.code != 200 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index d7baef07ba..bbcdb88b8b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -242,10 +242,10 @@ class DefaultConversationRepository @Inject constructor( if (recipient.isClosedGroupV2Recipient) { // admin check internally, assume either admin or all belong to user - storage.sendGroupUpdateDeleteMessage( - groupSessionId = recipient.address.serialize(), + groupManager.requestMessageDeletion( + groupId = AccountId(publicKey), messageHashes = listOf(serverHash) - ).await() + ) } else { SnodeAPI.deleteMessage( publicKey = publicKey, diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index c1501d888a..732278b727 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -177,8 +177,6 @@ interface StorageProtocol { fun insertGroupInfoLeaving(closedGroup: AccountId): Long? fun insertGroupInviteControlMessage(sentTimestamp: Long, senderPublicKey: String, closedGroup: AccountId, groupName: String): Long? fun updateGroupInfoChange(messageId: Long, newType: UpdateMessageData.Kind) - fun handleMemberLeftNotification(message: GroupUpdated, closedGroupId: AccountId) - fun sendGroupUpdateDeleteMessage(groupSessionId: String, messageHashes: List): Promise // Groups fun getAllGroups(includeInactive: Boolean): List diff --git a/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt index 8a31021ae9..89b8776fcf 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt @@ -3,6 +3,8 @@ package org.session.libsession.messaging.groups import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.messages.control.GroupUpdated 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 /** @@ -57,4 +59,13 @@ interface GroupManagerV2 { suspend fun handleKicked(groupId: AccountId) suspend fun setName(groupId: AccountId, newName: String) + + suspend fun requestMessageDeletion(groupId: AccountId, messageHashes: List) + + suspend fun handleDeleteMemberContent( + groupId: AccountId, + deleteMemberContent: GroupUpdateDeleteMemberContentMessage, + sender: AccountId, + senderIsVerifiedAdmin: Boolean, + ) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 3082d42d47..e56ab2c3be 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import network.loki.messenger.libsession_util.util.ExpiryMode 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.database.userAuth import org.session.libsession.messaging.MessagingModuleConfiguration @@ -585,45 +586,31 @@ private fun MessageReceiver.handleGroupUpdated(message: GroupUpdated, closedGrou } private fun handleDeleteMemberContent(message: GroupUpdated, closedGroup: AccountId) { - val storage = MessagingModuleConfiguration.shared.storage val deleteMemberContent = message.inner.deleteMemberContent val adminSig = if (deleteMemberContent.hasAdminSignature()) deleteMemberContent.adminSignature.toByteArray()!! else byteArrayOf() - val memberIds = deleteMemberContent.memberSessionIdsList - 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( - closedGroup, - adminSig, - messageToValidate - ) - // storage delete - storage.deleteMessagesByHash(threadId, hashes) - } - } else if (memberIds.isNotEmpty()) { - // Delete all from member Ids, and require admin sig? + val hasValidAdminSignature = adminSig.isNotEmpty() && runCatching { verifyAdminSignature( closedGroup, adminSig, - messageToValidate + buildDeleteMemberContentSignature( + memberIds = deleteMemberContent.memberSessionIdsList.asSequence().map(::AccountId).asIterable(), + messageHashes = deleteMemberContent.messageHashesList, + timestamp = message.sentTimestamp!!, + ) ) - for (member in memberIds) { - storage.deleteMessagesByUser(threadId, member) + }.isSuccess + + 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) { - MessagingModuleConfiguration.shared.storage.handleMemberLeftNotification(message, closedGroup) + MessagingModuleConfiguration.shared.storage.insertGroupInfoChange(message, closedGroup) } private fun handleGroupInfoChange(message: GroupUpdated, closedGroup: AccountId) {