mirror of
https://github.com/element-hq/element-android
synced 2024-11-23 18:05:36 +03:00
Basic sentry e2e reporting for rust + decrypt trust
This commit is contained in:
parent
2ae4b87f2f
commit
ae9711b7d1
8 changed files with 254 additions and 37 deletions
|
@ -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<MetricPlugin> = emptyList()
|
||||
val metricPlugins: List<MetricPlugin> = emptyList(),
|
||||
|
||||
val cryptoAnalyticsPlugin: CryptoMetricPlugin? = null
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<RoomKeyContent>() ?: 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<RoomKeyContent>() ?: 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<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)
|
||||
}
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,6 +151,7 @@ import javax.inject.Singleton
|
|||
flipperProxy.networkInterceptor(),
|
||||
),
|
||||
metricPlugins = vectorPlugins.plugins(),
|
||||
cryptoAnalyticsPlugin = vectorPlugins.cryptoMetricPlugin,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue