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

Compare commits

...

6 Commits

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

View File

@@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
import org.matrix.android.sdk.api.session.crypto.keysbackup.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")
}
}
}

View File

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

View File

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

View File

@@ -29,7 +29,6 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.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
}
}

View File

@@ -46,7 +46,7 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.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

View File

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

View File

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

View File

@@ -34,12 +34,12 @@ import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.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?

View File

@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.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)
}

View File

@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.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()

View File

@@ -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 {

View File

@@ -57,8 +57,8 @@ import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.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.
*

View File

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

View File

@@ -19,8 +19,27 @@ package org.matrix.android.sdk.internal.crypto.algorithms
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.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.

View File

@@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.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
)
}
}
/**

View File

@@ -27,7 +27,9 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
import org.matrix.android.sdk.api.util.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()
}
/**

View File

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

View File

@@ -52,6 +52,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
import org.matrix.android.sdk.internal.database.migration.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()
}
}

View File

@@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.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
}
}
}

View File

@@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.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(),

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo036(realm: DynamicRealm) : RealmMigrator(realm, 36) {
override fun doMigrate(realm: DynamicRealm) {
// add initial withheld code as part as the event entity
realm.schema.get("EventEntity")
?.addField(EventEntityFields.DECRYPTION_WITHHELD_CODE, String::class.java)
?.transform { dynamicObject ->
// previously the withheld code was stored in mCryptoErrorReason when the error type
// was KEYS_WITHHELD but now we are storing it properly for both type UNKNOWN_INBOUND_SESSION_ID and UNKNOWN_MESSAGE_INDEX
val reasonString = dynamicObject.getString(EventEntityFields.DECRYPTION_ERROR_REASON)
val errorCode = dynamicObject.getString(EventEntityFields.DECRYPTION_ERROR_CODE)
// try to see if it's a withheld code
val tryAsCode = WithHeldCode.fromCode(reasonString)
if (errorCode != null && tryAsCode != null) {
// ok migrate to proper code
dynamicObject.setString(EventEntityFields.DECRYPTION_WITHHELD_CODE, tryAsCode.value)
// the former code was WITHHELD, we can't know if it was unknown session or partially withheld, so assume unknown
// we could try to see if session is known but is it worth it?
dynamicObject.setString(EventEntityFields.DECRYPTION_WITHHELD_CODE, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID.name)
}
}
}
}

View File

@@ -79,6 +79,11 @@ internal open class EventEntity(
if (value != field) field = value
}
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)

View File

@@ -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,

View File

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

View File

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

View File

@@ -58,6 +58,7 @@ internal class DefaultTimeline(
private val roomId: String,
private val 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,

View File

@@ -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,

View File

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

View File

@@ -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(),

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
interface RebuildListener {
fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean
}

View File

@@ -61,6 +61,7 @@ internal class TimelineChunk(
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val 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,

View File

@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.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)
}
}

View File

@@ -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> {

View File

@@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.sync.handler
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.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
}
}

View File

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

View File

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

View File

@@ -80,11 +80,9 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.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))
}
}

View File

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

View File

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

View File

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

View File

@@ -27,8 +27,6 @@ import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormat
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.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, "")
}
}

View File

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