From 9263578dc2102c3fb3f00660376360c6a7abe738 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Wed, 4 Sep 2024 13:12:18 +0200 Subject: [PATCH 1/3] fix(proteus): prevent missing messages by using transactions Some clients reported missing messages. We found that it is not that hard to kill the app by swiping it away between decryption and DB insertion. CoreCrypto doesn't support transactions yet. So we're only tackling CryptoBox at the moment, but the API changes are adapting CoreCrypto for the future as well. By not saving the session before inserting the messages into the DB, we can try to process this event again and recover this message. --- .../ProteusClientCryptoBoxImpl.kt | 16 ++-- .../ProteusClientCoreCryptoImpl.kt | 22 ++--- .../ProteusClientCoreCryptoImpl.kt | 9 ++- .../wire/kalium/cryptography/ProteusClient.kt | 12 ++- .../kalium/cryptography/ProteusClientTest.kt | 80 +++++++++++++------ .../ProteusClientCryptoBoxImpl.kt | 9 ++- .../ProteusClientCryptoBoxImpl.kt | 20 ++--- .../message/NewMessageEventHandler.kt | 67 ++++++++-------- .../message/ProteusMessageUnpacker.kt | 72 ++++++++++------- .../message/NewMessageEventHandlerTest.kt | 38 +++++---- .../message/ProteusMessageUnpackerTest.kt | 14 ++-- 11 files changed, 221 insertions(+), 138 deletions(-) diff --git a/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt b/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt index 4414b91a749..8c496497808 100644 --- a/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt +++ b/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt @@ -110,18 +110,24 @@ class ProteusClientCryptoBoxImpl constructor( } } - override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray = lock.withLock { + override suspend fun decrypt( + message: ByteArray, + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T = lock.withLock { withContext(defaultContext) { val session = box.tryGetSession(sessionId.value) wrapException { if (session != null) { val decryptedMessage = session.decrypt(message) - session.save() - decryptedMessage + handleDecryptedMessage(decryptedMessage).also { + session.save() + } } else { val result = box.initSessionFromMessage(sessionId.value, message) - result.session.save() - result.message + handleDecryptedMessage(result.message).also { + result.session.save() + } } } } diff --git a/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt b/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt index 0d9cf4fa155..e417ee59341 100644 --- a/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt +++ b/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt @@ -31,7 +31,8 @@ import platform.Foundation.URLByAppendingPathComponent @Suppress("TooManyFunctions") class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: CoreCrypto) : ProteusClient { @Suppress("EmptyFunctionBlock") - override suspend fun close() {} + override suspend fun close() { + } override fun getIdentity(): ByteArray { return ByteArray(0) @@ -72,18 +73,21 @@ class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: Co wrapException { coreCrypto.proteusSessionFromPrekey(sessionId.value, toUByteList(preKeyCrypto.encodedData.decodeBase64Bytes())) } } - override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray { + override suspend fun decrypt( + message: ByteArray, + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T { val sessionExists = doesSessionExist(sessionId) return wrapException { - if (sessionExists) { - val decryptedMessage = toByteArray(coreCrypto.proteusDecrypt(sessionId.value, toUByteList(message))) - coreCrypto.proteusSessionSave(sessionId.value) - decryptedMessage + val decryptedMessage = if (sessionExists) { + toByteArray(coreCrypto.proteusDecrypt(sessionId.value, toUByteList(message))) } else { - val decryptedMessage = toByteArray(coreCrypto.proteusSessionFromMessage(sessionId.value, toUByteList(message))) + toByteArray(coreCrypto.proteusSessionFromMessage(sessionId.value, toUByteList(message))) + } + handleDecryptedMessage(decryptedMessage).also { coreCrypto.proteusSessionSave(sessionId.value) - decryptedMessage } } } @@ -129,7 +133,7 @@ class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: Co } @Suppress("TooGenericExceptionCaught") - private fun wrapException(b: () -> T): T { + private inline fun wrapException(b: () -> T): T { try { return b() } catch (e: CryptoException) { diff --git a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt index 9a373797466..506e7ab2233 100644 --- a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt +++ b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt @@ -85,15 +85,20 @@ class ProteusClientCoreCryptoImpl private constructor( wrapException { coreCrypto.proteusSessionFromPrekey(sessionId.value, preKeyCrypto.encodedData.decodeBase64Bytes()) } } - override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray { + override suspend fun decrypt( + message: ByteArray, + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T { val sessionExists = doesSessionExist(sessionId) return wrapException { - if (sessionExists) { + val decryptedMessage = if (sessionExists) { coreCrypto.proteusDecrypt(sessionId.value, message) } else { coreCrypto.proteusSessionFromMessage(sessionId.value, message) } + handleDecryptedMessage(decryptedMessage) } } diff --git a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt index e57fb8c4fe2..055ecc27841 100644 --- a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt +++ b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt @@ -81,8 +81,18 @@ interface ProteusClient { @Throws(ProteusException::class, CancellationException::class) suspend fun createSession(preKeyCrypto: PreKeyCrypto, sessionId: CryptoSessionId) + /** + * Decrypts a message. + * In case of success, calls [handleDecryptedMessage] with the decrypted bytes. + * @throws ProteusException in case of failure + * @throws CancellationException + */ @Throws(ProteusException::class, CancellationException::class) - suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray + suspend fun decrypt( + message: ByteArray, + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T @Throws(ProteusException::class, CancellationException::class) suspend fun encrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray diff --git a/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt b/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt index ec4f95be564..61c363b94d3 100644 --- a/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt +++ b/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt @@ -1,20 +1,20 @@ - /* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ +/* +* Wire +* Copyright (C) 2024 Wire Swiss GmbH +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see http://www.gnu.org/licenses/. +*/ package com.wire.kalium.cryptography @@ -93,7 +93,7 @@ class ProteusClientTest : BaseProteusClientTest() { val message = "Hi Alice!" val aliceKey = aliceClient.newPreKeys(0, 10).first() val encryptedMessage = bobClient.encryptWithPreKey(message.encodeToByteArray(), aliceKey, aliceSessionId) - val decryptedMessage = aliceClient.decrypt(encryptedMessage, bobSessionId) + val decryptedMessage = aliceClient.decrypt(encryptedMessage, bobSessionId) { it } assertEquals(message, decryptedMessage.decodeToString()) } @@ -105,11 +105,11 @@ class ProteusClientTest : BaseProteusClientTest() { val aliceKey = aliceClient.newPreKeys(0, 10).first() val message1 = "Hi Alice!" val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId) - aliceClient.decrypt(encryptedMessage1, bobSessionId) + aliceClient.decrypt(encryptedMessage1, bobSessionId) {} val message2 = "Hi again Alice!" val encryptedMessage2 = bobClient.encrypt(message2.encodeToByteArray(), aliceSessionId) - val decryptedMessage2 = aliceClient.decrypt(encryptedMessage2, bobSessionId) + val decryptedMessage2 = aliceClient.decrypt(encryptedMessage2, bobSessionId) { it } assertEquals(message2, decryptedMessage2.decodeToString()) } @@ -124,10 +124,10 @@ class ProteusClientTest : BaseProteusClientTest() { val aliceKey = aliceClient.newPreKeys(0, 10).first() val message1 = "Hi Alice!" val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId) - aliceClient.decrypt(encryptedMessage1, bobSessionId) + aliceClient.decrypt(encryptedMessage1, bobSessionId) {} val exception: ProteusException = assertFailsWith { - aliceClient.decrypt(encryptedMessage1, bobSessionId) + aliceClient.decrypt(encryptedMessage1, bobSessionId) {} } assertEquals(ProteusException.Code.DUPLICATE_MESSAGE, exception.code) } @@ -188,8 +188,42 @@ class ProteusClientTest : BaseProteusClientTest() { } } + // TODO: Implement on CoreCrypto as well once it supports transactions + @IgnoreJS + @Test + fun givenNonEncryptedClient_whenThrowingDuringTransaction_thenShouldNotSaveSessionAndBeAbleToDecryptAgain() = runTest { + val aliceRef = createProteusStoreRef(alice.id) + val failedAliceClient = createProteusClient(aliceRef) + val bobClient = createProteusClient(createProteusStoreRef(bob.id)) + + val aliceKey = failedAliceClient.newPreKeys(0, 10).first() + val message1 = "Hi Alice!" + + var decryptedCount = 0 + + val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId) + try { + failedAliceClient.decrypt(encryptedMessage1, bobSessionId) { + decryptedCount++ + throw NullPointerException("") + } + } catch (ignore: Throwable) { + /** No-op **/ + } + // Assume that the app crashed after decrypting but before saving session. + // Trying to decrypt again should succeed. + + val secondAliceClient = createProteusClient(aliceRef) + + val result = secondAliceClient.decrypt(encryptedMessage1, bobSessionId) { result -> + decryptedCount++ + result + } + assertEquals(message1, result.decodeToString()) + assertEquals(2, decryptedCount) + } + companion object { val PROTEUS_DB_SECRET = ProteusDBSecret("secret") } - } diff --git a/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt b/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt index f07272f107e..d88a8078e23 100644 --- a/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt +++ b/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt @@ -102,12 +102,13 @@ class ProteusClientCryptoBoxImpl : ProteusClient { box.session_from_prekey(sessionId.value, preKeyBundle.toArrayBuffer()).await() } - override suspend fun decrypt( + override suspend fun decrypt( message: ByteArray, - sessionId: CryptoSessionId - ): ByteArray { + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T { val decryptedMessage = box.decrypt(sessionId.value, message.toArrayBuffer()).await() - return Int8Array(decryptedMessage.buffer).unsafeCast() + return handleDecryptedMessage(Int8Array(decryptedMessage.buffer).unsafeCast()) } override suspend fun encrypt( diff --git a/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt b/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt index 33dbbde607a..39c69e50611 100644 --- a/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt +++ b/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt @@ -27,17 +27,13 @@ import java.util.Base64 import kotlin.coroutines.CoroutineContext @Suppress("TooManyFunctions") -class ProteusClientCryptoBoxImpl constructor( +class ProteusClientCryptoBoxImpl( rootDir: String ) : ProteusClient { - private val path: String + private val path: String = rootDir private lateinit var box: CryptoBox - init { - path = rootDir - } - fun openOrCreate() { val directory = File(path) box = wrapException { @@ -84,14 +80,18 @@ class ProteusClientCryptoBoxImpl constructor( wrapException { box.encryptFromPreKeys(sessionId.value, toPreKey(preKeyCrypto), ByteArray(0)) } } - override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray { - return wrapException { box.decrypt(sessionId.value, message) } + override suspend fun decrypt( + message: ByteArray, + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T = wrapException { + handleDecryptedMessage(box.decrypt(sessionId.value, message)) } override suspend fun encrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray { return wrapException { box.encryptFromSession(sessionId.value, message) - }?.let { it } ?: throw ProteusException(null, ProteusException.Code.SESSION_NOT_FOUND) + } ?: throw ProteusException(null, ProteusException.Code.SESSION_NOT_FOUND) } override suspend fun encryptBatched(message: ByteArray, sessionIds: List): Map { @@ -121,7 +121,7 @@ class ProteusClientCryptoBoxImpl constructor( } @Suppress("TooGenericExceptionCaught") - private fun wrapException(b: () -> T): T { + private inline fun wrapException(b: () -> T): T { try { return b() } catch (e: CryptoException) { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt index ff54b70d4b1..d184eabd271 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt @@ -59,44 +59,43 @@ internal class NewMessageEventHandlerImpl( override suspend fun handleNewProteusMessage(event: Event.Conversation.NewMessage, deliveryInfo: EventDeliveryInfo) { val eventLogger = logger.createEventProcessingLogger(event) - proteusMessageUnpacker.unpackProteusMessage(event) - .onFailure { - val logMap = mapOf( - "event" to event.toLogMap(), - "errorInfo" to "$it", - "protocol" to "Proteus" - ) - - if (it is ProteusFailure && it.proteusException.code == ProteusException.Code.DUPLICATE_MESSAGE) { - logger.i("Ignoring duplicate event: ${logMap.toJsonElement()}") - return - } + proteusMessageUnpacker.unpackProteusMessage(event) { + processApplicationMessage(it, deliveryInfo) + it + }.onSuccess { + eventLogger.logSuccess( + "protocol" to "Proteus", + "messageType" to it.messageTypeDescription, + ) + }.onFailure { + val logMap = mapOf( + "event" to event.toLogMap(), + "errorInfo" to "$it", + "protocol" to "Proteus" + ) - logger.e("Failed to decrypt event: ${logMap.toJsonElement()}") + if (it is ProteusFailure && it.proteusException.code == ProteusException.Code.DUPLICATE_MESSAGE) { + logger.i("Ignoring duplicate event: ${logMap.toJsonElement()}") + return + } - applicationMessageHandler.handleDecryptionError( - eventId = event.id, - conversationId = event.conversationId, - messageInstant = event.messageInstant, + logger.e("Failed to decrypt event: ${logMap.toJsonElement()}") + + applicationMessageHandler.handleDecryptionError( + eventId = event.id, + conversationId = event.conversationId, + messageInstant = event.messageInstant, + senderUserId = event.senderUserId, + senderClientId = event.senderClientId, + content = MessageContent.FailedDecryption( + encodedData = event.encryptedExternalContent?.data, + isDecryptionResolved = false, senderUserId = event.senderUserId, - senderClientId = event.senderClientId, - content = MessageContent.FailedDecryption( - encodedData = event.encryptedExternalContent?.data, - isDecryptionResolved = false, - senderUserId = event.senderUserId, - clientId = ClientId(event.senderClientId.value) - ) - ) - eventLogger.logFailure(it, "protocol" to "Proteus") - }.onSuccess { - if (it is MessageUnpackResult.ApplicationMessage) { - processApplicationMessage(it, deliveryInfo) - } - eventLogger.logSuccess( - "protocol" to "Proteus", - "messageType" to it.messageTypeDescription, + clientId = ClientId(event.senderClientId.value) ) - } + ) + eventLogger.logFailure(it, "protocol" to "Proteus") + } } override suspend fun handleNewMLSMessage(event: Event.Conversation.NewMLSMessage, deliveryInfo: EventDeliveryInfo) { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpacker.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpacker.kt index 7d039df3d23..9e984e1835c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpacker.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpacker.kt @@ -46,7 +46,10 @@ import io.ktor.utils.io.core.toByteArray internal interface ProteusMessageUnpacker { - suspend fun unpackProteusMessage(event: Event.Conversation.NewMessage): Either + suspend fun unpackProteusMessage( + event: Event.Conversation.NewMessage, + handleMessage: suspend (applicationMessage: MessageUnpackResult.ApplicationMessage) -> T + ): Either } @@ -59,7 +62,10 @@ internal class ProteusMessageUnpackerImpl( private val logger get() = kaliumLogger.withFeatureId(KaliumLogger.Companion.ApplicationFlow.EVENT_RECEIVER) - override suspend fun unpackProteusMessage(event: Event.Conversation.NewMessage): Either { + override suspend fun unpackProteusMessage( + event: Event.Conversation.NewMessage, + handleMessage: suspend (applicationMessage: MessageUnpackResult.ApplicationMessage) -> T + ): Either { val decodedContentBytes = Base64.decodeFromBase64(event.content.toByteArray()) val cryptoSessionId = CryptoSessionId( idMapper.toCryptoQualifiedIDId(event.senderUserId), @@ -68,37 +74,45 @@ internal class ProteusMessageUnpackerImpl( return proteusClientProvider.getOrError() .flatMap { wrapProteusRequest { - it.decrypt(decodedContentBytes, cryptoSessionId) + it.decrypt(decodedContentBytes, cryptoSessionId) { + val plainMessageBlob = PlainMessageBlob(it) + getReadableMessageContent(plainMessageBlob, event.encryptedExternalContent).map { readableContent -> + val appMessage = MessageUnpackResult.ApplicationMessage( + conversationId = event.conversationId, + instant = event.messageInstant, + senderUserId = event.senderUserId, + senderClientId = event.senderClientId, + content = readableContent + ) + handleMessage(appMessage) + } + } } - } - .map { PlainMessageBlob(it) } - .flatMap { plainMessageBlob -> getReadableMessageContent(plainMessageBlob, event.encryptedExternalContent) } - .onFailure { - when (it) { - is CoreFailure.Unknown -> logger.e("UnknownFailure when processing message: $it", it.rootCause) + }.flatMap { it } + .onFailure { logUnpackingError(it, event, cryptoSessionId) } + } - is ProteusFailure -> { - val loggableException = - "{ \"code\": \"${it.proteusException.code.name}\", \"message\": \"${it.proteusException.message}\", " + - "\"error\": \"${it.proteusException.stackTraceToString()}\"," + - "\"senderClientId\": \"${event.senderClientId.value.obfuscateId()}\"," + - "\"senderUserId\": \"${event.senderUserId.value.obfuscateId()}\"," + - "\"cryptoClientId\": \"${cryptoSessionId.cryptoClientId.value.obfuscateId()}\"," + - "\"cryptoUserId\": \"${cryptoSessionId.userId.value.obfuscateId()}\"}" - logger.e("ProteusFailure when processing message detail: $loggableException") - } + private fun logUnpackingError( + it: CoreFailure, + event: Event.Conversation.NewMessage, + cryptoSessionId: CryptoSessionId + ) { + when (it) { + is CoreFailure.Unknown -> logger.e("UnknownFailure when processing message: $it", it.rootCause) - else -> logger.e("Failure when processing message: $it") - } - }.map { readableContent -> - MessageUnpackResult.ApplicationMessage( - conversationId = event.conversationId, - instant = event.messageInstant, - senderUserId = event.senderUserId, - senderClientId = event.senderClientId, - content = readableContent - ) + is ProteusFailure -> { + val loggableException = + "{ \"code\": \"${it.proteusException.code.name}\", \"message\": \"${it.proteusException.message}\", " + + "\"error\": \"${it.proteusException.stackTraceToString()}\"," + + "\"senderClientId\": \"${event.senderClientId.value.obfuscateId()}\"," + + "\"senderUserId\": \"${event.senderUserId.value.obfuscateId()}\"," + + "\"cryptoClientId\": \"${cryptoSessionId.cryptoClientId.value.obfuscateId()}\"," + + "\"cryptoUserId\": \"${cryptoSessionId.userId.value.obfuscateId()}\"}" + logger.e("ProteusFailure when processing message detail: $loggableException") } + + else -> logger.e("Failure when processing message: $it") + } } private fun getReadableMessageContent( diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt index 50009a3f18c..fdeda8041b5 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt @@ -35,9 +35,9 @@ import com.wire.kalium.logic.feature.message.ephemeral.EphemeralMessageDeletionH import com.wire.kalium.logic.framework.TestEvent import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.isRight import com.wire.kalium.logic.sync.receiver.handler.legalhold.LegalHoldHandler import com.wire.kalium.util.DateTimeUtil -import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString import io.mockative.Mock import io.mockative.any import io.mockative.coEvery @@ -48,7 +48,6 @@ import io.mockative.once import io.mockative.verify import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant -import kotlinx.datetime.toInstant import kotlin.test.Test class NewMessageEventHandlerTest { @@ -56,7 +55,7 @@ class NewMessageEventHandlerTest { @Test fun givenProteusEvent_whenHandling_shouldAskProteusUnpackerToDecrypt() = runTest { val (arrangement, newMessageEventHandler) = Arrangement() - .withProteusUnpackerReturning(Either.Right(MessageUnpackResult.HandshakeMessage)) + .withProteusUnpackerReturning(Either.Left(CoreFailure.InvalidEventSenderID)) .arrange() val newMessageEvent = TestEvent.newMessageEvent("encryptedContent") @@ -64,7 +63,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) } @@ -88,7 +87,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -116,7 +115,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -210,7 +209,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -234,7 +233,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -255,7 +254,7 @@ class NewMessageEventHandlerTest { val newMessageEvent = TestEvent.newMessageEvent("encryptedContent") newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) - coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) }.wasInvoked(exactly = once) + coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { arrangement.applicationMessageHandler.handleDecryptionError(any(), any(), any(), any(), any(), any()) }.wasNotInvoked() coVerify { arrangement.confirmationDeliveryHandler.enqueueConfirmationDelivery(any(), any()) }.wasNotInvoked() } @@ -270,7 +269,7 @@ class NewMessageEventHandlerTest { val newMessageEvent = TestEvent.newMessageEvent("encryptedContent") newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) - coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) }.wasInvoked(exactly = once) + coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { arrangement.applicationMessageHandler.handleDecryptionError(any(), any(), any(), any(), any(), any()) }.wasNotInvoked() coVerify { arrangement.confirmationDeliveryHandler.enqueueConfirmationDelivery(any(), any()) }.wasNotInvoked() } @@ -284,7 +283,7 @@ class NewMessageEventHandlerTest { val newMessageEvent = TestEvent.newMessageEvent("encryptedContent") newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) - coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) }.wasInvoked(exactly = once) + coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { arrangement.applicationMessageHandler.handleDecryptionError(any(), any(), any(), any(), any(), any()) }.wasNotInvoked() coVerify { arrangement.confirmationDeliveryHandler.enqueueConfirmationDelivery(any(), any()) }.wasInvoked(exactly = once) } @@ -323,7 +322,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -343,7 +342,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -433,10 +432,17 @@ class NewMessageEventHandlerTest { staleEpochVerifier ) - suspend fun withProteusUnpackerReturning(result: Either) = apply { + suspend fun withProteusUnpackerReturning(result: Either) = apply { coEvery { - proteusMessageUnpacker.unpackProteusMessage(any()) - }.returns(result) + proteusMessageUnpacker.unpackProteusMessage(any(), any()) + }.invokes { args -> + if (result is Either.Right) { + val lambda = args[1] as suspend (MessageUnpackResult.ApplicationMessage) -> MessageUnpackResult.ApplicationMessage + Either.Right(lambda(result.value)) + } else { + result + } + } } suspend fun withHandleLegalHoldSuccess() = apply { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpackerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpackerTest.kt index a00d99ac133..f5e3df2aa25 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpackerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpackerTest.kt @@ -25,6 +25,7 @@ import com.wire.kalium.cryptography.ProteusClient import com.wire.kalium.cryptography.utils.PlainData import com.wire.kalium.cryptography.utils.encryptDataWithAES256 import com.wire.kalium.cryptography.utils.generateRandomAES256Key +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.client.ProteusClientProvider import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.message.MessageContent @@ -76,7 +77,7 @@ class ProteusMessageUnpackerTest { val encodedEncryptedContent = Base64.encodeToBase64("Hello".encodeToByteArray()) val messageEvent = TestEvent.newMessageEvent(encodedEncryptedContent.decodeToString()) - proteusUnpacker.unpackProteusMessage(messageEvent) + proteusUnpacker.unpackProteusMessage(messageEvent) { } val cryptoSessionId = CryptoSessionId( CryptoUserID(messageEvent.senderUserId.value, messageEvent.senderUserId.domain), @@ -85,7 +86,7 @@ class ProteusMessageUnpackerTest { val decodedByteArray = Base64.decodeFromBase64(messageEvent.content.toByteArray()) coVerify { - arrangement.proteusClient.decrypt(matches { it.contentEquals(decodedByteArray) }, eq(cryptoSessionId)) + arrangement.proteusClient.decrypt(matches { it.contentEquals(decodedByteArray) }, eq(cryptoSessionId), any()) }.wasInvoked(exactly = once) } @@ -130,7 +131,7 @@ class ProteusMessageUnpackerTest { encryptedExternalContent = encryptedProtobufExternalContent ) - val result = proteusUnpacker.unpackProteusMessage(messageEvent) + val result = proteusUnpacker.unpackProteusMessage(messageEvent) { it } result.shouldSucceed { assertIs(it) @@ -152,8 +153,11 @@ class ProteusMessageUnpackerTest { suspend fun withProteusClientDecryptingByteArray(decryptedData: ByteArray) = apply { coEvery { - proteusClient.decrypt(any(), any()) - }.returns(decryptedData) + proteusClient.decrypt>(any(), any(), any()) + }.invokes { args -> + val lambda = args[2] as suspend (ByteArray) -> Either<*, *> + lambda.invoke(decryptedData) + } } fun withProtoContentMapperReturning(plainBlobMatcher: Matcher, protoContent: ProtoContent) = apply { From d71b0af06f59b9d95c1996041ff8359df8e1ce09 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Wed, 4 Sep 2024 13:26:38 +0200 Subject: [PATCH 2/3] test: disable iOS and JS as they don't have transaction support JS Cryptobox doesn't have it. iOS uses CoreCrypto --- .../kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt b/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt index 61c363b94d3..e6228559716 100644 --- a/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt +++ b/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt @@ -190,6 +190,7 @@ class ProteusClientTest : BaseProteusClientTest() { // TODO: Implement on CoreCrypto as well once it supports transactions @IgnoreJS + @IgnoreIOS @Test fun givenNonEncryptedClient_whenThrowingDuringTransaction_thenShouldNotSaveSessionAndBeAbleToDecryptAgain() = runTest { val aliceRef = createProteusStoreRef(alice.id) From e6a5adbee6eedcae71b51932a895f665fd6ac579 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Wed, 4 Sep 2024 14:07:00 +0200 Subject: [PATCH 3/3] test: disable JVM It seems that CryptoBox has static data across instances on JVM, so it can't be tested there either --- .../kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt b/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt index e6228559716..a27624f024a 100644 --- a/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt +++ b/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt @@ -190,6 +190,7 @@ class ProteusClientTest : BaseProteusClientTest() { // TODO: Implement on CoreCrypto as well once it supports transactions @IgnoreJS + @IgnoreJvm @IgnoreIOS @Test fun givenNonEncryptedClient_whenThrowingDuringTransaction_thenShouldNotSaveSessionAndBeAbleToDecryptAgain() = runTest {