diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt index 6dd45c570e..f7fb177e12 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailure.kt @@ -39,7 +39,7 @@ fun DecryptionFailure.toAnalyticsEvent(): Error { return Error( context = "mxc_crypto_error_type|${errorMsg}", domain = Error.Domain.E2EE, - name = this.error.toAnalyticsErrorName(), + name = this.toAnalyticsErrorName(), // this is deprecated keep for backward compatibility cryptoModule = Error.CryptoModule.Rust, cryptoSDK = Error.CryptoSDK.Rust, @@ -52,9 +52,10 @@ fun DecryptionFailure.toAnalyticsEvent(): Error { ) } -private fun MXCryptoError.toAnalyticsErrorName(): Error.Name { - return if (this is MXCryptoError.Base) { - when (errorType) { +private fun DecryptionFailure.toAnalyticsErrorName(): Error.Name { + val error = this.error + val name = if (error is MXCryptoError.Base) { + when (error.errorType) { MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.ErrorType.KEYS_WITHHELD -> Error.Name.OlmKeysNotSentError MXCryptoError.ErrorType.OLM -> Error.Name.OlmUnspecifiedError @@ -64,4 +65,12 @@ private fun MXCryptoError.toAnalyticsErrorName(): Error.Name { } else { Error.Name.UnknownError } + // check if it's an expected UTD! + val localAge = this.eventLocalAgeAtDecryptionFailure + val isHistorical = localAge != null && localAge < 0 + if (isHistorical && !this.ownIdentityTrustedAtTimeOfDecryptionFailure) { + return Error.Name.HistoricalMessage + } + + return name } diff --git a/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt b/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt index 6088d2c465..3be9a6dd18 100644 --- a/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt +++ b/vector/src/test/java/im/vector/app/features/analytics/DecryptionFailureTrackerTest.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldNotBeEqualTo import org.junit.Before import org.junit.Rule import org.junit.Test @@ -550,6 +551,142 @@ class DecryptionFailureTrackerTest { decryptionFailureTracker.stop() } + @Test + fun `should report historical UTDs as an expected UTD if not verified`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + val historicalEventTimestamp = formatter.parse("2024-03-08 09:24:11")!!.time + val sessionCreationTime = formatter.parse("2024-03-09 10:00:00")!!.time + + val eventSlot = slot() + + every { + fakeAnalyticsTracker.capture(event = capture(eventSlot)) + } just runs + + fakeSession.fakeCryptoService.cryptoDeviceInfo = CryptoDeviceInfo( + deviceId = "ABCDEFGHT", + userId = "@alice:matrix.org", + firstTimeSeenLocalTs = sessionCreationTime + ) + + var currentFakeTime = 100_000L + fakeClock.givenEpoch(currentFakeTime) + fakeActiveSessionDataSource.setActiveSession(fakeSession) + decryptionFailureTracker.start(CoroutineScope(coroutineContext)) + runCurrent() + + // historical event and session not verified + fakeSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(false) + val event = aFakeBobMxOrgEvent.copy( + originServerTs = historicalEventTimestamp + ) + decryptionFailureTracker.onEventDecryptionError(event, aUISIError) + runCurrent() + + // advance time to be ahead of the permanent UTD period + currentFakeTime += 70_000 + fakeClock.givenEpoch(currentFakeTime) + advanceTimeBy(70_000) + runCurrent() + + (eventSlot.captured as Error).name shouldBeEqualTo Error.Name.HistoricalMessage + + decryptionFailureTracker.stop() + } + + @Test + fun `should not report historical UTDs as an expected UTD if verified`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + val historicalEventTimestamp = formatter.parse("2024-03-08 09:24:11")!!.time + val sessionCreationTime = formatter.parse("2024-03-09 10:00:00")!!.time + + val eventSlot = slot() + + every { + fakeAnalyticsTracker.capture(event = capture(eventSlot)) + } just runs + + fakeSession.fakeCryptoService.cryptoDeviceInfo = CryptoDeviceInfo( + deviceId = "ABCDEFGHT", + userId = "@alice:matrix.org", + firstTimeSeenLocalTs = sessionCreationTime + ) + + var currentFakeTime = 100_000L + fakeClock.givenEpoch(currentFakeTime) + fakeActiveSessionDataSource.setActiveSession(fakeSession) + decryptionFailureTracker.start(CoroutineScope(coroutineContext)) + runCurrent() + + // historical event and session not verified + fakeSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(true) + val event = aFakeBobMxOrgEvent.copy( + originServerTs = historicalEventTimestamp + ) + decryptionFailureTracker.onEventDecryptionError(event, aUISIError) + runCurrent() + + // advance time to be ahead of the permanent UTD period + currentFakeTime += 70_000 + fakeClock.givenEpoch(currentFakeTime) + advanceTimeBy(70_000) + runCurrent() + + (eventSlot.captured as Error).name shouldNotBeEqualTo Error.Name.HistoricalMessage + + decryptionFailureTracker.stop() + } + + @Test + fun `should not report live UTDs as an expected UTD even if not verified`() = runTest { + val fakeSession = fakeMxOrgTestSession + + val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + val sessionCreationTime = formatter.parse("2024-03-09 10:00:00")!!.time + // 1mn after creation + val liveEventTimestamp = formatter.parse("2024-03-09 10:01:00")!!.time + + val eventSlot = slot() + + every { + fakeAnalyticsTracker.capture(event = capture(eventSlot)) + } just runs + + fakeSession.fakeCryptoService.cryptoDeviceInfo = CryptoDeviceInfo( + deviceId = "ABCDEFGHT", + userId = "@alice:matrix.org", + firstTimeSeenLocalTs = sessionCreationTime + ) + + var currentFakeTime = 100_000L + fakeClock.givenEpoch(currentFakeTime) + fakeActiveSessionDataSource.setActiveSession(fakeSession) + decryptionFailureTracker.start(CoroutineScope(coroutineContext)) + runCurrent() + + // historical event and session not verified + fakeSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(false) + val event = aFakeBobMxOrgEvent.copy( + originServerTs = liveEventTimestamp + ) + decryptionFailureTracker.onEventDecryptionError(event, aUISIError) + runCurrent() + + // advance time to be ahead of the permanent UTD period + currentFakeTime += 70_000 + fakeClock.givenEpoch(currentFakeTime) + advanceTimeBy(70_000) + runCurrent() + + (eventSlot.captured as Error).name shouldNotBeEqualTo Error.Name.HistoricalMessage + + decryptionFailureTracker.stop() + } + @Test fun `should report if permanent UTD`() = runTest { val fakeSession = fakeMxOrgTestSession