1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 16:22:41 +02:00

Compare commits

...

6 Commits

Author SHA1 Message Date
Valere
f1311eef1a quick format doc 2022-07-22 10:03:01 +02:00
Valere
eb10956bc0 rebuilt cached item when needed 2022-07-22 09:57:37 +02:00
Valere
b00b327d43 clean unused param 2022-07-21 13:42:47 +02:00
Valere
1d5f8cbe04 quick lint fix 2022-07-21 13:42:47 +02:00
Valere
9d0d93b89a fix unused string 2022-07-21 13:42:47 +02:00
Valere
135c92e613 Key request UI feedback and refactoring 2022-07-21 13:42:38 +02:00
42 changed files with 875 additions and 569 deletions

View File

@@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
@@ -63,6 +62,7 @@ import org.matrix.android.sdk.api.session.securestorage.KeyRef
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.api.util.toBase64NoPadding import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import java.util.UUID import java.util.UUID
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
@@ -523,18 +523,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
testHelper.runBlockingTest { testHelper.runBlockingTest {
try { session.cryptoService().decryptAndUpdateEvent(event, "")
session.cryptoService().decryptEvent(event, "").let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
}
} catch (error: MXCryptoError) {
// nop
}
} }
Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}")
event.getClearType() == EventType.MESSAGE && event.getClearType() == EventType.MESSAGE &&
@@ -548,15 +537,15 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
sentEventIds.forEach { sentEventId -> sentEventIds.forEach { sentEventId ->
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
testHelper.runBlockingTest { testHelper.runBlockingTest {
try { when (val result = session.cryptoService().decryptEvent(event, "")) {
session.cryptoService().decryptEvent(event, "") is DecryptionResult.Failure -> {
fail("Should not be able to decrypt event") val errorType = (result.error as? MXCryptoError.Base)?.errorType ?: MXCryptoError.ErrorType.UNABLE_TO_DECRYPT
} catch (error: MXCryptoError) { if (expectedError != null) {
val errorType = (error as? MXCryptoError.Base)?.errorType assertEquals("Unexpected reason", expectedError, errorType)
if (expectedError == null) { }
assertNotNull(errorType) }
} else { is DecryptionResult.Success -> {
assertEquals("Unexpected reason", expectedError, errorType) fail("Should not be able to decrypt event")
} }
} }
} }

View File

@@ -55,21 +55,23 @@ class DecryptRedactedEventTest : InstrumentedTest {
val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!! val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!!
testHelper.runBlockingTest { testHelper.runBlockingTest {
try { bobSession.cryptoService().decryptEvent(eventBobPov.root, "").fold(
val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "") { result ->
Assert.assertEquals( Assert.assertEquals(
"Unexpected redacted reason", "Unexpected redacted reason",
redactionReason, redactionReason,
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason") result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason")
) )
Assert.assertEquals( Assert.assertEquals(
"Unexpected Redacted event id", "Unexpected Redacted event id",
timelineEvent.eventId, timelineEvent.eventId,
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts
) )
} catch (failure: Throwable) { },
Assert.fail("Should not throw when decrypting a redacted event") { err, _ ->
} Assert.fail("Should not throw when decrypting a redacted event actual ${err.message}")
}
)
} }
} }
} }

View File

@@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto
import android.util.Log import android.util.Log
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.internal.assertEquals
import org.junit.Assert import org.junit.Assert
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
@@ -61,7 +60,7 @@ import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.common.TestMatrixCallback
import org.matrix.android.sdk.mustFail import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
// @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.") // @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
@@ -504,17 +503,17 @@ class E2eeSanityTests : InstrumentedTest {
// Confirm we can decrypt one but not the other // Confirm we can decrypt one but not the other
testHelper.runBlockingTest { testHelper.runBlockingTest {
mustFail(message = "Should not be able to decrypt event") { Assert.assertTrue(
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") "Should not be able to decrypt event",
} newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") is DecryptionResult.Failure
)
} }
testHelper.runBlockingTest { testHelper.runBlockingTest {
try { Assert.assertTrue(
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") "Should be able to decrypt event",
} catch (error: MXCryptoError) { newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") is DecryptionResult.Success
fail("Should be able to decrypt event") )
}
} }
// Now let's verify bobs session, and re-request keys // Now let's verify bobs session, and re-request keys
@@ -535,22 +534,15 @@ class E2eeSanityTests : InstrumentedTest {
// we should be able to decrypt both // we should be able to decrypt both
testHelper.waitWithLatch { testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) { testHelper.retryPeriodicallyWithLatch(it) {
val canDecryptFirst = try { val canDecryptFirst =
testHelper.runBlockingTest { testHelper.runBlockingTest {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
} } is DecryptionResult.Success
true
} catch (error: MXCryptoError) { val canDecryptSecond =
false testHelper.runBlockingTest {
} newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
val canDecryptSecond = try { } is DecryptionResult.Success
testHelper.runBlockingTest {
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
}
true
} catch (error: MXCryptoError) {
false
}
canDecryptFirst && canDecryptSecond canDecryptFirst && canDecryptSecond
} }
} }

View File

@@ -29,7 +29,6 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
@@ -40,6 +39,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
@@ -226,13 +226,10 @@ class UnwedgingTest : InstrumentedTest {
testHelper.waitWithLatch { testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) { testHelper.retryPeriodicallyWithLatch(it) {
// we should get back the key and be able to decrypt // we should get back the key and be able to decrypt
val result = testHelper.runBlockingTest { val canDecrypt = testHelper.runBlockingTest {
tryOrNull { bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") } is DecryptionResult.Success
} canDecrypt
}
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
result != null
} }
} }

View File

@@ -46,7 +46,7 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.mustFail import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@@ -95,9 +95,10 @@ class KeyShareTests : InstrumentedTest {
assert(receivedEvent!!.isEncrypted()) assert(receivedEvent!!.isEncrypted())
commonTestHelper.runBlockingTest { commonTestHelper.runBlockingTest {
mustFail { assertTrue(
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") "Should not be able to decrypt",
} aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") is DecryptionResult.Failure
)
} }
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
@@ -166,9 +167,10 @@ class KeyShareTests : InstrumentedTest {
} }
commonTestHelper.runBlockingTest { commonTestHelper.runBlockingTest {
mustFail { assertTrue(
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") "Should not be able to decrypt",
} aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") is DecryptionResult.Failure
)
} }
// Mark the device as trusted // Mark the device as trusted

View File

@@ -20,6 +20,7 @@ import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.fail
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -27,7 +28,6 @@ import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.RequestResult import org.matrix.android.sdk.api.session.crypto.RequestResult
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@@ -41,7 +41,6 @@ import org.matrix.android.sdk.common.MockOkHttpInterceptor
import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@@ -96,17 +95,16 @@ class WithHeldTests : InstrumentedTest {
// Bob should not be able to decrypt because the keys is withheld // Bob should not be able to decrypt because the keys is withheld
// .. might need to wait a bit for stability? // .. might need to wait a bit for stability?
testHelper.runBlockingTest { testHelper.runBlockingTest {
mustFail( bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "").fold(
message = "This session should not be able to decrypt", {
failureBlock = { failure -> fail("This session should not be able to decrypt")
},
{ failure, code ->
val type = (failure as MXCryptoError.Base).errorType val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, type)
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, code?.value)
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
} }
) { )
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
} }
// Let's see if the reply we got from bob first session is unverified // Let's see if the reply we got from bob first session is unverified
@@ -139,16 +137,16 @@ class WithHeldTests : InstrumentedTest {
// Previous message should still be undecryptable (partially withheld session) // Previous message should still be undecryptable (partially withheld session)
// .. might need to wait a bit for stability? // .. might need to wait a bit for stability?
testHelper.runBlockingTest { testHelper.runBlockingTest {
mustFail( bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "").fold(
message = "This session should not be able to decrypt", {
failureBlock = { failure -> fail("This session should not be able to decrypt")
},
{ failure, code ->
val type = (failure as MXCryptoError.Base).errorType val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage Assert.assertEquals("Error should be partially withheld", MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, type)
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, code?.value)
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) }
}) { )
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
} }
} }
@@ -187,16 +185,16 @@ class WithHeldTests : InstrumentedTest {
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
// .. might need to wait a bit for stability? // .. might need to wait a bit for stability?
testHelper.runBlockingTest { testHelper.runBlockingTest {
mustFail( bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "").fold(
message = "This session should not be able to decrypt", {
failureBlock = { failure -> fail("This session should not be able to decrypt")
},
{ failure, code ->
val type = (failure as MXCryptoError.Base).errorType val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, type)
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, code?.value)
Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) }
}) { )
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
}
} }
// Ensure that alice has marked the session to be shared with bob // Ensure that alice has marked the session to be shared with bob
@@ -262,10 +260,8 @@ class WithHeldTests : InstrumentedTest {
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also { val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
// try to decrypt and force key request // try to decrypt and force key request
tryOrNull { testHelper.runBlockingTest {
testHelper.runBlockingTest { bobSecondSession.cryptoService().decryptEvent(it.root, "")
bobSecondSession.cryptoService().decryptEvent(it.root, "")
}
} }
} }
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId

View File

@@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.crypto.replayattack package org.matrix.android.sdk.internal.crypto.replayattack
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import org.amshove.kluent.internal.assertFailsWith
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.fail import org.junit.Assert.fail
@@ -64,11 +63,15 @@ class ReplayAttackTest : InstrumentedTest {
// Lets decrypt the original event // Lets decrypt the original event
aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
// Lets decrypt the fake event that will have the same message index // Lets decrypt the fake event that will have the same message index
val exception = assertFailsWith<MXCryptoError.Base> {
// An exception should be thrown while the same index would have been used for the previous decryption aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId).fold(
aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId) {
} fail("Decryption should have fail because of duplicate index")
assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType) },
{ err, _ ->
assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, (err as MXCryptoError.Base).errorType)
}
)
} }
cryptoTestData.cleanUp(testHelper) cryptoTestData.cleanUp(testHelper)
} }

View File

@@ -34,12 +34,12 @@ import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
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.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.android.sdk.internal.crypto.model.SessionInfo import org.matrix.android.sdk.internal.crypto.model.SessionInfo
interface CryptoService { interface CryptoService {
@@ -145,10 +145,9 @@ interface CryptoService {
fun discardOutboundSession(roomId: String) fun discardOutboundSession(roomId: String)
@Throws(MXCryptoError::class) suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) suspend fun decryptAndUpdateEvent(event: Event, timeline: String)
fun getEncryptionAlgorithm(roomId: String): String? fun getEncryptionAlgorithm(roomId: String): String?

View File

@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.olm.OlmException import org.matrix.olm.OlmException
/** /**
@@ -60,7 +61,7 @@ sealed class MXCryptoError : Throwable() {
OLM, OLM,
UNKNOWN_DEVICES, UNKNOWN_DEVICES,
UNKNOWN_MESSAGE_INDEX, UNKNOWN_MESSAGE_INDEX,
KEYS_WITHHELD // KEYS_WITHHELD
} }
companion object { companion object {
@@ -91,4 +92,6 @@ sealed class MXCryptoError : Throwable() {
const val NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." + const val NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." +
" Perhaps the homeserver is hiding the configuration event." " Perhaps the homeserver is hiding the configuration event."
} }
fun toResult() = DecryptionResult.Failure(this)
} }

View File

@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
@@ -99,6 +100,13 @@ data class Event(
@Transient @Transient
var mCryptoErrorReason: String? = null var mCryptoErrorReason: String? = null
/**
* If the session is unknown (UISI) or paritially known (unknown message index)
* we can have more info from the session owner on why it was not sent.
*/
@Transient
var mCryptoWithHeldCode: WithHeldCode? = null
@Transient @Transient
var sendState: SendState = SendState.UNKNOWN var sendState: SendState = SendState.UNKNOWN
@@ -131,6 +139,7 @@ data class Event(
it.mxDecryptionResult = mxDecryptionResult it.mxDecryptionResult = mxDecryptionResult
it.mCryptoError = mCryptoError it.mCryptoError = mCryptoError
it.mCryptoErrorReason = mCryptoErrorReason it.mCryptoErrorReason = mCryptoErrorReason
it.mCryptoWithHeldCode = mCryptoWithHeldCode
it.sendState = sendState it.sendState = sendState
it.ageLocalTs = ageLocalTs it.ageLocalTs = ageLocalTs
it.threadDetails = threadDetails it.threadDetails = threadDetails
@@ -278,6 +287,7 @@ data class Event(
if (mxDecryptionResult != other.mxDecryptionResult) return false if (mxDecryptionResult != other.mxDecryptionResult) return false
if (mCryptoError != other.mCryptoError) return false if (mCryptoError != other.mCryptoError) return false
if (mCryptoErrorReason != other.mCryptoErrorReason) return false if (mCryptoErrorReason != other.mCryptoErrorReason) return false
if (mCryptoWithHeldCode != other.mCryptoWithHeldCode) return false
if (sendState != other.sendState) return false if (sendState != other.sendState) return false
if (threadDetails != other.threadDetails) return false if (threadDetails != other.threadDetails) return false
return true return true
@@ -297,6 +307,7 @@ data class Event(
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0) result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
result = 31 * result + (mCryptoError?.hashCode() ?: 0) result = 31 * result + (mCryptoError?.hashCode() ?: 0)
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0) result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
result = 31 * result + (mCryptoWithHeldCode?.hashCode() ?: 0)
result = 31 * result + sendState.hashCode() result = 31 * result + sendState.hashCode()
result = 31 * result + threadDetails.hashCode() result = 31 * result + threadDetails.hashCode()

View File

@@ -61,7 +61,12 @@ data class TimelineEvent(
val ownedByThreadChunk: Boolean = false, val ownedByThreadChunk: Boolean = false,
val senderInfo: SenderInfo, val senderInfo: SenderInfo,
val annotations: EventAnnotationsSummary? = null, val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<ReadReceipt> = emptyList() val readReceipts: List<ReadReceipt> = emptyList(),
/**
* If true the SDK is currently trying to get the keys from other sessions.
* will only be set when used with Timeline, if not will be null
*/
val hasActiveRequestForKeys: Boolean? = null,
) { ) {
init { init {

View File

@@ -57,8 +57,8 @@ import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
import org.matrix.android.sdk.api.session.crypto.model.TrailType import org.matrix.android.sdk.api.session.crypto.model.TrailType
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
@@ -75,6 +75,7 @@ import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
@@ -752,34 +753,35 @@ internal class DefaultCryptoService @Inject constructor(
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or throw in case of error * @return the MXEventDecryptionResult data, or throw in case of error
*/ */
@Throws(MXCryptoError::class) override suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult {
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return internalDecryptEvent(event, timeline)
}
/**
* Decrypt an event asynchronously.
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @param callback the callback to return data or null
*/
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
eventDecryptor.decryptEventAsync(event, timeline, callback)
}
/**
* Decrypt an event.
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or null in case of error
*/
@Throws(MXCryptoError::class)
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return eventDecryptor.decryptEvent(event, timeline) return eventDecryptor.decryptEvent(event, timeline)
} }
/**
* Decrypts the event and udpdates the event fields with the results.
*/
override suspend fun decryptAndUpdateEvent(event: Event, timeline: String) {
eventDecryptor.decryptEvent(event, timeline).fold(
{ decryptionResult ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = decryptionResult.clearEvent,
senderKey = decryptionResult.senderCurve25519Key,
keysClaimed = decryptionResult.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
)
// clear errors
event.mCryptoError = null
event.mCryptoErrorReason = null
event.mCryptoWithHeldCode = null
},
{ error, code ->
event.mCryptoError = (error as? MXCryptoError.Base)?.errorType ?: MXCryptoError.ErrorType.UNABLE_TO_DECRYPT
event.mCryptoErrorReason = (error as? MXCryptoError.Base)?.technicalMessage ?: error.message
event.mCryptoWithHeldCode = code
}
)
}
/** /**
* Reset replay attack data for the given timeline. * Reset replay attack data for the given timeline.
* *

View File

@@ -16,18 +16,16 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
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 kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
@@ -35,9 +33,9 @@ import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.extensions.foldToCallback
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
@@ -49,7 +47,6 @@ private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO)
@SessionScope @SessionScope
internal class EventDecryptor @Inject constructor( internal class EventDecryptor @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val clock: Clock, private val clock: Clock,
private val roomDecryptorProvider: RoomDecryptorProvider, private val roomDecryptorProvider: RoomDecryptorProvider,
@@ -80,24 +77,26 @@ internal class EventDecryptor @Inject constructor(
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or throw in case of error * @return the MXEventDecryptionResult data, or throw in case of error
*/ */
@Throws(MXCryptoError::class) suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult {
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return internalDecryptEvent(event, timeline) return internalDecryptEvent(event, timeline)
} }
/** suspend fun decryptAndUpdate(event: Event, timeline: String) {
* Decrypt an event asynchronously. val result = internalDecryptEvent(event, timeline)
* when (result) {
* @param event the raw event. is DecryptionResult.Success -> {
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. val decryptionResult = result.decryptedResult
* @param callback the callback to return data or null event.mxDecryptionResult = OlmDecryptionResult(
*/ payload = decryptionResult.clearEvent,
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) { senderKey = decryptionResult.senderCurve25519Key,
// is it needed to do that on the crypto scope?? keysClaimed = decryptionResult.claimedEd25519Key?.let { mapOf("ed25519" to it) },
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
runCatching { )
internalDecryptEvent(event, timeline) }
}.foldToCallback(callback) is DecryptionResult.Failure -> {
event.mCryptoError = (result.error as? MXCryptoError.Base)?.errorType ?: MXCryptoError.ErrorType.UNABLE_TO_DECRYPT
event.mCryptoErrorReason = (result.error as? MXCryptoError.Base)?.technicalMessage ?: result.error.message
}
} }
} }
@@ -108,20 +107,23 @@ internal class EventDecryptor @Inject constructor(
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or null in case of error * @return the MXEventDecryptionResult data, or null in case of error
*/ */
@Throws(MXCryptoError::class) private suspend fun internalDecryptEvent(event: Event, timeline: String): DecryptionResult {
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val eventContent = event.content val eventContent = event.content
if (eventContent == null) { if (eventContent == null) {
Timber.tag(loggerTag.value).e("decryptEvent : empty event content") Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) return DecryptionResult.Failure(
MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
)
} else if (event.isRedacted()) { } else if (event.isRedacted()) {
// we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm // we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm
return MXEventDecryptionResult( return DecryptionResult.Success(
clearEvent = mapOf( MXEventDecryptionResult(
"room_id" to event.roomId.orEmpty(), clearEvent = mapOf(
"type" to EventType.MESSAGE, "room_id" to event.roomId.orEmpty(),
"content" to emptyMap<String, Any>(), "type" to EventType.MESSAGE,
"unsigned" to event.unsignedData.toContent() "content" to emptyMap<String, Any>(),
"unsigned" to event.unsignedData.toContent()
)
) )
) )
} else { } else {
@@ -130,25 +132,27 @@ internal class EventDecryptor @Inject constructor(
if (alg == null) { if (alg == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
Timber.tag(loggerTag.value).e("decryptEvent() : $reason") Timber.tag(loggerTag.value).e("decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) return DecryptionResult.Failure(
MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
)
} else { } else {
try { return alg.decryptEvent(event, timeline).also {
return alg.decryptEvent(event, timeline) if (it is DecryptionResult.Failure) {
} catch (mxCryptoError: MXCryptoError) { val mxCryptoError = it.error
Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
if (algorithm == MXCRYPTO_ALGORITHM_OLM) { if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
if (mxCryptoError is MXCryptoError.Base && if (mxCryptoError is MXCryptoError.Base &&
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) { mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
// need to find sending device // need to find sending device
val olmContent = event.content.toModel<OlmEventContent>() val olmContent = event.content.toModel<OlmEventContent>()
if (event.senderId != null && olmContent?.senderKey != null) { if (event.senderId != null && olmContent?.senderKey != null) {
markOlmSessionForUnwedging(event.senderId, olmContent.senderKey) markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
} else { } else {
Timber.tag(loggerTag.value).d("Can't mark as wedge malformed") Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
}
} }
} }
} }
throw mxCryptoError
} }
} }
} }

View File

@@ -19,8 +19,27 @@ package org.matrix.android.sdk.internal.crypto.algorithms
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
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.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
fun MXEventDecryptionResult.toResult() = DecryptionResult.Success(this)
sealed class DecryptionResult {
data class Success(val decryptedResult: MXEventDecryptionResult) : DecryptionResult()
data class Failure(val error: MXCryptoError, val withheldInfo: WithHeldCode? = null) : DecryptionResult()
fun fold(success: (MXEventDecryptionResult) -> Unit, error: (MXCryptoError, WithHeldCode?) -> Unit) {
when (this) {
is Success -> {
success(this.decryptedResult)
}
is Failure -> {
error(this.error, this.withheldInfo)
}
}
}
}
/** /**
* An interface for decrypting data. * An interface for decrypting data.
*/ */
@@ -33,8 +52,7 @@ internal interface IMXDecrypting {
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the decryption information, or an error * @return the decryption information, or an error
*/ */
@Throws(MXCryptoError::class) suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
/** /**
* Handle a key event. * Handle a key event.

View File

@@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -54,29 +55,33 @@ internal class MXMegolmDecryption(
*/ */
// private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap() // private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
@Throws(MXCryptoError::class) override suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult {
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return decryptEvent(event, timeline, true) return decryptEvent(event, timeline, true)
} }
@Throws(MXCryptoError::class) private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): DecryptionResult {
private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail") Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
if (event.roomId.isNullOrBlank()) { if (event.roomId.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) return DecryptionResult.Failure(
MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
)
} }
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) ?: return DecryptionResult.Failure(
MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
)
if (encryptedEventContent.senderKey.isNullOrBlank() || if (encryptedEventContent.senderKey.isNullOrBlank() ||
encryptedEventContent.sessionId.isNullOrBlank() || encryptedEventContent.sessionId.isNullOrBlank() ||
encryptedEventContent.ciphertext.isNullOrBlank()) { encryptedEventContent.ciphertext.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) return DecryptionResult.Failure(
MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
)
} }
return runCatching { try {
olmDevice.decryptGroupMessage( val olmDecryptionResult = olmDevice.decryptGroupMessage(
encryptedEventContent.ciphertext, encryptedEventContent.ciphertext,
event.roomId, event.roomId,
timeline, timeline,
@@ -84,85 +89,73 @@ internal class MXMegolmDecryption(
encryptedEventContent.sessionId, encryptedEventContent.sessionId,
encryptedEventContent.senderKey encryptedEventContent.senderKey
) )
} return if (olmDecryptionResult.payload != null) {
.fold( MXEventDecryptionResult(
{ olmDecryptionResult -> clearEvent = olmDecryptionResult.payload,
// the decryption succeeds senderCurve25519Key = olmDecryptionResult.senderKey,
if (olmDecryptionResult.payload != null) { claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
MXEventDecryptionResult( forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
clearEvent = olmDecryptionResult.payload, .orEmpty()
senderCurve25519Key = olmDecryptionResult.senderKey, ).also {
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), liveEventManager.get().dispatchLiveEventDecrypted(event, it)
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain }.let {
.orEmpty() DecryptionResult.Success(it)
).also { }
liveEventManager.get().dispatchLiveEventDecrypted(event, it) } else {
} return DecryptionResult.Failure(
} else { MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
},
{ throwable ->
liveEventManager.get().dispatchLiveEventDecryptionFailed(event, throwable)
if (throwable is MXCryptoError.OlmError) {
// TODO Check the value of .message
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
// So we know that session, but it's ratcheted and we can't decrypt at that index
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
// Check if partially withheld
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) {
// Encapsulate as withHeld exception
throw MXCryptoError.Base(
MXCryptoError.ErrorType.KEYS_WITHHELD,
withHeldInfo.code?.value ?: "",
withHeldInfo.reason
)
}
throw MXCryptoError.Base(
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
"UNKNOWN_MESSAGE_INDEX",
null
)
}
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason)
throw MXCryptoError.Base(
MXCryptoError.ErrorType.OLM,
reason,
detailedReason
)
}
if (throwable is MXCryptoError.Base) {
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
// Check if it was withheld by sender to enrich error code
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) {
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
// Encapsulate as withHeld exception
throw MXCryptoError.Base(
MXCryptoError.ErrorType.KEYS_WITHHELD,
withHeldInfo.code?.value ?: "",
withHeldInfo.reason
)
}
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
}
}
throw throwable
}
) )
}
} catch (olmError: MXCryptoError.OlmError) {
liveEventManager.get().dispatchLiveEventDecryptionFailed(event, olmError)
return if (olmError.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
// So we know that session, but it's ratcheted and we can't decrypt at that index
DecryptionResult.Failure(
MXCryptoError.Base(
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
"UNKNOWN_MESSAGE_INDEX",
null
),
// Check if partially withheld
cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)?.code
).also {
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
}
} else {
DecryptionResult.Failure(
MXCryptoError.Base(
MXCryptoError.ErrorType.OLM,
String.format(MXCryptoError.OLM_REASON, olmError.olmException.message),
String.format(MXCryptoError.DETAILED_OLM_REASON, event.eventId, olmError.olmException.message)
)
).also {
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
}
}
} catch (cryptoError: MXCryptoError) {
liveEventManager.get().dispatchLiveEventDecryptionFailed(event, cryptoError)
return DecryptionResult.Failure(
cryptoError,
cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)?.code
).also {
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
}
} catch (other: Throwable) {
Timber.e("Unexpected decryption error, should not happen", other)
return DecryptionResult.Failure(
MXCryptoError.Base(
MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
other.localizedMessage ?: "Unexpected error"
),
cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)?.code
)
}
} }
/** /**

View File

@@ -27,7 +27,9 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
import org.matrix.android.sdk.internal.crypto.algorithms.toResult
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.util.convertFromUTF8 import org.matrix.android.sdk.internal.util.convertFromUTF8
import timber.log.Timber import timber.log.Timber
@@ -42,36 +44,36 @@ internal class MXOlmDecryption(
) : ) :
IMXDecrypting { IMXDecrypting {
@Throws(MXCryptoError::class) override suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult {
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { val olmEventContent = event.content.toModel<OlmEventContent>()
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run { ?: return MXCryptoError.Base(
Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format") MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
throw MXCryptoError.Base( MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON
MXCryptoError.ErrorType.BAD_EVENT_FORMAT, ).toResult().also {
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format")
) }
}
val cipherText = olmEventContent.ciphertext ?: run { val cipherText = olmEventContent.ciphertext
Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text") ?: return MXCryptoError.Base(
throw MXCryptoError.Base( MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
MXCryptoError.ErrorType.MISSING_CIPHER_TEXT, MXCryptoError.MISSING_CIPHER_TEXT_REASON
MXCryptoError.MISSING_CIPHER_TEXT_REASON ).toResult().also {
) Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text")
} }
val senderKey = olmEventContent.senderKey ?: run { val senderKey = olmEventContent.senderKey
Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key") ?: return MXCryptoError.Base(
throw MXCryptoError.Base( MXCryptoError.ErrorType.MISSING_SENDER_KEY,
MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON ).toResult().also {
) Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key")
} }
val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run { val messageAny = cipherText[olmDevice.deviceCurve25519Key]
Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients") ?: return MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)
throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON) .toResult().also {
} Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
}
// The message for myUser // The message for myUser
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -81,8 +83,9 @@ internal class MXOlmDecryption(
if (decryptedPayload == null) { if (decryptedPayload == null) {
Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey") Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) return MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON).toResult()
} }
val payloadString = convertFromUTF8(decryptedPayload) val payloadString = convertFromUTF8(decryptedPayload)
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE) val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
@@ -90,92 +93,97 @@ internal class MXOlmDecryption(
if (payload == null) { if (payload == null) {
Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload") Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON) return MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON).toResult()
} }
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run { val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString)
Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format") ?: return MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) .toResult().also {
} Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format")
}
if (olmPayloadContent.recipient.isNullOrBlank()) { if (olmPayloadContent.recipient.isNullOrBlank()) {
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient") val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
Timber.tag(loggerTag.value).e("## decryptEvent() : $reason") return MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason) .toResult().also {
Timber.tag(loggerTag.value).e("## decryptEvent() : $reason")
}
} }
if (olmPayloadContent.recipient != userId) { if (olmPayloadContent.recipient != userId) {
Timber.tag(loggerTag.value).e( return MXCryptoError.Base(
"## decryptEvent() : Event ${event.eventId}:" +
" Intended recipient ${olmPayloadContent.recipient} does not match our id $userId"
)
throw MXCryptoError.Base(
MXCryptoError.ErrorType.BAD_RECIPIENT, MXCryptoError.ErrorType.BAD_RECIPIENT,
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient) String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)
) ).toResult().also {
Timber.tag(loggerTag.value).e(
"## decryptEvent() : Event ${event.eventId}:" +
" Intended recipient ${olmPayloadContent.recipient} does not match our id $userId"
)
}
} }
val recipientKeys = olmPayloadContent.recipientKeys ?: run { val recipientKeys = olmPayloadContent.recipientKeys
Timber.tag(loggerTag.value).e( ?: return MXCryptoError.Base(
"## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" + MXCryptoError.ErrorType.MISSING_PROPERTY,
" property; cannot prevent unknown-key attack" String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")
) ).toResult().also {
throw MXCryptoError.Base( Timber.tag(loggerTag.value).e(
MXCryptoError.ErrorType.MISSING_PROPERTY, "## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys") " property; cannot prevent unknown-key attack"
) )
} }
val ed25519 = recipientKeys["ed25519"] val ed25519 = recipientKeys["ed25519"]
if (ed25519 != olmDevice.deviceEd25519Key) { if (ed25519 != olmDevice.deviceEd25519Key) {
Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours") Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
throw MXCryptoError.Base( return MXCryptoError.Base(
MXCryptoError.ErrorType.BAD_RECIPIENT_KEY, MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
MXCryptoError.BAD_RECIPIENT_KEY_REASON MXCryptoError.BAD_RECIPIENT_KEY_REASON
) ).toResult()
} }
if (olmPayloadContent.sender.isNullOrBlank()) { if (olmPayloadContent.sender.isNullOrBlank()) {
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack") .e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
throw MXCryptoError.Base( return MXCryptoError.Base(
MXCryptoError.ErrorType.MISSING_PROPERTY, MXCryptoError.ErrorType.MISSING_PROPERTY,
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender") String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")
) ).toResult()
} }
if (olmPayloadContent.sender != event.senderId) { if (olmPayloadContent.sender != event.senderId) {
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}") .e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
throw MXCryptoError.Base( return MXCryptoError.Base(
MXCryptoError.ErrorType.FORWARDED_MESSAGE, MXCryptoError.ErrorType.FORWARDED_MESSAGE,
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender) String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)
) ).toResult()
} }
// that's strange we don't use olm in room anymore
if (olmPayloadContent.roomId != event.roomId) { if (olmPayloadContent.roomId != event.roomId) {
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.e("## decryptEvent() : Event ${event.eventId}: room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}") .e("## decryptEvent() : Event ${event.eventId}: room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
throw MXCryptoError.Base( return MXCryptoError.Base(
MXCryptoError.ErrorType.BAD_ROOM, MXCryptoError.ErrorType.BAD_ROOM,
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId) String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId)
) ).toResult()
} }
val keys = olmPayloadContent.keys ?: run { val keys = olmPayloadContent.keys
Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys") ?: return MXCryptoError.Base(
throw MXCryptoError.Base( MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON
MXCryptoError.MISSING_CIPHER_TEXT_REASON ).toResult().also {
) Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys")
} }
return MXEventDecryptionResult( return MXEventDecryptionResult(
clearEvent = payload, clearEvent = payload,
senderCurve25519Key = senderKey, senderCurve25519Key = senderKey,
claimedEd25519Key = keys["ed25519"] claimedEd25519Key = keys["ed25519"]
) ).toResult()
} }
/** /**

View File

@@ -1260,6 +1260,9 @@ internal class RealmCryptoStore @Inject constructor(
if (newState == OutgoingRoomKeyRequestState.UNSENT) { if (newState == OutgoingRoomKeyRequestState.UNSENT) {
// clear the old replies // clear the old replies
this.replies.deleteAllFromRealm() this.replies.deleteAllFromRealm()
this.creationTimeStamp = clock.epochMillis()
} else if (newState == OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) {
this.creationTimeStamp = clock.epochMillis()
} }
} }
} }
@@ -1658,6 +1661,9 @@ internal class RealmCryptoStore @Inject constructor(
val roomId = withHeldContent.roomId ?: return val roomId = withHeldContent.roomId ?: return
val sessionId = withHeldContent.sessionId ?: return val sessionId = withHeldContent.sessionId ?: return
if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return
Timber.tag(loggerTag.value).v(
"WithHeldMegolmSession set for $roomId|$sessionId is ${withHeldContent.code}"
)
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let { WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let {
it.code = withHeldContent.code it.code = withHeldContent.code
@@ -1679,6 +1685,10 @@ internal class RealmCryptoStore @Inject constructor(
senderKey = it.senderKey senderKey = it.senderKey
) )
} }
}.also {
Timber.tag(loggerTag.value).v(
"WithHeldMegolmSession get for $roomId|$sessionId is ${it?.code}"
)
} }
} }

View File

@@ -52,6 +52,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036
import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@@ -60,7 +61,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 35L, schemaVersion = 36L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@@ -105,5 +106,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 33) MigrateSessionTo033(realm).perform() if (oldVersion < 33) MigrateSessionTo033(realm).perform()
if (oldVersion < 34) MigrateSessionTo034(realm).perform() if (oldVersion < 34) MigrateSessionTo034(realm).perform()
if (oldVersion < 35) MigrateSessionTo035(realm).perform() if (oldVersion < 35) MigrateSessionTo035(realm).perform()
if (oldVersion < 36) MigrateSessionTo036(realm).perform()
} }
} }

View File

@@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
@@ -219,23 +220,26 @@ private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEnt
cryptoService ?: return cryptoService ?: return
val event = eventEntity.asDomain() val event = eventEntity.asDomain()
if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) { if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) {
try { Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}") // Event from sync does not have roomId, so add it to the event first
// Event from sync does not have roomId, so add it to the event first // note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching
// note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), "") }
val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), "") } when (result) {
event.mxDecryptionResult = OlmDecryptionResult( is DecryptionResult.Success -> {
payload = result.clearEvent, val decryptedResult = result.decryptedResult
senderKey = result.senderCurve25519Key, event.mxDecryptionResult = OlmDecryptionResult(
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, payload = decryptedResult.clearEvent,
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain senderKey = decryptedResult.senderCurve25519Key,
) keysClaimed = decryptedResult.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
// Save decryption result, to not decrypt every time we enter the thread list forwardingCurve25519KeyChain = decryptedResult.forwardingCurve25519KeyChain
eventEntity.setDecryptionResult(result) )
} catch (e: MXCryptoError) { // Save decryption result, to not decrypt every time we enter the thread list
if (e is MXCryptoError.Base) { eventEntity.setDecryptionResult(decryptedResult)
event.mCryptoError = e.errorType }
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription is DecryptionResult.Failure -> {
event.mCryptoError = (result.error as? MXCryptoError.Base)?.errorType ?: MXCryptoError.ErrorType.UNABLE_TO_DECRYPT
event.mCryptoErrorReason = (result.error as? MXCryptoError.Base)?.technicalMessage ?: result.error.message
event.mCryptoWithHeldCode = result.withheldInfo
} }
} }
} }

View File

@@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.UnsignedData import org.matrix.android.sdk.api.session.events.model.UnsignedData
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.sender.SenderInfo
@@ -56,6 +57,7 @@ internal object EventMapper {
} }
eventEntity.decryptionErrorReason = event.mCryptoErrorReason eventEntity.decryptionErrorReason = event.mCryptoErrorReason
eventEntity.decryptionErrorCode = event.mCryptoError?.name eventEntity.decryptionErrorCode = event.mCryptoError?.name
eventEntity.decryptionWithheldCode = event.mCryptoWithHeldCode?.value
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
eventEntity.rootThreadEventId = event.getRootThreadEventId() eventEntity.rootThreadEventId = event.getRootThreadEventId()
eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0 eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
@@ -90,18 +92,23 @@ internal object EventMapper {
it.ageLocalTs = eventEntity.ageLocalTs it.ageLocalTs = eventEntity.ageLocalTs
it.sendState = eventEntity.sendState it.sendState = eventEntity.sendState
it.sendStateDetails = eventEntity.sendStateDetails it.sendStateDetails = eventEntity.sendStateDetails
eventEntity.decryptionResultJson?.let { json ->
val decryptionJson = eventEntity.decryptionResultJson
if (decryptionJson != null) {
try { try {
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(decryptionJson)
} catch (t: JsonDataException) { } catch (t: JsonDataException) {
Timber.e(t, "Failed to parse decryption result") Timber.e(t, "Failed to parse decryption result")
} }
} else {
// TODO get the full crypto error object
eventEntity.decryptionErrorCode?.let { errorCode ->
it.mCryptoError = MXCryptoError.ErrorType.valueOf(errorCode)
it.mCryptoWithHeldCode = WithHeldCode.fromCode(eventEntity.decryptionWithheldCode)
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
}
} }
// TODO get the full crypto error object
it.mCryptoError = eventEntity.decryptionErrorCode?.let { errorCode ->
MXCryptoError.ErrorType.valueOf(errorCode)
}
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
it.threadDetails = ThreadDetails( it.threadDetails = ThreadDetails(
isRootThread = eventEntity.isRootThread, isRootThread = eventEntity.isRootThread,
isThread = if (it.threadDetails?.isThread == true) true else eventEntity.isThread(), isThread = if (it.threadDetails?.isThread == true) true else eventEntity.isThread(),

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 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.internal.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo036(realm: DynamicRealm) : RealmMigrator(realm, 36) {
override fun doMigrate(realm: DynamicRealm) {
// add initial withheld code as part as the event entity
realm.schema.get("EventEntity")
?.addField(EventEntityFields.DECRYPTION_WITHHELD_CODE, String::class.java)
?.transform { dynamicObject ->
// previously the withheld code was stored in mCryptoErrorReason when the error type
// was KEYS_WITHHELD but now we are storing it properly for both type UNKNOWN_INBOUND_SESSION_ID and UNKNOWN_MESSAGE_INDEX
val reasonString = dynamicObject.getString(EventEntityFields.DECRYPTION_ERROR_REASON)
val errorCode = dynamicObject.getString(EventEntityFields.DECRYPTION_ERROR_CODE)
// try to see if it's a withheld code
val tryAsCode = WithHeldCode.fromCode(reasonString)
if (errorCode != null && tryAsCode != null) {
// ok migrate to proper code
dynamicObject.setString(EventEntityFields.DECRYPTION_WITHHELD_CODE, tryAsCode.value)
// the former code was WITHHELD, we can't know if it was unknown session or partially withheld, so assume unknown
// we could try to see if session is known but is it worth it?
dynamicObject.setString(EventEntityFields.DECRYPTION_WITHHELD_CODE, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID.name)
}
}
}
}

View File

@@ -79,6 +79,11 @@ internal open class EventEntity(
if (value != field) field = value if (value != field) field = value
} }
var decryptionWithheldCode: String? = null
set(value) {
if (value != field) field = value
}
companion object companion object
fun setDecryptionResult(result: MXEventDecryptionResult) { fun setDecryptionResult(result: MXEventDecryptionResult) {
@@ -93,6 +98,7 @@ internal open class EventEntity(
decryptionResultJson = adapter.toJson(decryptionResult) decryptionResultJson = adapter.toJson(decryptionResult)
decryptionErrorCode = null decryptionErrorCode = null
decryptionErrorReason = null decryptionErrorReason = null
decryptionWithheldCode = null
// If we have an EventInsertEntity for the eventId we make sures it can be processed now. // If we have an EventInsertEntity for the eventId we make sures it can be processed now.
realm.where(EventInsertEntity::class.java) realm.where(EventInsertEntity::class.java)

View File

@@ -17,8 +17,6 @@ package org.matrix.android.sdk.internal.session.room.relation.threads
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
@@ -214,27 +212,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
} }
/**
* Invoke the event decryption mechanism for a specific event.
*/
private suspend fun decryptIfNeeded(event: Event, roomId: String) {
try {
// Event from sync does not have roomId, so add it to the event first
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) {
event.mCryptoError = e.errorType
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
}
}
}
private fun handleReaction( private fun handleReaction(
realm: Realm, realm: Realm,
event: Event, event: Event,

View File

@@ -20,7 +20,7 @@ import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
@@ -80,7 +81,7 @@ internal class RoomSummaryUpdater @Inject constructor(
val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId) val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId)
if (roomSummaryEntity != null) { if (roomSummaryEntity != null) {
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
latestPreviewableEvent?.attemptToDecrypt() latestPreviewableEvent?.attemptToDecrypt(true)
} }
} }
@@ -129,14 +130,14 @@ internal class RoomSummaryUpdater @Inject constructor(
Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]") Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")
val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
Timber.d("## CRYPTO: currentEncryptionEvent is $encryptionEvent") Timber.v("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs
if (lastActivityFromEvent != null) { if (lastActivityFromEvent != null) {
roomSummaryEntity.lastActivityTime = lastActivityFromEvent roomSummaryEntity.lastActivityTime = lastActivityFromEvent
latestPreviewableEvent.attemptToDecrypt() latestPreviewableEvent.attemptToDecrypt(false)
} }
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 || roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 ||
@@ -186,17 +187,27 @@ internal class RoomSummaryUpdater @Inject constructor(
} }
} }
private fun TimelineEventEntity.attemptToDecrypt() { private fun TimelineEventEntity.attemptToDecrypt(force: Boolean) {
when (val root = this.root) { when (val root = this.root) {
null -> { null -> {
Timber.v("Decryption skipped due to missing root event $eventId") Timber.v("Decryption skipped due to missing root event $eventId")
} }
else -> { else -> {
if (root.type == EventType.ENCRYPTED && root.decryptionResultJson == null) { if (root.type == EventType.ENCRYPTED) {
Timber.v("Should decrypt $eventId") if (force || (root.decryptionResultJson == null && root.decryptionErrorCode == null)) {
tryOrNull { Timber.v("Should decrypt $eventId")
runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") } when (val res = runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") }) {
}?.let { root.setDecryptionResult(it) } is DecryptionResult.Success -> {
root.setDecryptionResult(res.decryptedResult)
}
is DecryptionResult.Failure -> {
val error = res.error
root.decryptionErrorCode = (error as? MXCryptoError.Base)?.errorType?.name ?: MXCryptoError.ErrorType.UNABLE_TO_DECRYPT.name
root.decryptionErrorReason = (error as? MXCryptoError.Base)?.technicalMessage ?: error.message
root.decryptionWithheldCode = res.withheldInfo?.value
}
}
}
} }
} }
} }

View File

@@ -0,0 +1,224 @@
/*
* Copyright (c) 2021 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.internal.session.room.timeline
import io.realm.Realm
import io.realm.RealmChangeListener
import io.realm.RealmConfiguration
import io.realm.RealmResults
import io.realm.kotlin.where
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import okhttp3.internal.closeQuietly
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
import org.matrix.android.sdk.internal.util.createBackgroundHandler
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
private val loggerTag = LoggerTag("KeyGossipStatusManager", LoggerTag.CRYPTO)
/**
* Used by DefaultTimeline to decorates timeline events with some crypto database related information.
* The controller will then be able to update the display of events.
*/
internal class CryptoInfoEventDecorator(
private val realmConfiguration: RealmConfiguration,
private val roomId: String,
private val clock: Clock,
private val rebuildListener: RebuildListener,
private val onEventsUpdated: (Boolean) -> Unit,
) : RealmChangeListener<RealmResults<OutgoingKeyRequestEntity>> {
companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("DefaultTimeline_Thread")
const val MAX_AGE_FOR_REQUEST_TO_BE_IDLE = 60_000L
}
private val dispatcher = BACKGROUND_HANDLER.asCoroutineDispatcher()
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
private val sequencer = SemaphoreCoroutineSequencer()
private var outgoingRequestList: RealmResults<OutgoingKeyRequestEntity>? = null
private var decoratedEvents = mutableMapOf<String, MutableList<String>>()
private var activeOutgoingRequestsForSession = emptyList<String>()
private var realmInstance = AtomicReference<Realm>()
private val isStarted = AtomicBoolean(false)
private var tickerTimer: Timer? = null
private var isE2EE = false
fun getActiveRequestForSession(): List<String> {
return synchronized(this) {
activeOutgoingRequestsForSession.toList()
}
}
fun start() {
scope.launch {
sequencer.post {
if (isStarted.compareAndSet(false, true)) {
realmInstance.set(Realm.getInstance(realmConfiguration))
realmInstance.get().where<CryptoRoomEntity>()
.equalTo(CryptoRoomEntityFields.ROOM_ID, roomId)
.findFirst()?.let {
isE2EE = it.wasEncryptedOnce ?: false
}
val now = clock.epochMillis()
// we can limit the query to the most recent request at the time of creation of the CryptoInfoEventDecorator
val createdAfter = now - MAX_AGE_FOR_REQUEST_TO_BE_IDLE
outgoingRequestList = realmInstance.get().where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
.`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR,
listOf(
OutgoingRoomKeyRequestState.UNSENT,
OutgoingRoomKeyRequestState.SENT,
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND,
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED,
)
.map { it.name }
.toTypedArray()
)
.greaterThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, createdAfter)
.findAllAsync().also {
it?.addChangeListener(this@CryptoInfoEventDecorator)
}
}
}
}
}
fun decorateTimelineEvent(event: TimelineEvent): TimelineEvent {
if (!isE2EE) return event
if (event.root.getClearType() != EventType.ENCRYPTED) return event
val megolmSessionId = event.root.content?.get("session_id") as? String ?: return event
// we might want to remember the built events per megolm session
synchronized(this) {
decoratedEvents.getOrPut(megolmSessionId) { mutableListOf() }.add(event.eventId)
}
val hasActiveRequests = getActiveRequestForSession().contains(megolmSessionId)
return if (hasActiveRequests) {
event.copy(
hasActiveRequestForKeys = hasActiveRequests
)
} else event
}
fun stop() {
scope.coroutineContext.cancelChildren()
scope.launch {
sequencer.post {
if (isStarted.compareAndSet(true, false)) {
outgoingRequestList?.removeAllChangeListeners()
decoratedEvents.clear()
outgoingRequestList = null
realmInstance.get().closeQuietly()
tickerTimer?.cancel()
tickerTimer = null
}
}
}
}
override fun onChange(results: RealmResults<OutgoingKeyRequestEntity>) {
if (!results.isLoaded || results.isEmpty()) {
return
}
Timber.tag(loggerTag.value).v("OutgoingKeyRequests data changes")
val now = clock.epochMillis()
val newList = mutableListOf<String>()
// we consider a request active for 1mn?
results.forEach {
if (!it.isValid) return@forEach
val creationTs = it.creationTimeStamp ?: return@forEach
val megolmSessionId = it.megolmSessionId ?: return@forEach
val isActive = when (it.requestState) {
OutgoingRoomKeyRequestState.UNSENT -> true
OutgoingRoomKeyRequestState.SENT -> true
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> false
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> false
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> true
}
val requestAge = now - creationTs
if (isActive && requestAge < MAX_AGE_FOR_REQUEST_TO_BE_IDLE) {
// we consider as active
newList.add(megolmSessionId)
Timber.tag(loggerTag.value).v("Adding active request for megolmSessionId $megolmSessionId age:$requestAge")
} else {
Timber.tag(loggerTag.value).v("Ignoring inactive request for megolmSessionId $megolmSessionId age:$requestAge")
}
}
val sessionsToRebuild = mutableSetOf<String>()
val builtMap = mutableMapOf<String, List<String>>()
val oldActive = getActiveRequestForSession()
synchronized(this) {
sessionsToRebuild.addAll(oldActive)
sessionsToRebuild.addAll(newList)
activeOutgoingRequestsForSession = newList
builtMap.putAll(decoratedEvents)
}
sessionsToRebuild
.flatMap { builtMap[it] ?: emptyList() }
.forEach { eventId ->
rebuildListener.rebuildEvent(eventId) {
decorateTimelineEvent(it)
}
}
onEventsUpdated(true)
// do we need to schedule a ticker update next?
if (newList.isNotEmpty()) {
// yes we have active ones, we should refresh
tickerTimer?.cancel()
tickerTimer = Timer()
tickerTimer?.schedule(
object : TimerTask() {
override fun run() {
Timber.tag(loggerTag.value).v("OutgoingKeyRequests ticker")
scope.launch {
outgoingRequestList?.let {
onChange(it)
}
}
}
},
MAX_AGE_FOR_REQUEST_TO_BE_IDLE
)
}
}
}

View File

@@ -58,6 +58,7 @@ internal class DefaultTimeline(
private val roomId: String, private val roomId: String,
private val initialEventId: String?, private val initialEventId: String?,
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
cryptoRealmConfiguration: RealmConfiguration,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler, private val readReceiptHandler: ReadReceiptHandler,
private val settings: TimelineSettings, private val settings: TimelineSettings,
@@ -101,6 +102,7 @@ internal class DefaultTimeline(
eventDecryptor = eventDecryptor, eventDecryptor = eventDecryptor,
paginationTask = paginationTask, paginationTask = paginationTask,
realmConfiguration = realmConfiguration, realmConfiguration = realmConfiguration,
cryptoRealmConfiguration = cryptoRealmConfiguration,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
fetchThreadTimelineTask = fetchThreadTimelineTask, fetchThreadTimelineTask = fetchThreadTimelineTask,
getContextOfEventTask = getEventTask, getContextOfEventTask = getEventTask,

View File

@@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.realm.RealmConfiguration
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@@ -29,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
@@ -40,6 +42,7 @@ import org.matrix.android.sdk.internal.util.time.Clock
internal class DefaultTimelineService @AssistedInject constructor( internal class DefaultTimelineService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
@CryptoDatabase private val cryptoRealmConfiguration: RealmConfiguration,
private val timelineInput: TimelineInput, private val timelineInput: TimelineInput,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
private val eventDecryptor: TimelineEventDecryptor, private val eventDecryptor: TimelineEventDecryptor,
@@ -68,6 +71,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
initialEventId = eventId, initialEventId = eventId,
settings = settings, settings = settings,
realmConfiguration = monarchy.realmConfiguration, realmConfiguration = monarchy.realmConfiguration,
cryptoRealmConfiguration = cryptoRealmConfiguration,
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
paginationTask = paginationTask, paginationTask = paginationTask,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,

View File

@@ -16,8 +16,6 @@
package org.matrix.android.sdk.internal.session.room.timeline package org.matrix.android.sdk.internal.session.room.timeline
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.EventDecryptor import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
@@ -48,17 +46,7 @@ internal class DefaultGetEventTask @Inject constructor(
// Try to decrypt the Event // Try to decrypt the Event
if (event.isEncrypted()) { if (event.isEncrypted()) {
tryOrNull(message = "Unable to decrypt the event") { eventDecryptor.decryptAndUpdate(event, "")
eventDecryptor.decryptEvent(event, "")
}
?.let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
}
} }
event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0)

View File

@@ -90,6 +90,7 @@ internal class LoadTimelineStrategy constructor(
data class Dependencies( data class Dependencies(
val timelineSettings: TimelineSettings, val timelineSettings: TimelineSettings,
val realm: AtomicReference<Realm>, val realm: AtomicReference<Realm>,
val cryptoRealmConfiguration: RealmConfiguration,
val eventDecryptor: TimelineEventDecryptor, val eventDecryptor: TimelineEventDecryptor,
val paginationTask: PaginationTask, val paginationTask: PaginationTask,
val realmConfiguration: RealmConfiguration, val realmConfiguration: RealmConfiguration,
@@ -127,7 +128,7 @@ internal class LoadTimelineStrategy constructor(
} }
} }
private val uiEchoManagerListener = object : UIEchoManager.Listener { private val rebuildEventListener = object : RebuildListener {
override fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean { override fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean {
return timelineChunk?.rebuildEvent(eventId, builder, searchInNext = true, searchInPrev = true).orFalse() return timelineChunk?.rebuildEvent(eventId, builder, searchInNext = true, searchInPrev = true).orFalse()
} }
@@ -161,7 +162,7 @@ internal class LoadTimelineStrategy constructor(
} }
} }
private val uiEchoManager = UIEchoManager(uiEchoManagerListener, clock) private val uiEchoManager = UIEchoManager(rebuildEventListener, clock)
private val sendingEventsDataSource: SendingEventsDataSource = RealmSendingEventsDataSource( private val sendingEventsDataSource: SendingEventsDataSource = RealmSendingEventsDataSource(
roomId = roomId, roomId = roomId,
realm = dependencies.realm, realm = dependencies.realm,
@@ -176,11 +177,20 @@ internal class LoadTimelineStrategy constructor(
dependencies.matrixCoroutineDispatchers.main dependencies.matrixCoroutineDispatchers.main
) )
private val cryptoInfoEventDecorator = CryptoInfoEventDecorator(
realmConfiguration = dependencies.cryptoRealmConfiguration,
roomId = roomId,
clock = clock,
rebuildListener = rebuildEventListener,
onEventsUpdated = dependencies.onEventsUpdated
)
suspend fun onStart() { suspend fun onStart() {
dependencies.eventDecryptor.start() dependencies.eventDecryptor.start()
dependencies.timelineInput.listeners.add(timelineInputListener) dependencies.timelineInput.listeners.add(timelineInputListener)
val realm = dependencies.realm.get() val realm = dependencies.realm.get()
sendingEventsDataSource.start() sendingEventsDataSource.start()
cryptoInfoEventDecorator.start()
chunkEntity = getChunkEntity(realm).also { chunkEntity = getChunkEntity(realm).also {
it.addChangeListener(chunkEntityListener) it.addChangeListener(chunkEntityListener)
timelineChunk = it.createTimelineChunk() timelineChunk = it.createTimelineChunk()
@@ -196,6 +206,7 @@ internal class LoadTimelineStrategy constructor(
dependencies.timelineInput.listeners.remove(timelineInputListener) dependencies.timelineInput.listeners.remove(timelineInputListener)
chunkEntity?.removeChangeListener(chunkEntityListener) chunkEntity?.removeChangeListener(chunkEntityListener)
sendingEventsDataSource.stop() sendingEventsDataSource.stop()
cryptoInfoEventDecorator.stop()
timelineChunk?.close(closeNext = true, closePrev = true) timelineChunk?.close(closeNext = true, closePrev = true)
getContextLatch?.cancel() getContextLatch?.cancel()
chunkEntity = null chunkEntity = null
@@ -336,6 +347,7 @@ internal class LoadTimelineStrategy constructor(
fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask, fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask,
timelineEventMapper = dependencies.timelineEventMapper, timelineEventMapper = dependencies.timelineEventMapper,
uiEchoManager = uiEchoManager, uiEchoManager = uiEchoManager,
cryptoInfoEventDecorator = cryptoInfoEventDecorator,
threadsAwarenessHandler = dependencies.threadsAwarenessHandler, threadsAwarenessHandler = dependencies.threadsAwarenessHandler,
lightweightSettingsStorage = dependencies.lightweightSettingsStorage, lightweightSettingsStorage = dependencies.lightweightSettingsStorage,
initialEventId = mode.originEventId(), initialEventId = mode.originEventId(),

View File

@@ -0,0 +1,23 @@
/*
* 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.internal.session.room.timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
interface RebuildListener {
fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean
}

View File

@@ -61,6 +61,7 @@ internal class TimelineChunk(
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val uiEchoManager: UIEchoManager?, private val uiEchoManager: UIEchoManager?,
private val cryptoInfoEventDecorator: CryptoInfoEventDecorator?,
private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val lightweightSettingsStorage: LightweightSettingsStorage, private val lightweightSettingsStorage: LightweightSettingsStorage,
private val initialEventId: String?, private val initialEventId: String?,
@@ -414,6 +415,8 @@ internal class TimelineChunk(
).let { ).let {
// eventually enhance with ui echo? // eventually enhance with ui echo?
(uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it) (uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it)
}.let {
(cryptoInfoEventDecorator?.decorateTimelineEvent(it) ?: it)
} }
/** /**
@@ -576,6 +579,7 @@ internal class TimelineChunk(
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
timelineEventMapper = timelineEventMapper, timelineEventMapper = timelineEventMapper,
uiEchoManager = uiEchoManager, uiEchoManager = uiEchoManager,
cryptoInfoEventDecorator = cryptoInfoEventDecorator,
threadsAwarenessHandler = threadsAwarenessHandler, threadsAwarenessHandler = threadsAwarenessHandler,
lightweightSettingsStorage = lightweightSettingsStorage, lightweightSettingsStorage = lightweightSettingsStorage,
initialEventId = null, initialEventId = null,

View File

@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.crypto.NewSessionListener
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.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
@@ -129,29 +130,30 @@ internal class TimelineEventDecryptor @Inject constructor(
threadAwareNonEncryptedEvents(request, realm) threadAwareNonEncryptedEvents(request, realm)
return return
} }
try { // note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching
// note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching when (val result = runBlocking { cryptoService.decryptEvent(request.event, timelineId) }) {
val result = runBlocking { cryptoService.decryptEvent(request.event, timelineId) } is DecryptionResult.Success -> {
Timber.v("Successfully decrypted event ${event.eventId}") Timber.v("Successfully decrypted event ${event.eventId}")
realm.executeTransaction { realm.executeTransaction {
val eventId = event.eventId ?: return@executeTransaction val eventId = event.eventId ?: return@executeTransaction
val eventEntity = EventEntity val eventEntity = EventEntity
.where(it, eventId = eventId) .where(it, eventId = eventId)
.findFirst() .findFirst()
eventEntity?.setDecryptionResult(result) eventEntity?.setDecryptionResult(result.decryptedResult)
val decryptedEvent = eventEntity?.asDomain() val decryptedEvent = eventEntity?.asDomain()
threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity) threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
}
} }
} catch (e: MXCryptoError) { is DecryptionResult.Failure -> {
Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}") val error = result.error
if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) { Timber.v("Failed to decrypt event ${event.eventId} : ${error.localizedMessage}")
// Keep track of unknown sessions to automatically try to decrypt on new session
realm.executeTransaction { realm.executeTransaction {
EventEntity.where(it, eventId = event.eventId ?: "") EventEntity.where(it, eventId = event.eventId ?: "")
.findFirst() .findFirst()
?.let { ?.let {
it.decryptionErrorCode = e.errorType.name it.decryptionErrorCode = (error as? MXCryptoError.Base)?.errorType?.name ?: MXCryptoError.ErrorType.UNABLE_TO_DECRYPT.name
it.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription it.decryptionErrorReason = (error as? MXCryptoError.Base)?.technicalMessage ?: error.message
it.decryptionWithheldCode = result.withheldInfo?.value
} }
} }
event.content?.toModel<EncryptedEventContent>()?.let { content -> event.content?.toModel<EncryptedEventContent>()?.let { content ->
@@ -163,12 +165,9 @@ internal class TimelineEventDecryptor @Inject constructor(
} }
} }
} }
} catch (t: Throwable) { }
Timber.e("Failed to decrypt event ${event.eventId}, ${t.localizedMessage}") synchronized(existingRequests) {
} finally { existingRequests.remove(request)
synchronized(existingRequests) {
existingRequests.remove(request)
}
} }
} }

View File

@@ -29,14 +29,10 @@ import timber.log.Timber
import java.util.Collections import java.util.Collections
internal class UIEchoManager( internal class UIEchoManager(
private val listener: Listener, private val listener: RebuildListener,
private val clock: Clock, private val clock: Clock,
) { ) {
interface Listener {
fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean
}
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList()) private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
fun getInMemorySendingEvents(): List<TimelineEvent> { fun getInMemorySendingEvents(): List<TimelineEvent> {

View File

@@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.sync.handler
import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@@ -28,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.api.session.sync.model.SyncResponse
import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
import org.matrix.android.sdk.internal.session.sync.ProgressReporter import org.matrix.android.sdk.internal.session.sync.ProgressReporter
import timber.log.Timber import timber.log.Timber
@@ -68,40 +68,33 @@ internal class CryptoSyncHandler @Inject constructor(
* @param timelineId the timeline identifier * @param timelineId the timeline identifier
* @return true if the event has been decrypted * @return true if the event has been decrypted
*/ */
private suspend fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean { private suspend fun decryptToDeviceEvent(event: Event, timelineId: String?) {
Timber.v("## CRYPTO | decryptToDeviceEvent") Timber.v("## CRYPTO | decryptToDeviceEvent")
if (event.getClearType() == EventType.ENCRYPTED) { if (event.getClearType() == EventType.ENCRYPTED) {
var result: MXEventDecryptionResult? = null when (val result = cryptoService.decryptEvent(event, timelineId ?: "")) {
try { is DecryptionResult.Success -> {
result = cryptoService.decryptEvent(event, timelineId ?: "") val decryptionResult = result.decryptedResult
} catch (exception: MXCryptoError) { event.mxDecryptionResult = OlmDecryptionResult(
event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType // setCryptoError(exception.cryptoError) payload = decryptionResult.clearEvent,
val senderKey = event.content.toModel<OlmEventContent>()?.senderKey ?: "<unknown sender key>" senderKey = decryptionResult.senderCurve25519Key,
// try to find device id to ease log reading keysClaimed = decryptionResult.claimedEd25519Key?.let { mapOf("ed25519" to it) },
val deviceId = cryptoService.getCryptoDeviceInfo(event.senderId!!).firstOrNull { forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
it.identityKey() == senderKey )
}?.deviceId ?: senderKey }
Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>") is DecryptionResult.Failure -> {
} catch (failure: Throwable) { val exception = result.error
Timber.e(failure, "## CRYPTO | Failed to decrypt to device event from ${event.senderId}") event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType ?: MXCryptoError.ErrorType.UNABLE_TO_DECRYPT
} event.mCryptoWithHeldCode = result.withheldInfo
val senderKey = event.content.toModel<OlmEventContent>()?.senderKey ?: "<unknown sender key>"
if (null != result) { // try to find device id to ease log reading
event.mxDecryptionResult = OlmDecryptionResult( val deviceId = event.senderId?.let {
payload = result.clearEvent, cryptoService.getCryptoDeviceInfo(it).firstOrNull {
senderKey = result.senderCurve25519Key, it.identityKey() == senderKey
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, }?.deviceId
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain } ?: senderKey
) Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>")
return true }
} else {
// Could happen for to device events
// None of the known session could decrypt the message
// In this case unwedging process might have been started (rate limited)
Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}")
} }
} }
return false
} }
} }

View File

@@ -21,7 +21,6 @@ import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@@ -539,22 +538,11 @@ internal class RoomSyncHandler @Inject constructor(
} }
private fun decryptIfNeeded(event: Event, roomId: String) { private fun decryptIfNeeded(event: Event, roomId: String) {
try { if (event.mxDecryptionResult == null) {
val timelineId = generateTimelineId(roomId) val timelineId = generateTimelineId(roomId)
// Event from sync does not have roomId, so add it to the event first // Event from sync does not have roomId, so add it to the event first
// note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching // note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching
val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), timelineId) } runBlocking { cryptoService.decryptAndUpdateEvent(event.copy(roomId = roomId), timelineId) }
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) {
event.mCryptoError = e.errorType
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
}
} }
} }

View File

@@ -146,8 +146,7 @@ class DecryptionFailureTracker @Inject constructor(
private fun MXCryptoError.ErrorType.toAnalyticsErrorName(): DetailedErrorName { private fun MXCryptoError.ErrorType.toAnalyticsErrorName(): DetailedErrorName {
val detailed = "$name | mxc_crypto_error_type" val detailed = "$name | mxc_crypto_error_type"
val errorName = when (this) { val errorName = when (this) {
MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID -> Error.Name.OlmKeysNotSentError
MXCryptoError.ErrorType.KEYS_WITHHELD -> Error.Name.OlmKeysNotSentError
MXCryptoError.ErrorType.OLM -> Error.Name.OlmUnspecifiedError MXCryptoError.ErrorType.OLM -> Error.Name.OlmUnspecifiedError
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX -> Error.Name.OlmIndexError MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX -> Error.Name.OlmIndexError
else -> Error.Name.UnknownError else -> Error.Name.UnknownError

View File

@@ -80,11 +80,9 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
@@ -1111,13 +1109,7 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) { private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) {
room.getTimelineEvent(action.eventId)?.let { room.getTimelineEvent(action.eventId)?.let {
val code = when (it.root.mCryptoError) { val code = it.root.mCryptoWithHeldCode
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
WithHeldCode.fromCode(it.root.mCryptoErrorReason)
}
else -> null
}
_viewEvents.post(RoomDetailViewEvents.ShowE2EErrorMessage(code)) _viewEvents.post(RoomDetailViewEvents.ShowE2EErrorMessage(code))
} }
} }

View File

@@ -420,6 +420,11 @@ class MessageActionsViewModel @AssistedInject constructor(
} }
} }
addViewSourceItems(timelineEvent) addViewSourceItems(timelineEvent)
} else {
if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1 ||
timelineEvent.senderInfo.userId != session.myUserId) {
add(EventSharedAction.ReRequestKey(timelineEvent.eventId))
}
} }
add(EventSharedAction.CopyPermalink(eventId)) add(EventSharedAction.CopyPermalink(eventId))
if (session.myUserId != timelineEvent.root.senderId) { if (session.myUserId != timelineEvent.root.senderId) {

View File

@@ -29,11 +29,8 @@ import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.isReply import org.matrix.android.sdk.api.session.events.model.isReply
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import timber.log.Timber
import java.util.UUID import java.util.UUID
class ViewEditHistoryViewModel @AssistedInject constructor( class ViewEditHistoryViewModel @AssistedInject constructor(
@@ -76,18 +73,7 @@ class ViewEditHistoryViewModel @AssistedInject constructor(
val timelineID = event.roomId + UUID.randomUUID().toString() val timelineID = event.roomId + UUID.randomUUID().toString()
// We need to check encryption // We need to check encryption
if (event.isEncrypted() && event.mxDecryptionResult == null) { if (event.isEncrypted() && event.mxDecryptionResult == null) {
// for now decrypt sync session.cryptoService().decryptAndUpdateEvent(event, timelineID)
try {
val result = session.cryptoService().decryptEvent(event, timelineID)
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.w("Failed to decrypt event in history")
}
} }
if (event.eventId == eventId) { if (event.eventId == eventId) {

View File

@@ -30,9 +30,9 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.image import me.gujun.android.span.image
import me.gujun.android.span.span import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import javax.inject.Inject import javax.inject.Inject
@@ -56,16 +56,18 @@ class EncryptedItemFactory @Inject constructor(
val cryptoError = event.root.mCryptoError val cryptoError = event.root.mCryptoError
val spannableStr = if (vectorPreferences.developerMode()) { val spannableStr = if (vectorPreferences.developerMode()) {
val errorDescription = // In developer mode we want to show the raw error
if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { val errorDescription = if (event.root.mCryptoWithHeldCode != null) {
stringProvider.getString(R.string.notice_crypto_error_unknown_inbound_session_id) "${cryptoError?.name ?: "NULL"} | ${event.root.mCryptoWithHeldCode?.value}"
} else { } else {
// TODO i18n cryptoError?.name ?: "NULL"
cryptoError?.name }
}
val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null } val message = stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription).let {
?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) if (event.hasActiveRequestForKeys == true) {
"$it - key request in progress"
} else it
}
span(message) { span(message) {
textStyle = "italic" textStyle = "italic"
textColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) textColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
@@ -78,29 +80,38 @@ class EncryptedItemFactory @Inject constructor(
textColor = colorFromAttribute textColor = colorFromAttribute
} }
} else { } else {
when (cryptoError) { if (event.hasActiveRequestForKeys == true) {
MXCryptoError.ErrorType.KEYS_WITHHELD -> { span {
span { drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let { image(it, "baseline")
image(it, "baseline") +" "
+" " }
} span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_requesting_keys)) {
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) { textStyle = "italic"
textStyle = "italic" textColor = colorFromAttribute
textColor = colorFromAttribute
}
} }
} }
else -> { } else if (event.root.mCryptoWithHeldCode != null) {
span { val messageId = when (event.root.mCryptoWithHeldCode) {
drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let { WithHeldCode.BLACKLISTED -> R.string.crypto_error_withheld_blacklisted
image(it, "baseline") WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified
+" " else -> R.string.crypto_error_withheld_generic
} }
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) { span {
textStyle = "italic" drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
textColor = colorFromAttribute image(it, "baseline")
} +" "
}
span(stringProvider.getString(messageId)) {
textStyle = "italic"
textColor = colorFromAttribute
}
}
} else {
span {
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_no_keys)) {
textStyle = "italic"
textColor = colorFromAttribute
} }
} }
} }

View File

@@ -27,8 +27,6 @@ import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormat
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isEdition import org.matrix.android.sdk.api.session.events.model.isEdition
@@ -48,7 +46,6 @@ import org.matrix.android.sdk.api.session.room.timeline.getEditedEventId
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber import timber.log.Timber
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
/** /**
@@ -207,16 +204,7 @@ class NotifiableEventResolver @Inject constructor(
if (root.isEncrypted() && root.mxDecryptionResult == null) { if (root.isEncrypted() && root.mxDecryptionResult == null) {
// TODO use a global event decryptor? attache to session and that listen to new sessionId? // TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync // for now decrypt sync
try { session.cryptoService().decryptAndUpdateEvent(root, "")
val result = session.cryptoService().decryptEvent(root, root.roomId + UUID.randomUUID().toString())
root.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (ignore: MXCryptoError) {
}
} }
} }

View File

@@ -125,7 +125,8 @@
<string name="notice_power_level_diff">%1$s from %2$s to %3$s</string> <string name="notice_power_level_diff">%1$s from %2$s to %3$s</string>
<string name="notice_crypto_unable_to_decrypt">** Unable to decrypt: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Unable to decrypt: %s **</string>
<string name="notice_crypto_error_unknown_inbound_session_id">The sender\'s device has not sent us the keys for this message.</string> <!-- TODO TO BE REMOVED -->
<string name="notice_crypto_error_unknown_inbound_session_id" tools:ignore="UnusedResources">The sender\'s device has not sent us the keys for this message.</string>
<!-- Messages --> <!-- Messages -->
@@ -2673,8 +2674,11 @@
<string name="room_settings_save_success">You changed room settings successfully</string> <string name="room_settings_save_success">You changed room settings successfully</string>
<string name="room_settings_set_avatar">Set avatar</string> <string name="room_settings_set_avatar">Set avatar</string>
<string name="notice_crypto_unable_to_decrypt_final">You cannot access this message</string> <string name="notice_crypto_unable_to_decrypt_no_keys">"Unable to decrypt message, the sender's device has not sent us the keys."</string>
<!-- TODO TO BE REMOVED -->
<string name="notice_crypto_unable_to_decrypt_final" tools:ignore="UnusedResources">You cannot access this message</string>
<string name="notice_crypto_unable_to_decrypt_friendly">Waiting for this message, this may take a while</string> <string name="notice_crypto_unable_to_decrypt_friendly">Waiting for this message, this may take a while</string>
<string name="notice_crypto_unable_to_decrypt_requesting_keys">"Requesting keys to decrypt this message, this may take a while"</string>
<string name="notice_crypto_unable_to_decrypt_friendly_desc">Due to end-to-end encryption, you might need to wait for someone\'s message to arrive because the encryption keys were not properly sent to you.</string> <string name="notice_crypto_unable_to_decrypt_friendly_desc">Due to end-to-end encryption, you might need to wait for someone\'s message to arrive because the encryption keys were not properly sent to you.</string>
<string name="crypto_error_withheld_blacklisted">You cannot access this message because you have been blocked by the sender</string> <string name="crypto_error_withheld_blacklisted">You cannot access this message because you have been blocked by the sender</string>
<string name="crypto_error_withheld_unverified">You cannot access this message because your session is not trusted by the sender</string> <string name="crypto_error_withheld_unverified">You cannot access this message because your session is not trusted by the sender</string>