diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 0e3559f01b..3277f9de0d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api import okhttp3.ConnectionSpec import okhttp3.Interceptor import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.metrics.CryptoMetricPlugin import org.matrix.android.sdk.api.metrics.MetricPlugin import java.net.Proxy @@ -80,5 +81,7 @@ data class MatrixConfiguration( /** * Metrics plugin that can be used to capture metrics from matrix-sdk-android. */ - val metricPlugins: List = emptyList() + val metricPlugins: List = emptyList(), + + val cryptoAnalyticsPlugin: CryptoMetricPlugin? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/CryptoMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/CryptoMetricPlugin.kt new file mode 100644 index 0000000000..1c8a6089a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/CryptoMetricPlugin.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.metrics + +import android.util.LruCache +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.crypto.MXCryptoError + +sealed class CryptoEvent { + + data class FailedToDecryptToDevice( + val error: String? + ) : CryptoEvent() + + data class FailedToSendToDevice(val eventTye: String) : CryptoEvent() + + data class UnableToDecryptRoomMessage( + val sessionId: String, + val error: String? + ) : CryptoEvent() + + data class LateDecryptRoomMessage(val sessionId: String, val source: String) : CryptoEvent() +} + +abstract class CryptoMetricPlugin { + + internal sealed class Report { + data class RoomE2EEReport(val error: MXCryptoError.Base, val sessionId: String) : Report() + data class ToDeviceDecryptReport(val error: Throwable) : Report() + data class ToDeviceSendReport(val error: Throwable) : Report() + data class OnRoomKeyImported(val sessionId: String, val source: String) : Report() + } + + // should I scope that to some parent job? + val scope = CoroutineScope(SupervisorJob()) + + private val channel = Channel(capacity = Channel.UNLIMITED) + + // Basic to avoid double reporting for same session and detect late reception + private val uisiCache = LruCache(200) + + init { + scope.launch { + for (ev in channel) { + handleEvent(ev) + } + } + } + + private fun handleEvent(ev: Report) { + when (ev) { + is Report.RoomE2EEReport -> { + if (uisiCache.get(ev.sessionId) == null) { + uisiCache.put(ev.sessionId, Unit) + captureEvent( + CryptoEvent.UnableToDecryptRoomMessage( + sessionId = ev.sessionId, + error = ev.error.errorType.toString() + ) + ) + } + } + is Report.ToDeviceDecryptReport -> { + captureEvent(CryptoEvent.FailedToDecryptToDevice(ev.error.message.toString())) + } + is Report.ToDeviceSendReport -> { + captureEvent(CryptoEvent.FailedToSendToDevice(ev.error.message.orEmpty())) + } + is Report.OnRoomKeyImported -> { + if (uisiCache.get(ev.sessionId) != null) { + // ok we have an uisi for this session + captureEvent( + CryptoEvent.LateDecryptRoomMessage( + sessionId = ev.sessionId, + source = ev.source + ) + ) + } + } + } + } + + fun onFailedToDecryptRoomMessage(error: MXCryptoError.Base, sessionId: String) { + channel.trySend( + Report.RoomE2EEReport(error, sessionId) + ) + } + + fun onFailToSendToDevice(failure: Throwable) { + channel.trySend( + Report.ToDeviceSendReport(failure) + ) + } + fun onFailToDecryptToDevice(failure: Throwable) { + channel.trySend( + Report.ToDeviceDecryptReport(failure) + ) + } + + fun onRoomKeyImported(sessionId: String, source: String) { + channel.trySend( + Report.OnRoomKeyImported(sessionId = sessionId, source = source) + ) + } + + protected abstract fun captureEvent(cryptoEvent: CryptoEvent) +} diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index fa0f5b7cb9..6d6754a357 100644 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP @@ -77,6 +78,7 @@ import org.matrix.rustcomponents.sdk.crypto.MegolmV1BackupKey import org.matrix.rustcomponents.sdk.crypto.Request import org.matrix.rustcomponents.sdk.crypto.RequestType import org.matrix.rustcomponents.sdk.crypto.RoomKeyCounts +import org.matrix.rustcomponents.sdk.crypto.VerificationState import org.matrix.rustcomponents.sdk.crypto.setLogger import timber.log.Timber import java.io.File @@ -121,17 +123,22 @@ internal class OlmMachine @Inject constructor( @SessionFilesDirectory path: File, private val requestSender: RequestSender, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val moshi: Moshi, + baseMoshi: Moshi, private val verificationsProvider: VerificationsProvider, private val deviceFactory: Device.Factory, private val getUserIdentity: GetUserIdentityUseCase, private val ensureUsersKeys: EnsureUsersKeysUseCase, + private val matrixConfiguration: MatrixConfiguration, ) { private val inner: InnerMachine = InnerMachine(userId, deviceId, path.toString(), null) private val flowCollectors = FlowCollectors() + private val moshi = baseMoshi.newBuilder() + .add(CheckNumberType.JSON_ADAPTER_FACTORY) + .build() + /** Get our own user ID. */ fun userId(): String { return inner.userId() @@ -431,18 +438,31 @@ internal class OlmMachine @Inject constructor( senderCurve25519Key = decrypted.senderCurve25519Key, claimedEd25519Key = decrypted.claimedEd25519Key, forwardingCurve25519KeyChain = decrypted.forwardingCurve25519Chain, - // TODO how to get key safety? need to add binding to - // get_verification_state - isSafe = true, + isSafe = decrypted.verificationState == VerificationState.TRUSTED, ) } catch (throwable: Throwable) { - val reason = - String.format( + val reThrow = when (throwable) { + is DecryptionException.Megolm -> { + // TODO more bindings for missing room key + MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, throwable.message.orEmpty()) + } + is DecryptionException.Identifier -> { + MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON) + } + else -> { + val reason = String.format( MXCryptoError.UNABLE_TO_DECRYPT_REASON, throwable.message, "m.megolm.v1.aes-sha2" ) - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) + MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) + } + } + matrixConfiguration.cryptoAnalyticsPlugin?.onFailedToDecryptRoomMessage( + reThrow, + (event.content?.get("session_id") as? String) ?: "" + ) + throw reThrow } } diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt index 81fd0174e7..ebdea2a168 100755 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM @@ -130,6 +131,7 @@ internal class RustCryptoService @Inject constructor( private val encryptEventContent: EncryptEventContentUseCase, private val getRoomUserIds: GetRoomUserIdsUseCase, private val outgoingRequestsProcessor: OutgoingRequestsProcessor, + private val matrixConfiguration: MatrixConfiguration, ) : CryptoService { private val isStarting = AtomicBoolean(false) @@ -586,38 +588,44 @@ internal class RustCryptoService @Inject constructor( val toDeviceEvents = this.olmMachine.receiveSyncChanges(toDevice, deviceChanges, keyCounts) // Notify the our listeners about room keys so decryption is retried. - if (toDeviceEvents.events != null) { - toDeviceEvents.events.forEach { event -> - when (event.type) { - EventType.ROOM_KEY -> { - val content = event.getClearContent().toModel() ?: return@forEach - content.sessionKey - val roomId = content.sessionId ?: return@forEach - val sessionId = content.sessionId + toDeviceEvents.events.orEmpty().forEach { event -> + if (event.getClearType() == EventType.ENCRYPTED) { + // rust failed to decrypt it + matrixConfiguration.cryptoAnalyticsPlugin?.onFailToDecryptToDevice( + Throwable("receiveSyncChanges") + ) + } + when (event.type) { + EventType.ROOM_KEY -> { + val content = event.getClearContent().toModel() ?: return@forEach + content.sessionKey + val roomId = content.sessionId ?: return@forEach + val sessionId = content.sessionId - notifyRoomKeyReceived(roomId, sessionId) - } - EventType.FORWARDED_ROOM_KEY -> { - val content = event.getClearContent().toModel() ?: return@forEach - - val roomId = content.sessionId ?: return@forEach - val sessionId = content.sessionId - - notifyRoomKeyReceived(roomId, sessionId) - } - EventType.SEND_SECRET -> { - // The rust-sdk will clear this event if it's invalid, this will produce an invalid base64 error - // when we try to construct the recovery key. - val secretContent = event.getClearContent().toModel() ?: return@forEach - this.keysBackupService.onSecretKeyGossip(secretContent.secretValue) - } - else -> { - this.verificationService.onEvent(null, event) - } + notifyRoomKeyReceived(roomId, sessionId) + matrixConfiguration.cryptoAnalyticsPlugin?.onRoomKeyImported(sessionId, EventType.FORWARDED_ROOM_KEY) } + EventType.FORWARDED_ROOM_KEY -> { + val content = event.getClearContent().toModel() ?: return@forEach + + val roomId = content.sessionId ?: return@forEach + val sessionId = content.sessionId + + notifyRoomKeyReceived(roomId, sessionId) + matrixConfiguration.cryptoAnalyticsPlugin?.onRoomKeyImported(sessionId, EventType.FORWARDED_ROOM_KEY) + } + EventType.SEND_SECRET -> { + // The rust-sdk will clear this event if it's invalid, this will produce an invalid base64 error + // when we try to construct the recovery key. + val secretContent = event.getClearContent().toModel() ?: return@forEach + this.keysBackupService.onSecretKeyGossip(secretContent.secretValue) + } + else -> { + this.verificationService.onEvent(null, event) + } + } liveEventManager.get().dispatchOnLiveToDevice(event) } - } } /** diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt index 2bfc5e3e26..5f59fad6a3 100644 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/OutgoingRequestsProcessor.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.crypto.ComputeShieldForGroupUseCase @@ -41,7 +42,8 @@ internal class OutgoingRequestsProcessor @Inject constructor( private val requestSender: RequestSender, private val coroutineScope: CoroutineScope, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, - private val computeShieldForGroup: ComputeShieldForGroupUseCase + private val computeShieldForGroup: ComputeShieldForGroupUseCase, + private val matrixConfiguration: MatrixConfiguration, ) { private val lock: Mutex = Mutex() @@ -156,6 +158,7 @@ internal class OutgoingRequestsProcessor @Inject constructor( true } catch (throwable: Throwable) { Timber.tag(loggerTag.value).e(throwable, "## sendToDevice(): error") + matrixConfiguration.cryptoAnalyticsPlugin?.onFailToSendToDevice(throwable) false } } diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt index fc3168fbf3..921c661c4a 100644 --- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -151,6 +151,7 @@ import javax.inject.Singleton flipperProxy.networkInterceptor(), ), metricPlugins = vectorPlugins.plugins(), + cryptoAnalyticsPlugin = vectorPlugins.cryptoMetricPlugin, ) } diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt index 4278c1011b..9fc08a73ec 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/VectorPlugins.kt @@ -16,6 +16,7 @@ package im.vector.app.features.analytics.metrics +import im.vector.app.features.analytics.metrics.sentry.SentryCryptoAnalytics import im.vector.app.features.analytics.metrics.sentry.SentryDownloadDeviceKeysMetrics import im.vector.app.features.analytics.metrics.sentry.SentrySyncDurationMetrics import org.matrix.android.sdk.api.metrics.MetricPlugin @@ -29,6 +30,7 @@ import javax.inject.Singleton data class VectorPlugins @Inject constructor( val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics, val sentrySyncDurationMetrics: SentrySyncDurationMetrics, + val cryptoMetricPlugin: SentryCryptoAnalytics ) { /** * Returns [List] of all [MetricPlugin] hold by this class. diff --git a/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryCryptoAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryCryptoAnalytics.kt new file mode 100644 index 0000000000..2fcbc5fc56 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/metrics/sentry/SentryCryptoAnalytics.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.metrics.sentry + +import im.vector.app.BuildConfig +import io.sentry.Sentry +import io.sentry.SentryEvent +import io.sentry.protocol.Message +import org.matrix.android.sdk.api.metrics.CryptoEvent +import org.matrix.android.sdk.api.metrics.CryptoMetricPlugin +import javax.inject.Inject + +class SentryCryptoAnalytics @Inject constructor() : CryptoMetricPlugin() { + + override fun captureEvent(cryptoEvent: CryptoEvent) { + if (!Sentry.isEnabled()) return + val event = SentryEvent() + event.setTag("e2eFlavor", BuildConfig.FLAVOR) + event.setTag("e2eType", "crypto") + when (cryptoEvent) { + is CryptoEvent.FailedToDecryptToDevice -> { + event.message = Message().apply { message = "FailedToDecryptToDevice" } + event.setExtra("e2eOlmError", cryptoEvent.error ?: "Unknown") + } + is CryptoEvent.FailedToSendToDevice -> { + event.message = Message().apply { message = "FailedToSendToDevice" } + event.setExtra("e2eEventType", cryptoEvent.eventTye) + } + is CryptoEvent.LateDecryptRoomMessage -> { + event.message = Message().apply { message = "LateDecryptRoomMessage" } + event.setTag("e2eSource", cryptoEvent.source) + event.setExtra("e2eSessionId", cryptoEvent.sessionId) + } + is CryptoEvent.UnableToDecryptRoomMessage -> { + event.message = Message().apply { message = "UnableToDecryptRoomMessage" } + event.setExtra("e2eSessionId", cryptoEvent.sessionId) + event.setTag("e2eMegolmError", cryptoEvent.error.orEmpty()) + } + } + Sentry.captureEvent(event) + } +}