Basic sentry e2e reporting for rust + decrypt trust

This commit is contained in:
valere 2022-12-02 18:24:23 +01:00
parent 2ae4b87f2f
commit ae9711b7d1
8 changed files with 254 additions and 37 deletions

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.api
import okhttp3.ConnectionSpec import okhttp3.ConnectionSpec
import okhttp3.Interceptor import okhttp3.Interceptor
import org.matrix.android.sdk.api.crypto.MXCryptoConfig 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 org.matrix.android.sdk.api.metrics.MetricPlugin
import java.net.Proxy import java.net.Proxy
@ -80,5 +81,7 @@ data class MatrixConfiguration(
/** /**
* Metrics plugin that can be used to capture metrics from matrix-sdk-android. * Metrics plugin that can be used to capture metrics from matrix-sdk-android.
*/ */
val metricPlugins: List<MetricPlugin> = emptyList() val metricPlugins: List<MetricPlugin> = emptyList(),
val cryptoAnalyticsPlugin: CryptoMetricPlugin? = null
) )

View file

@ -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<Report>(capacity = Channel.UNLIMITED)
// Basic to avoid double reporting for same session and detect late reception
private val uisiCache = LruCache<String, Unit>(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)
}

View file

@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP 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.Request
import org.matrix.rustcomponents.sdk.crypto.RequestType import org.matrix.rustcomponents.sdk.crypto.RequestType
import org.matrix.rustcomponents.sdk.crypto.RoomKeyCounts import org.matrix.rustcomponents.sdk.crypto.RoomKeyCounts
import org.matrix.rustcomponents.sdk.crypto.VerificationState
import org.matrix.rustcomponents.sdk.crypto.setLogger import org.matrix.rustcomponents.sdk.crypto.setLogger
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -121,17 +123,22 @@ internal class OlmMachine @Inject constructor(
@SessionFilesDirectory path: File, @SessionFilesDirectory path: File,
private val requestSender: RequestSender, private val requestSender: RequestSender,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val moshi: Moshi, baseMoshi: Moshi,
private val verificationsProvider: VerificationsProvider, private val verificationsProvider: VerificationsProvider,
private val deviceFactory: Device.Factory, private val deviceFactory: Device.Factory,
private val getUserIdentity: GetUserIdentityUseCase, private val getUserIdentity: GetUserIdentityUseCase,
private val ensureUsersKeys: EnsureUsersKeysUseCase, private val ensureUsersKeys: EnsureUsersKeysUseCase,
private val matrixConfiguration: MatrixConfiguration,
) { ) {
private val inner: InnerMachine = InnerMachine(userId, deviceId, path.toString(), null) private val inner: InnerMachine = InnerMachine(userId, deviceId, path.toString(), null)
private val flowCollectors = FlowCollectors() private val flowCollectors = FlowCollectors()
private val moshi = baseMoshi.newBuilder()
.add(CheckNumberType.JSON_ADAPTER_FACTORY)
.build()
/** Get our own user ID. */ /** Get our own user ID. */
fun userId(): String { fun userId(): String {
return inner.userId() return inner.userId()
@ -431,18 +438,31 @@ internal class OlmMachine @Inject constructor(
senderCurve25519Key = decrypted.senderCurve25519Key, senderCurve25519Key = decrypted.senderCurve25519Key,
claimedEd25519Key = decrypted.claimedEd25519Key, claimedEd25519Key = decrypted.claimedEd25519Key,
forwardingCurve25519KeyChain = decrypted.forwardingCurve25519Chain, forwardingCurve25519KeyChain = decrypted.forwardingCurve25519Chain,
// TODO how to get key safety? need to add binding to isSafe = decrypted.verificationState == VerificationState.TRUSTED,
// get_verification_state
isSafe = true,
) )
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
val reason = val reThrow = when (throwable) {
String.format( 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, MXCryptoError.UNABLE_TO_DECRYPT_REASON,
throwable.message, throwable.message,
"m.megolm.v1.aes-sha2" "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
} }
} }

View file

@ -28,6 +28,7 @@ import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM 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 encryptEventContent: EncryptEventContentUseCase,
private val getRoomUserIds: GetRoomUserIdsUseCase, private val getRoomUserIds: GetRoomUserIdsUseCase,
private val outgoingRequestsProcessor: OutgoingRequestsProcessor, private val outgoingRequestsProcessor: OutgoingRequestsProcessor,
private val matrixConfiguration: MatrixConfiguration,
) : CryptoService { ) : CryptoService {
private val isStarting = AtomicBoolean(false) private val isStarting = AtomicBoolean(false)
@ -586,38 +588,44 @@ internal class RustCryptoService @Inject constructor(
val toDeviceEvents = this.olmMachine.receiveSyncChanges(toDevice, deviceChanges, keyCounts) val toDeviceEvents = this.olmMachine.receiveSyncChanges(toDevice, deviceChanges, keyCounts)
// Notify the our listeners about room keys so decryption is retried. // Notify the our listeners about room keys so decryption is retried.
if (toDeviceEvents.events != null) { toDeviceEvents.events.orEmpty().forEach { event ->
toDeviceEvents.events.forEach { event -> if (event.getClearType() == EventType.ENCRYPTED) {
when (event.type) { // rust failed to decrypt it
EventType.ROOM_KEY -> { matrixConfiguration.cryptoAnalyticsPlugin?.onFailToDecryptToDevice(
val content = event.getClearContent().toModel<RoomKeyContent>() ?: return@forEach Throwable("receiveSyncChanges")
content.sessionKey )
val roomId = content.sessionId ?: return@forEach }
val sessionId = content.sessionId when (event.type) {
EventType.ROOM_KEY -> {
val content = event.getClearContent().toModel<RoomKeyContent>() ?: return@forEach
content.sessionKey
val roomId = content.sessionId ?: return@forEach
val sessionId = content.sessionId
notifyRoomKeyReceived(roomId, sessionId) notifyRoomKeyReceived(roomId, sessionId)
} matrixConfiguration.cryptoAnalyticsPlugin?.onRoomKeyImported(sessionId, EventType.FORWARDED_ROOM_KEY)
EventType.FORWARDED_ROOM_KEY -> {
val content = event.getClearContent().toModel<ForwardedRoomKeyContent>() ?: 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<SecretSendEventContent>() ?: return@forEach
this.keysBackupService.onSecretKeyGossip(secretContent.secretValue)
}
else -> {
this.verificationService.onEvent(null, event)
}
} }
EventType.FORWARDED_ROOM_KEY -> {
val content = event.getClearContent().toModel<ForwardedRoomKeyContent>() ?: 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<SecretSendEventContent>() ?: return@forEach
this.keysBackupService.onSecretKeyGossip(secretContent.secretValue)
}
else -> {
this.verificationService.onEvent(null, event)
}
}
liveEventManager.get().dispatchOnLiveToDevice(event) liveEventManager.get().dispatchOnLiveToDevice(event)
} }
}
} }
/** /**

View file

@ -23,6 +23,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock 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.logger.LoggerTag
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.ComputeShieldForGroupUseCase import org.matrix.android.sdk.internal.crypto.ComputeShieldForGroupUseCase
@ -41,7 +42,8 @@ internal class OutgoingRequestsProcessor @Inject constructor(
private val requestSender: RequestSender, private val requestSender: RequestSender,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val computeShieldForGroup: ComputeShieldForGroupUseCase private val computeShieldForGroup: ComputeShieldForGroupUseCase,
private val matrixConfiguration: MatrixConfiguration,
) { ) {
private val lock: Mutex = Mutex() private val lock: Mutex = Mutex()
@ -156,6 +158,7 @@ internal class OutgoingRequestsProcessor @Inject constructor(
true true
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.tag(loggerTag.value).e(throwable, "## sendToDevice(): error") Timber.tag(loggerTag.value).e(throwable, "## sendToDevice(): error")
matrixConfiguration.cryptoAnalyticsPlugin?.onFailToSendToDevice(throwable)
false false
} }
} }

View file

@ -151,6 +151,7 @@ import javax.inject.Singleton
flipperProxy.networkInterceptor(), flipperProxy.networkInterceptor(),
), ),
metricPlugins = vectorPlugins.plugins(), metricPlugins = vectorPlugins.plugins(),
cryptoAnalyticsPlugin = vectorPlugins.cryptoMetricPlugin,
) )
} }

View file

@ -16,6 +16,7 @@
package im.vector.app.features.analytics.metrics 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.SentryDownloadDeviceKeysMetrics
import im.vector.app.features.analytics.metrics.sentry.SentrySyncDurationMetrics import im.vector.app.features.analytics.metrics.sentry.SentrySyncDurationMetrics
import org.matrix.android.sdk.api.metrics.MetricPlugin import org.matrix.android.sdk.api.metrics.MetricPlugin
@ -29,6 +30,7 @@ import javax.inject.Singleton
data class VectorPlugins @Inject constructor( data class VectorPlugins @Inject constructor(
val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics, val sentryDownloadDeviceKeysMetrics: SentryDownloadDeviceKeysMetrics,
val sentrySyncDurationMetrics: SentrySyncDurationMetrics, val sentrySyncDurationMetrics: SentrySyncDurationMetrics,
val cryptoMetricPlugin: SentryCryptoAnalytics
) { ) {
/** /**
* Returns [List] of all [MetricPlugin] hold by this class. * Returns [List] of all [MetricPlugin] hold by this class.

View file

@ -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)
}
}