mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 00:02:48 +02:00
Compare commits
6 Commits
develop
...
feature/bc
Author | SHA1 | Date | |
---|---|---|---|
|
f1311eef1a | ||
|
eb10956bc0 | ||
|
b00b327d43 | ||
|
1d5f8cbe04 | ||
|
9d0d93b89a | ||
|
135c92e613 |
@@ -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.MegolmBackupCreationInfo
|
||||
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.OutgoingSasVerificationTransaction
|
||||
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.awaitCallback
|
||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
@@ -523,18 +523,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
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
|
||||
}
|
||||
session.cryptoService().decryptAndUpdateEvent(event, "")
|
||||
}
|
||||
Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}")
|
||||
event.getClearType() == EventType.MESSAGE &&
|
||||
@@ -548,15 +537,15 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
session.cryptoService().decryptEvent(event, "")
|
||||
fail("Should not be able to decrypt event")
|
||||
} catch (error: MXCryptoError) {
|
||||
val errorType = (error as? MXCryptoError.Base)?.errorType
|
||||
if (expectedError == null) {
|
||||
assertNotNull(errorType)
|
||||
} else {
|
||||
assertEquals("Unexpected reason", expectedError, errorType)
|
||||
when (val result = session.cryptoService().decryptEvent(event, "")) {
|
||||
is DecryptionResult.Failure -> {
|
||||
val errorType = (result.error as? MXCryptoError.Base)?.errorType ?: MXCryptoError.ErrorType.UNABLE_TO_DECRYPT
|
||||
if (expectedError != null) {
|
||||
assertEquals("Unexpected reason", expectedError, errorType)
|
||||
}
|
||||
}
|
||||
is DecryptionResult.Success -> {
|
||||
fail("Should not be able to decrypt event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -55,21 +55,23 @@ class DecryptRedactedEventTest : InstrumentedTest {
|
||||
val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!!
|
||||
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "")
|
||||
Assert.assertEquals(
|
||||
"Unexpected redacted reason",
|
||||
redactionReason,
|
||||
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason")
|
||||
)
|
||||
Assert.assertEquals(
|
||||
"Unexpected Redacted event id",
|
||||
timelineEvent.eventId,
|
||||
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts
|
||||
)
|
||||
} catch (failure: Throwable) {
|
||||
Assert.fail("Should not throw when decrypting a redacted event")
|
||||
}
|
||||
bobSession.cryptoService().decryptEvent(eventBobPov.root, "").fold(
|
||||
{ result ->
|
||||
Assert.assertEquals(
|
||||
"Unexpected redacted reason",
|
||||
redactionReason,
|
||||
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason")
|
||||
)
|
||||
Assert.assertEquals(
|
||||
"Unexpected Redacted event id",
|
||||
timelineEvent.eventId,
|
||||
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts
|
||||
)
|
||||
},
|
||||
{ err, _ ->
|
||||
Assert.fail("Should not throw when decrypting a redacted event actual ${err.message}")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto
|
||||
import android.util.Log
|
||||
import androidx.test.filters.LargeTest
|
||||
import kotlinx.coroutines.delay
|
||||
import org.amshove.kluent.fail
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.junit.Assert
|
||||
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.TestConstants
|
||||
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
|
||||
|
||||
// @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
|
||||
testHelper.runBlockingTest {
|
||||
mustFail(message = "Should not be able to decrypt event") {
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||
}
|
||||
Assert.assertTrue(
|
||||
"Should not be able to decrypt event",
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") is DecryptionResult.Failure
|
||||
)
|
||||
}
|
||||
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||
} catch (error: MXCryptoError) {
|
||||
fail("Should be able to decrypt event")
|
||||
}
|
||||
Assert.assertTrue(
|
||||
"Should be able to decrypt event",
|
||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") is DecryptionResult.Success
|
||||
)
|
||||
}
|
||||
|
||||
// Now let's verify bobs session, and re-request keys
|
||||
@@ -535,22 +534,15 @@ class E2eeSanityTests : InstrumentedTest {
|
||||
// we should be able to decrypt both
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
val canDecryptFirst = try {
|
||||
testHelper.runBlockingTest {
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||
}
|
||||
true
|
||||
} catch (error: MXCryptoError) {
|
||||
false
|
||||
}
|
||||
val canDecryptSecond = try {
|
||||
testHelper.runBlockingTest {
|
||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||
}
|
||||
true
|
||||
} catch (error: MXCryptoError) {
|
||||
false
|
||||
}
|
||||
val canDecryptFirst =
|
||||
testHelper.runBlockingTest {
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||
} is DecryptionResult.Success
|
||||
|
||||
val canDecryptSecond =
|
||||
testHelper.runBlockingTest {
|
||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||
} is DecryptionResult.Success
|
||||
canDecryptFirst && canDecryptSecond
|
||||
}
|
||||
}
|
||||
|
@@ -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.UserPasswordAuth
|
||||
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.events.model.EventType
|
||||
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.common.CommonTestHelper.Companion.runCryptoTest
|
||||
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.store.db.deserializeFromRealm
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
|
||||
@@ -226,13 +226,10 @@ class UnwedgingTest : InstrumentedTest {
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
// we should get back the key and be able to decrypt
|
||||
val result = testHelper.runBlockingTest {
|
||||
tryOrNull {
|
||||
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
|
||||
}
|
||||
}
|
||||
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
|
||||
result != null
|
||||
val canDecrypt = testHelper.runBlockingTest {
|
||||
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
|
||||
} is DecryptionResult.Success
|
||||
canDecrypt
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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.SessionTestParams
|
||||
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)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@@ -95,9 +95,10 @@ class KeyShareTests : InstrumentedTest {
|
||||
assert(receivedEvent!!.isEncrypted())
|
||||
|
||||
commonTestHelper.runBlockingTest {
|
||||
mustFail {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
}
|
||||
assertTrue(
|
||||
"Should not be able to decrypt",
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") is DecryptionResult.Failure
|
||||
)
|
||||
}
|
||||
|
||||
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
@@ -166,9 +167,10 @@ class KeyShareTests : InstrumentedTest {
|
||||
}
|
||||
|
||||
commonTestHelper.runBlockingTest {
|
||||
mustFail {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
}
|
||||
assertTrue(
|
||||
"Should not be able to decrypt",
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") is DecryptionResult.Failure
|
||||
)
|
||||
}
|
||||
|
||||
// Mark the device as trusted
|
||||
|
@@ -20,6 +20,7 @@ import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -27,7 +28,6 @@ import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
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.RequestResult
|
||||
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.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.mustFail
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@@ -96,17 +95,16 @@ class WithHeldTests : InstrumentedTest {
|
||||
// Bob should not be able to decrypt because the keys is withheld
|
||||
// .. might need to wait a bit for stability?
|
||||
testHelper.runBlockingTest {
|
||||
mustFail(
|
||||
message = "This session should not be able to decrypt",
|
||||
failureBlock = { failure ->
|
||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "").fold(
|
||||
{
|
||||
fail("This session should not be able to decrypt")
|
||||
},
|
||||
{ failure, code ->
|
||||
val type = (failure as MXCryptoError.Base).errorType
|
||||
val technicalMessage = failure.technicalMessage
|
||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
|
||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, code?.value)
|
||||
}
|
||||
) {
|
||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// .. might need to wait a bit for stability?
|
||||
testHelper.runBlockingTest {
|
||||
mustFail(
|
||||
message = "This session should not be able to decrypt",
|
||||
failureBlock = { failure ->
|
||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "").fold(
|
||||
{
|
||||
fail("This session should not be able to decrypt")
|
||||
},
|
||||
{ failure, code ->
|
||||
val type = (failure as MXCryptoError.Base).errorType
|
||||
val technicalMessage = failure.technicalMessage
|
||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
|
||||
}) {
|
||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||
}
|
||||
Assert.assertEquals("Error should be partially withheld", MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, code?.value)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,16 +185,16 @@ class WithHeldTests : InstrumentedTest {
|
||||
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
|
||||
// .. might need to wait a bit for stability?
|
||||
testHelper.runBlockingTest {
|
||||
mustFail(
|
||||
message = "This session should not be able to decrypt",
|
||||
failureBlock = { failure ->
|
||||
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "").fold(
|
||||
{
|
||||
fail("This session should not be able to decrypt")
|
||||
},
|
||||
{ failure, code ->
|
||||
val type = (failure as MXCryptoError.Base).errorType
|
||||
val technicalMessage = failure.technicalMessage
|
||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage)
|
||||
}) {
|
||||
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
|
||||
}
|
||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, code?.value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Ensure that alice has marked the session to be shared with bob
|
||||
@@ -262,10 +260,8 @@ class WithHeldTests : InstrumentedTest {
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
|
||||
// try to decrypt and force key request
|
||||
tryOrNull {
|
||||
testHelper.runBlockingTest {
|
||||
bobSecondSession.cryptoService().decryptEvent(it.root, "")
|
||||
}
|
||||
testHelper.runBlockingTest {
|
||||
bobSecondSession.cryptoService().decryptEvent(it.root, "")
|
||||
}
|
||||
}
|
||||
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
|
||||
|
@@ -17,7 +17,6 @@
|
||||
package org.matrix.android.sdk.internal.crypto.replayattack
|
||||
|
||||
import androidx.test.filters.LargeTest
|
||||
import org.amshove.kluent.internal.assertFailsWith
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.fail
|
||||
@@ -64,11 +63,15 @@ class ReplayAttackTest : InstrumentedTest {
|
||||
// Lets decrypt the original event
|
||||
aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
|
||||
// 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)
|
||||
}
|
||||
assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType)
|
||||
|
||||
aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId).fold(
|
||||
{
|
||||
fail("Decryption should have fail because of duplicate index")
|
||||
},
|
||||
{ err, _ ->
|
||||
assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, (err as MXCryptoError.Base).errorType)
|
||||
}
|
||||
)
|
||||
}
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
@@ -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.MXDeviceInfo
|
||||
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.verification.VerificationService
|
||||
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.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
|
||||
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
|
||||
|
||||
interface CryptoService {
|
||||
@@ -145,10 +145,9 @@ interface CryptoService {
|
||||
|
||||
fun discardOutboundSession(roomId: String)
|
||||
|
||||
@Throws(MXCryptoError::class)
|
||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
||||
suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult
|
||||
|
||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>)
|
||||
suspend fun decryptAndUpdateEvent(event: Event, timeline: String)
|
||||
|
||||
fun getEncryptionAlgorithm(roomId: String): String?
|
||||
|
||||
|
@@ -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.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.DecryptionResult
|
||||
import org.matrix.olm.OlmException
|
||||
|
||||
/**
|
||||
@@ -60,7 +61,7 @@ sealed class MXCryptoError : Throwable() {
|
||||
OLM,
|
||||
UNKNOWN_DEVICES,
|
||||
UNKNOWN_MESSAGE_INDEX,
|
||||
KEYS_WITHHELD
|
||||
// KEYS_WITHHELD
|
||||
}
|
||||
|
||||
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." +
|
||||
" Perhaps the homeserver is hiding the configuration event."
|
||||
}
|
||||
|
||||
fun toResult() = DecryptionResult.Failure(this)
|
||||
}
|
||||
|
@@ -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.model.OlmDecryptionResult
|
||||
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.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||
@@ -99,6 +100,13 @@ data class Event(
|
||||
@Transient
|
||||
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
|
||||
var sendState: SendState = SendState.UNKNOWN
|
||||
|
||||
@@ -131,6 +139,7 @@ data class Event(
|
||||
it.mxDecryptionResult = mxDecryptionResult
|
||||
it.mCryptoError = mCryptoError
|
||||
it.mCryptoErrorReason = mCryptoErrorReason
|
||||
it.mCryptoWithHeldCode = mCryptoWithHeldCode
|
||||
it.sendState = sendState
|
||||
it.ageLocalTs = ageLocalTs
|
||||
it.threadDetails = threadDetails
|
||||
@@ -278,6 +287,7 @@ data class Event(
|
||||
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
||||
if (mCryptoError != other.mCryptoError) return false
|
||||
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
|
||||
if (mCryptoWithHeldCode != other.mCryptoWithHeldCode) return false
|
||||
if (sendState != other.sendState) return false
|
||||
if (threadDetails != other.threadDetails) return false
|
||||
return true
|
||||
@@ -297,6 +307,7 @@ data class Event(
|
||||
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
||||
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
||||
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
|
||||
result = 31 * result + (mCryptoWithHeldCode?.hashCode() ?: 0)
|
||||
result = 31 * result + sendState.hashCode()
|
||||
result = 31 * result + threadDetails.hashCode()
|
||||
|
||||
|
@@ -61,7 +61,12 @@ data class TimelineEvent(
|
||||
val ownedByThreadChunk: Boolean = false,
|
||||
val senderInfo: SenderInfo,
|
||||
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 {
|
||||
|
@@ -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.MXDeviceInfo
|
||||
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.OlmDecryptionResult
|
||||
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.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.internal.crypto.actions.MegolmSessionDataImporter
|
||||
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.IMXGroupEncryption
|
||||
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.
|
||||
* @return the MXEventDecryptionResult data, or throw in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
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 {
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult {
|
||||
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.
|
||||
*
|
||||
|
@@ -16,18 +16,16 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||
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.model.MXEventDecryptionResult
|
||||
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.EventType
|
||||
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.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
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.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import timber.log.Timber
|
||||
@@ -49,7 +47,6 @@ private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO)
|
||||
|
||||
@SessionScope
|
||||
internal class EventDecryptor @Inject constructor(
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val clock: Clock,
|
||||
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.
|
||||
* @return the MXEventDecryptionResult data, or throw in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult {
|
||||
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
|
||||
*/
|
||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||
// is it needed to do that on the crypto scope??
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
runCatching {
|
||||
internalDecryptEvent(event, timeline)
|
||||
}.foldToCallback(callback)
|
||||
suspend fun decryptAndUpdate(event: Event, timeline: String) {
|
||||
val result = internalDecryptEvent(event, timeline)
|
||||
when (result) {
|
||||
is DecryptionResult.Success -> {
|
||||
val decryptionResult = result.decryptedResult
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = decryptionResult.clearEvent,
|
||||
senderKey = decryptionResult.senderCurve25519Key,
|
||||
keysClaimed = decryptionResult.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
|
||||
)
|
||||
}
|
||||
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.
|
||||
* @return the MXEventDecryptionResult data, or null in case of error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): DecryptionResult {
|
||||
val eventContent = event.content
|
||||
if (eventContent == null) {
|
||||
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()) {
|
||||
// we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm
|
||||
return MXEventDecryptionResult(
|
||||
clearEvent = mapOf(
|
||||
"room_id" to event.roomId.orEmpty(),
|
||||
"type" to EventType.MESSAGE,
|
||||
"content" to emptyMap<String, Any>(),
|
||||
"unsigned" to event.unsignedData.toContent()
|
||||
return DecryptionResult.Success(
|
||||
MXEventDecryptionResult(
|
||||
clearEvent = mapOf(
|
||||
"room_id" to event.roomId.orEmpty(),
|
||||
"type" to EventType.MESSAGE,
|
||||
"content" to emptyMap<String, Any>(),
|
||||
"unsigned" to event.unsignedData.toContent()
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
@@ -130,25 +132,27 @@ internal class EventDecryptor @Inject constructor(
|
||||
if (alg == null) {
|
||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
||||
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 {
|
||||
try {
|
||||
return alg.decryptEvent(event, timeline)
|
||||
} catch (mxCryptoError: MXCryptoError) {
|
||||
Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
||||
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
||||
if (mxCryptoError is MXCryptoError.Base &&
|
||||
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
||||
// need to find sending device
|
||||
val olmContent = event.content.toModel<OlmEventContent>()
|
||||
if (event.senderId != null && olmContent?.senderKey != null) {
|
||||
markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
|
||||
return alg.decryptEvent(event, timeline).also {
|
||||
if (it is DecryptionResult.Failure) {
|
||||
val mxCryptoError = it.error
|
||||
Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
||||
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
||||
if (mxCryptoError is MXCryptoError.Base &&
|
||||
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
||||
// need to find sending device
|
||||
val olmContent = event.content.toModel<OlmEventContent>()
|
||||
if (event.senderId != null && olmContent?.senderKey != null) {
|
||||
markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw mxCryptoError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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.model.MXEventDecryptionResult
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
@@ -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.
|
||||
* @return the decryption information, or an error
|
||||
*/
|
||||
@Throws(MXCryptoError::class)
|
||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
||||
suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult
|
||||
|
||||
/**
|
||||
* Handle a key event.
|
||||
|
@@ -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.internal.crypto.MXOlmDevice
|
||||
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.keysbackup.DefaultKeysBackupService
|
||||
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()
|
||||
|
||||
@Throws(MXCryptoError::class)
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult {
|
||||
return decryptEvent(event, timeline, true)
|
||||
}
|
||||
|
||||
@Throws(MXCryptoError::class)
|
||||
private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
|
||||
private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): DecryptionResult {
|
||||
Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
|
||||
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>()
|
||||
?: 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() ||
|
||||
encryptedEventContent.sessionId.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 {
|
||||
olmDevice.decryptGroupMessage(
|
||||
try {
|
||||
val olmDecryptionResult = olmDevice.decryptGroupMessage(
|
||||
encryptedEventContent.ciphertext,
|
||||
event.roomId,
|
||||
timeline,
|
||||
@@ -84,85 +89,73 @@ internal class MXMegolmDecryption(
|
||||
encryptedEventContent.sessionId,
|
||||
encryptedEventContent.senderKey
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
{ olmDecryptionResult ->
|
||||
// the decryption succeeds
|
||||
if (olmDecryptionResult.payload != null) {
|
||||
MXEventDecryptionResult(
|
||||
clearEvent = olmDecryptionResult.payload,
|
||||
senderCurve25519Key = olmDecryptionResult.senderKey,
|
||||
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
|
||||
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
|
||||
.orEmpty()
|
||||
).also {
|
||||
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
|
||||
}
|
||||
} else {
|
||||
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
|
||||
}
|
||||
return if (olmDecryptionResult.payload != null) {
|
||||
MXEventDecryptionResult(
|
||||
clearEvent = olmDecryptionResult.payload,
|
||||
senderCurve25519Key = olmDecryptionResult.senderKey,
|
||||
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
|
||||
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
|
||||
.orEmpty()
|
||||
).also {
|
||||
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
|
||||
}.let {
|
||||
DecryptionResult.Success(it)
|
||||
}
|
||||
} else {
|
||||
return DecryptionResult.Failure(
|
||||
MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||
)
|
||||
}
|
||||
} 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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.JsonDict
|
||||
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.toResult
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.util.convertFromUTF8
|
||||
import timber.log.Timber
|
||||
@@ -42,36 +44,36 @@ internal class MXOlmDecryption(
|
||||
) :
|
||||
IMXDecrypting {
|
||||
|
||||
@Throws(MXCryptoError::class)
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format")
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
|
||||
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON
|
||||
)
|
||||
}
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): DecryptionResult {
|
||||
val olmEventContent = event.content.toModel<OlmEventContent>()
|
||||
?: return MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
|
||||
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON
|
||||
).toResult().also {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format")
|
||||
}
|
||||
|
||||
val cipherText = olmEventContent.ciphertext ?: run {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text")
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
|
||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON
|
||||
)
|
||||
}
|
||||
val cipherText = olmEventContent.ciphertext
|
||||
?: return MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
|
||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON
|
||||
).toResult().also {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text")
|
||||
}
|
||||
|
||||
val senderKey = olmEventContent.senderKey ?: run {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key")
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.MISSING_SENDER_KEY,
|
||||
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON
|
||||
)
|
||||
}
|
||||
val senderKey = olmEventContent.senderKey
|
||||
?: return MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.MISSING_SENDER_KEY,
|
||||
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON
|
||||
).toResult().also {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key")
|
||||
}
|
||||
|
||||
val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)
|
||||
}
|
||||
val messageAny = cipherText[olmDevice.deviceCurve25519Key]
|
||||
?: return 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
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -81,8 +83,9 @@ internal class MXOlmDecryption(
|
||||
|
||||
if (decryptedPayload == null) {
|
||||
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 adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
@@ -90,92 +93,97 @@ internal class MXOlmDecryption(
|
||||
|
||||
if (payload == null) {
|
||||
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 {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
||||
}
|
||||
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString)
|
||||
?: return 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()) {
|
||||
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent() : $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
|
||||
return MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
|
||||
.toResult().also {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent() : $reason")
|
||||
}
|
||||
}
|
||||
|
||||
if (olmPayloadContent.recipient != userId) {
|
||||
Timber.tag(loggerTag.value).e(
|
||||
"## decryptEvent() : Event ${event.eventId}:" +
|
||||
" Intended recipient ${olmPayloadContent.recipient} does not match our id $userId"
|
||||
)
|
||||
throw MXCryptoError.Base(
|
||||
return MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.BAD_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 {
|
||||
Timber.tag(loggerTag.value).e(
|
||||
"## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
|
||||
" property; cannot prevent unknown-key attack"
|
||||
)
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.MISSING_PROPERTY,
|
||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")
|
||||
)
|
||||
}
|
||||
val recipientKeys = olmPayloadContent.recipientKeys
|
||||
?: return MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.MISSING_PROPERTY,
|
||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")
|
||||
).toResult().also {
|
||||
Timber.tag(loggerTag.value).e(
|
||||
"## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
|
||||
" property; cannot prevent unknown-key attack"
|
||||
)
|
||||
}
|
||||
|
||||
val ed25519 = recipientKeys["ed25519"]
|
||||
|
||||
if (ed25519 != olmDevice.deviceEd25519Key) {
|
||||
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.BAD_RECIPIENT_KEY_REASON
|
||||
)
|
||||
).toResult()
|
||||
}
|
||||
|
||||
if (olmPayloadContent.sender.isNullOrBlank()) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.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,
|
||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")
|
||||
)
|
||||
).toResult()
|
||||
}
|
||||
|
||||
if (olmPayloadContent.sender != event.senderId) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
|
||||
throw MXCryptoError.Base(
|
||||
return MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.FORWARDED_MESSAGE,
|
||||
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) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.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,
|
||||
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId)
|
||||
)
|
||||
).toResult()
|
||||
}
|
||||
|
||||
val keys = olmPayloadContent.keys ?: run {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys")
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
|
||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON
|
||||
)
|
||||
}
|
||||
val keys = olmPayloadContent.keys
|
||||
?: return MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
|
||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON
|
||||
).toResult().also {
|
||||
Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys")
|
||||
}
|
||||
|
||||
return MXEventDecryptionResult(
|
||||
clearEvent = payload,
|
||||
senderCurve25519Key = senderKey,
|
||||
claimedEd25519Key = keys["ed25519"]
|
||||
)
|
||||
).toResult()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1260,6 +1260,9 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
if (newState == OutgoingRoomKeyRequestState.UNSENT) {
|
||||
// clear the old replies
|
||||
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 sessionId = withHeldContent.sessionId ?: return
|
||||
if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return
|
||||
Timber.tag(loggerTag.value).v(
|
||||
"WithHeldMegolmSession set for $roomId|$sessionId is ${withHeldContent.code}"
|
||||
)
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let {
|
||||
it.code = withHeldContent.code
|
||||
@@ -1679,6 +1685,10 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
senderKey = it.senderKey
|
||||
)
|
||||
}
|
||||
}.also {
|
||||
Timber.tag(loggerTag.value).v(
|
||||
"WithHeldMegolmSession get for $roomId|$sessionId is ${it?.code}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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.MigrateSessionTo034
|
||||
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.database.MatrixRealmMigration
|
||||
import javax.inject.Inject
|
||||
@@ -60,7 +61,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
||||
private val normalizer: Normalizer
|
||||
) : MatrixRealmMigration(
|
||||
dbName = "Session",
|
||||
schemaVersion = 35L,
|
||||
schemaVersion = 36L,
|
||||
) {
|
||||
/**
|
||||
* Forces all RealmSessionStoreMigration instances to be equal.
|
||||
@@ -105,5 +106,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
||||
if (oldVersion < 33) MigrateSessionTo033(realm).perform()
|
||||
if (oldVersion < 34) MigrateSessionTo034(realm).perform()
|
||||
if (oldVersion < 35) MigrateSessionTo035(realm).perform()
|
||||
if (oldVersion < 36) MigrateSessionTo036(realm).perform()
|
||||
}
|
||||
}
|
||||
|
@@ -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.threads.model.ThreadSummary
|
||||
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.toEntity
|
||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
||||
@@ -219,23 +220,26 @@ private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEnt
|
||||
cryptoService ?: return
|
||||
val event = eventEntity.asDomain()
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) {
|
||||
try {
|
||||
Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
|
||||
// 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
|
||||
val result = runBlocking { 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
|
||||
)
|
||||
// Save decryption result, to not decrypt every time we enter the thread list
|
||||
eventEntity.setDecryptionResult(result)
|
||||
} catch (e: MXCryptoError) {
|
||||
if (e is MXCryptoError.Base) {
|
||||
event.mCryptoError = e.errorType
|
||||
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||
Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
|
||||
// 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
|
||||
val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), "") }
|
||||
when (result) {
|
||||
is DecryptionResult.Success -> {
|
||||
val decryptedResult = result.decryptedResult
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = decryptedResult.clearEvent,
|
||||
senderKey = decryptedResult.senderCurve25519Key,
|
||||
keysClaimed = decryptedResult.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = decryptedResult.forwardingCurve25519KeyChain
|
||||
)
|
||||
// Save decryption result, to not decrypt every time we enter the thread list
|
||||
eventEntity.setDecryptionResult(decryptedResult)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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.EventType
|
||||
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.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||
@@ -56,6 +57,7 @@ internal object EventMapper {
|
||||
}
|
||||
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
|
||||
eventEntity.decryptionErrorCode = event.mCryptoError?.name
|
||||
eventEntity.decryptionWithheldCode = event.mCryptoWithHeldCode?.value
|
||||
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
|
||||
eventEntity.rootThreadEventId = event.getRootThreadEventId()
|
||||
eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
|
||||
@@ -90,18 +92,23 @@ internal object EventMapper {
|
||||
it.ageLocalTs = eventEntity.ageLocalTs
|
||||
it.sendState = eventEntity.sendState
|
||||
it.sendStateDetails = eventEntity.sendStateDetails
|
||||
eventEntity.decryptionResultJson?.let { json ->
|
||||
|
||||
val decryptionJson = eventEntity.decryptionResultJson
|
||||
if (decryptionJson != null) {
|
||||
try {
|
||||
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
|
||||
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(decryptionJson)
|
||||
} catch (t: JsonDataException) {
|
||||
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(
|
||||
isRootThread = eventEntity.isRootThread,
|
||||
isThread = if (it.threadDetails?.isThread == true) true else eventEntity.isThread(),
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -79,6 +79,11 @@ internal open class EventEntity(
|
||||
if (value != field) field = value
|
||||
}
|
||||
|
||||
var decryptionWithheldCode: String? = null
|
||||
set(value) {
|
||||
if (value != field) field = value
|
||||
}
|
||||
|
||||
companion object
|
||||
|
||||
fun setDecryptionResult(result: MXEventDecryptionResult) {
|
||||
@@ -93,6 +98,7 @@ internal open class EventEntity(
|
||||
decryptionResultJson = adapter.toJson(decryptionResult)
|
||||
decryptionErrorCode = null
|
||||
decryptionErrorReason = null
|
||||
decryptionWithheldCode = null
|
||||
|
||||
// If we have an EventInsertEntity for the eventId we make sures it can be processed now.
|
||||
realm.where(EventInsertEntity::class.java)
|
||||
|
@@ -17,8 +17,6 @@ package org.matrix.android.sdk.internal.session.room.relation.threads
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
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.EventType
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
realm: Realm,
|
||||
event: Event,
|
||||
|
@@ -20,7 +20,7 @@ import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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.content.EncryptionEventContent
|
||||
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.RoomSyncUnreadNotifications
|
||||
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.database.mapper.ContentMapper
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
@@ -80,7 +81,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||
val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId)
|
||||
if (roomSummaryEntity != null) {
|
||||
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]")
|
||||
|
||||
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 lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs
|
||||
if (lastActivityFromEvent != null) {
|
||||
roomSummaryEntity.lastActivityTime = lastActivityFromEvent
|
||||
latestPreviewableEvent.attemptToDecrypt()
|
||||
latestPreviewableEvent.attemptToDecrypt(false)
|
||||
}
|
||||
|
||||
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) {
|
||||
null -> {
|
||||
Timber.v("Decryption skipped due to missing root event $eventId")
|
||||
}
|
||||
else -> {
|
||||
if (root.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
|
||||
Timber.v("Should decrypt $eventId")
|
||||
tryOrNull {
|
||||
runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") }
|
||||
}?.let { root.setDecryptionResult(it) }
|
||||
if (root.type == EventType.ENCRYPTED) {
|
||||
if (force || (root.decryptionResultJson == null && root.decryptionErrorCode == null)) {
|
||||
Timber.v("Should decrypt $eventId")
|
||||
when (val res = runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") }) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -58,6 +58,7 @@ internal class DefaultTimeline(
|
||||
private val roomId: String,
|
||||
private val initialEventId: String?,
|
||||
private val realmConfiguration: RealmConfiguration,
|
||||
cryptoRealmConfiguration: RealmConfiguration,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val readReceiptHandler: ReadReceiptHandler,
|
||||
private val settings: TimelineSettings,
|
||||
@@ -101,6 +102,7 @@ internal class DefaultTimeline(
|
||||
eventDecryptor = eventDecryptor,
|
||||
paginationTask = paginationTask,
|
||||
realmConfiguration = realmConfiguration,
|
||||
cryptoRealmConfiguration = cryptoRealmConfiguration,
|
||||
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
|
||||
fetchThreadTimelineTask = fetchThreadTimelineTask,
|
||||
getContextOfEventTask = getEventTask,
|
||||
|
@@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.realm.RealmConfiguration
|
||||
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.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.util.Optional
|
||||
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.session.room.membership.LoadRoomMembersTask
|
||||
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(
|
||||
@Assisted private val roomId: String,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
@CryptoDatabase private val cryptoRealmConfiguration: RealmConfiguration,
|
||||
private val timelineInput: TimelineInput,
|
||||
private val contextOfEventTask: GetContextOfEventTask,
|
||||
private val eventDecryptor: TimelineEventDecryptor,
|
||||
@@ -68,6 +71,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
||||
initialEventId = eventId,
|
||||
settings = settings,
|
||||
realmConfiguration = monarchy.realmConfiguration,
|
||||
cryptoRealmConfiguration = cryptoRealmConfiguration,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
paginationTask = paginationTask,
|
||||
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
|
||||
|
@@ -16,8 +16,6 @@
|
||||
|
||||
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.internal.crypto.EventDecryptor
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
@@ -48,17 +46,7 @@ internal class DefaultGetEventTask @Inject constructor(
|
||||
|
||||
// Try to decrypt the Event
|
||||
if (event.isEncrypted()) {
|
||||
tryOrNull(message = "Unable to decrypt the 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
|
||||
)
|
||||
}
|
||||
eventDecryptor.decryptAndUpdate(event, "")
|
||||
}
|
||||
|
||||
event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0)
|
||||
|
@@ -90,6 +90,7 @@ internal class LoadTimelineStrategy constructor(
|
||||
data class Dependencies(
|
||||
val timelineSettings: TimelineSettings,
|
||||
val realm: AtomicReference<Realm>,
|
||||
val cryptoRealmConfiguration: RealmConfiguration,
|
||||
val eventDecryptor: TimelineEventDecryptor,
|
||||
val paginationTask: PaginationTask,
|
||||
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 {
|
||||
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(
|
||||
roomId = roomId,
|
||||
realm = dependencies.realm,
|
||||
@@ -176,11 +177,20 @@ internal class LoadTimelineStrategy constructor(
|
||||
dependencies.matrixCoroutineDispatchers.main
|
||||
)
|
||||
|
||||
private val cryptoInfoEventDecorator = CryptoInfoEventDecorator(
|
||||
realmConfiguration = dependencies.cryptoRealmConfiguration,
|
||||
roomId = roomId,
|
||||
clock = clock,
|
||||
rebuildListener = rebuildEventListener,
|
||||
onEventsUpdated = dependencies.onEventsUpdated
|
||||
)
|
||||
|
||||
suspend fun onStart() {
|
||||
dependencies.eventDecryptor.start()
|
||||
dependencies.timelineInput.listeners.add(timelineInputListener)
|
||||
val realm = dependencies.realm.get()
|
||||
sendingEventsDataSource.start()
|
||||
cryptoInfoEventDecorator.start()
|
||||
chunkEntity = getChunkEntity(realm).also {
|
||||
it.addChangeListener(chunkEntityListener)
|
||||
timelineChunk = it.createTimelineChunk()
|
||||
@@ -196,6 +206,7 @@ internal class LoadTimelineStrategy constructor(
|
||||
dependencies.timelineInput.listeners.remove(timelineInputListener)
|
||||
chunkEntity?.removeChangeListener(chunkEntityListener)
|
||||
sendingEventsDataSource.stop()
|
||||
cryptoInfoEventDecorator.stop()
|
||||
timelineChunk?.close(closeNext = true, closePrev = true)
|
||||
getContextLatch?.cancel()
|
||||
chunkEntity = null
|
||||
@@ -336,6 +347,7 @@ internal class LoadTimelineStrategy constructor(
|
||||
fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask,
|
||||
timelineEventMapper = dependencies.timelineEventMapper,
|
||||
uiEchoManager = uiEchoManager,
|
||||
cryptoInfoEventDecorator = cryptoInfoEventDecorator,
|
||||
threadsAwarenessHandler = dependencies.threadsAwarenessHandler,
|
||||
lightweightSettingsStorage = dependencies.lightweightSettingsStorage,
|
||||
initialEventId = mode.originEventId(),
|
||||
|
@@ -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
|
||||
}
|
@@ -61,6 +61,7 @@ internal class TimelineChunk(
|
||||
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val uiEchoManager: UIEchoManager?,
|
||||
private val cryptoInfoEventDecorator: CryptoInfoEventDecorator?,
|
||||
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
|
||||
private val lightweightSettingsStorage: LightweightSettingsStorage,
|
||||
private val initialEventId: String?,
|
||||
@@ -414,6 +415,8 @@ internal class TimelineChunk(
|
||||
).let {
|
||||
// eventually enhance with ui echo?
|
||||
(uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it)
|
||||
}.let {
|
||||
(cryptoInfoEventDecorator?.decorateTimelineEvent(it) ?: it)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -576,6 +579,7 @@ internal class TimelineChunk(
|
||||
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
|
||||
timelineEventMapper = timelineEventMapper,
|
||||
uiEchoManager = uiEchoManager,
|
||||
cryptoInfoEventDecorator = cryptoInfoEventDecorator,
|
||||
threadsAwarenessHandler = threadsAwarenessHandler,
|
||||
lightweightSettingsStorage = lightweightSettingsStorage,
|
||||
initialEventId = null,
|
||||
|
@@ -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.content.EncryptedEventContent
|
||||
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.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
@@ -129,29 +130,30 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||
threadAwareNonEncryptedEvents(request, realm)
|
||||
return
|
||||
}
|
||||
try {
|
||||
// note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching
|
||||
val result = runBlocking { cryptoService.decryptEvent(request.event, timelineId) }
|
||||
Timber.v("Successfully decrypted event ${event.eventId}")
|
||||
realm.executeTransaction {
|
||||
val eventId = event.eventId ?: return@executeTransaction
|
||||
val eventEntity = EventEntity
|
||||
.where(it, eventId = eventId)
|
||||
.findFirst()
|
||||
eventEntity?.setDecryptionResult(result)
|
||||
val decryptedEvent = eventEntity?.asDomain()
|
||||
threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
|
||||
// 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) }) {
|
||||
is DecryptionResult.Success -> {
|
||||
Timber.v("Successfully decrypted event ${event.eventId}")
|
||||
realm.executeTransaction {
|
||||
val eventId = event.eventId ?: return@executeTransaction
|
||||
val eventEntity = EventEntity
|
||||
.where(it, eventId = eventId)
|
||||
.findFirst()
|
||||
eventEntity?.setDecryptionResult(result.decryptedResult)
|
||||
val decryptedEvent = eventEntity?.asDomain()
|
||||
threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
|
||||
}
|
||||
}
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}")
|
||||
if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) {
|
||||
// Keep track of unknown sessions to automatically try to decrypt on new session
|
||||
is DecryptionResult.Failure -> {
|
||||
val error = result.error
|
||||
Timber.v("Failed to decrypt event ${event.eventId} : ${error.localizedMessage}")
|
||||
realm.executeTransaction {
|
||||
EventEntity.where(it, eventId = event.eventId ?: "")
|
||||
.findFirst()
|
||||
?.let {
|
||||
it.decryptionErrorCode = e.errorType.name
|
||||
it.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||
it.decryptionErrorCode = (error as? MXCryptoError.Base)?.errorType?.name ?: MXCryptoError.ErrorType.UNABLE_TO_DECRYPT.name
|
||||
it.decryptionErrorReason = (error as? MXCryptoError.Base)?.technicalMessage ?: error.message
|
||||
it.decryptionWithheldCode = result.withheldInfo?.value
|
||||
}
|
||||
}
|
||||
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}")
|
||||
} finally {
|
||||
synchronized(existingRequests) {
|
||||
existingRequests.remove(request)
|
||||
}
|
||||
}
|
||||
synchronized(existingRequests) {
|
||||
existingRequests.remove(request)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -29,14 +29,10 @@ import timber.log.Timber
|
||||
import java.util.Collections
|
||||
|
||||
internal class UIEchoManager(
|
||||
private val listener: Listener,
|
||||
private val listener: RebuildListener,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
|
||||
interface Listener {
|
||||
fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean
|
||||
}
|
||||
|
||||
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||
|
||||
fun getInMemorySendingEvents(): List<TimelineEvent> {
|
||||
|
@@ -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.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.events.model.Event
|
||||
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.ToDeviceSyncResponse
|
||||
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.session.sync.ProgressReporter
|
||||
import timber.log.Timber
|
||||
@@ -68,40 +68,33 @@ internal class CryptoSyncHandler @Inject constructor(
|
||||
* @param timelineId the timeline identifier
|
||||
* @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")
|
||||
if (event.getClearType() == EventType.ENCRYPTED) {
|
||||
var result: MXEventDecryptionResult? = null
|
||||
try {
|
||||
result = cryptoService.decryptEvent(event, timelineId ?: "")
|
||||
} catch (exception: MXCryptoError) {
|
||||
event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType // setCryptoError(exception.cryptoError)
|
||||
val senderKey = event.content.toModel<OlmEventContent>()?.senderKey ?: "<unknown sender key>"
|
||||
// try to find device id to ease log reading
|
||||
val deviceId = cryptoService.getCryptoDeviceInfo(event.senderId!!).firstOrNull {
|
||||
it.identityKey() == senderKey
|
||||
}?.deviceId ?: senderKey
|
||||
Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>")
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "## CRYPTO | Failed to decrypt to device event from ${event.senderId}")
|
||||
}
|
||||
|
||||
if (null != result) {
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
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}")
|
||||
when (val result = cryptoService.decryptEvent(event, timelineId ?: "")) {
|
||||
is DecryptionResult.Success -> {
|
||||
val decryptionResult = result.decryptedResult
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = decryptionResult.clearEvent,
|
||||
senderKey = decryptionResult.senderCurve25519Key,
|
||||
keysClaimed = decryptionResult.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
|
||||
)
|
||||
}
|
||||
is DecryptionResult.Failure -> {
|
||||
val exception = result.error
|
||||
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>"
|
||||
// try to find device id to ease log reading
|
||||
val deviceId = event.senderId?.let {
|
||||
cryptoService.getCryptoDeviceInfo(it).firstOrNull {
|
||||
it.identityKey() == senderKey
|
||||
}?.deviceId
|
||||
} ?: senderKey
|
||||
Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,6 @@ import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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.events.model.Event
|
||||
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) {
|
||||
try {
|
||||
if (event.mxDecryptionResult == null) {
|
||||
val timelineId = generateTimelineId(roomId)
|
||||
// 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
|
||||
val result = runBlocking { cryptoService.decryptEvent(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
|
||||
}
|
||||
runBlocking { cryptoService.decryptAndUpdateEvent(event.copy(roomId = roomId), timelineId) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -146,8 +146,7 @@ class DecryptionFailureTracker @Inject constructor(
|
||||
private fun MXCryptoError.ErrorType.toAnalyticsErrorName(): DetailedErrorName {
|
||||
val detailed = "$name | mxc_crypto_error_type"
|
||||
val errorName = when (this) {
|
||||
MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID,
|
||||
MXCryptoError.ErrorType.KEYS_WITHHELD -> Error.Name.OlmKeysNotSentError
|
||||
MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID -> Error.Name.OlmKeysNotSentError
|
||||
MXCryptoError.ErrorType.OLM -> Error.Name.OlmUnspecifiedError
|
||||
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX -> Error.Name.OlmIndexError
|
||||
else -> Error.Name.UnknownError
|
||||
|
@@ -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.raw.RawService
|
||||
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.LocalEcho
|
||||
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.isTextMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
@@ -1111,13 +1109,7 @@ class TimelineViewModel @AssistedInject constructor(
|
||||
|
||||
private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) {
|
||||
room.getTimelineEvent(action.eventId)?.let {
|
||||
val code = when (it.root.mCryptoError) {
|
||||
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
|
||||
WithHeldCode.fromCode(it.root.mCryptoErrorReason)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
val code = it.root.mCryptoWithHeldCode
|
||||
_viewEvents.post(RoomDetailViewEvents.ShowE2EErrorMessage(code))
|
||||
}
|
||||
}
|
||||
|
@@ -420,6 +420,11 @@ class MessageActionsViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
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))
|
||||
if (session.myUserId != timelineEvent.root.senderId) {
|
||||
|
@@ -29,11 +29,8 @@ import im.vector.app.core.platform.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
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.getRoom
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
class ViewEditHistoryViewModel @AssistedInject constructor(
|
||||
@@ -76,18 +73,7 @@ class ViewEditHistoryViewModel @AssistedInject constructor(
|
||||
val timelineID = event.roomId + UUID.randomUUID().toString()
|
||||
// We need to check encryption
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
// for now decrypt sync
|
||||
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")
|
||||
}
|
||||
session.cryptoService().decryptAndUpdateEvent(event, timelineID)
|
||||
}
|
||||
|
||||
if (event.eventId == eventId) {
|
||||
|
@@ -30,9 +30,9 @@ import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import me.gujun.android.span.image
|
||||
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.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -56,16 +56,18 @@ class EncryptedItemFactory @Inject constructor(
|
||||
val cryptoError = event.root.mCryptoError
|
||||
|
||||
val spannableStr = if (vectorPreferences.developerMode()) {
|
||||
val errorDescription =
|
||||
if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
stringProvider.getString(R.string.notice_crypto_error_unknown_inbound_session_id)
|
||||
} else {
|
||||
// TODO i18n
|
||||
cryptoError?.name
|
||||
}
|
||||
// In developer mode we want to show the raw error
|
||||
val errorDescription = if (event.root.mCryptoWithHeldCode != null) {
|
||||
"${cryptoError?.name ?: "NULL"} | ${event.root.mCryptoWithHeldCode?.value}"
|
||||
} else {
|
||||
cryptoError?.name ?: "NULL"
|
||||
}
|
||||
|
||||
val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null }
|
||||
?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
|
||||
val message = stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription).let {
|
||||
if (event.hasActiveRequestForKeys == true) {
|
||||
"$it - key request in progress"
|
||||
} else it
|
||||
}
|
||||
span(message) {
|
||||
textStyle = "italic"
|
||||
textColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
|
||||
@@ -78,29 +80,38 @@ class EncryptedItemFactory @Inject constructor(
|
||||
textColor = colorFromAttribute
|
||||
}
|
||||
} else {
|
||||
when (cryptoError) {
|
||||
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
|
||||
span {
|
||||
drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
|
||||
image(it, "baseline")
|
||||
+" "
|
||||
}
|
||||
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) {
|
||||
textStyle = "italic"
|
||||
textColor = colorFromAttribute
|
||||
}
|
||||
if (event.hasActiveRequestForKeys == true) {
|
||||
span {
|
||||
drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
|
||||
image(it, "baseline")
|
||||
+" "
|
||||
}
|
||||
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_requesting_keys)) {
|
||||
textStyle = "italic"
|
||||
textColor = colorFromAttribute
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
span {
|
||||
drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
|
||||
image(it, "baseline")
|
||||
+" "
|
||||
}
|
||||
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) {
|
||||
textStyle = "italic"
|
||||
textColor = colorFromAttribute
|
||||
}
|
||||
} else if (event.root.mCryptoWithHeldCode != null) {
|
||||
val messageId = when (event.root.mCryptoWithHeldCode) {
|
||||
WithHeldCode.BLACKLISTED -> R.string.crypto_error_withheld_blacklisted
|
||||
WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified
|
||||
else -> R.string.crypto_error_withheld_generic
|
||||
}
|
||||
span {
|
||||
drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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.session.Session
|
||||
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.EventType
|
||||
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.util.toMatrixItem
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -207,16 +204,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||
if (root.isEncrypted() && root.mxDecryptionResult == null) {
|
||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
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) {
|
||||
}
|
||||
session.cryptoService().decryptAndUpdateEvent(root, "")
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -125,7 +125,8 @@
|
||||
<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_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 -->
|
||||
|
||||
@@ -2673,8 +2674,11 @@
|
||||
<string name="room_settings_save_success">You changed room settings successfully</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_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="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>
|
||||
|
Reference in New Issue
Block a user