diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 1c912b365f..cbe4cca8a3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -88,7 +88,10 @@ class CommonTestHelper(context: Context) { fun syncSession(session: Session) { val lock = CountDownLatch(1) - GlobalScope.launch(Dispatchers.Main) { session.open() } + val job = GlobalScope.launch(Dispatchers.Main) { + session.open() + } + runBlocking { job.join() } session.startSync(true) @@ -341,7 +344,7 @@ class CommonTestHelper(context: Context) { } // Transform a method with a MatrixCallback to a synchronous method - inline fun doSync(block: (MatrixCallback) -> Unit): T { + inline fun doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback) -> Unit): T { val lock = CountDownLatch(1) var result: T? = null @@ -354,7 +357,7 @@ class CommonTestHelper(context: Context) { block.invoke(callback) - await(lock) + await(lock, timeout) assertNotNull(result) return result!! @@ -366,8 +369,9 @@ class CommonTestHelper(context: Context) { fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) } fun signOutAndClose(session: Session) { - doSync { session.signOut(true, it) } - session.close() + doSync(60_000) { session.signOut(true, it) } + // no need signout will close + // session.close() } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index ec2b28a4da..b67172a908 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -32,9 +32,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.api.session.room.timeline.Timeline -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData @@ -197,47 +194,16 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! - val lock = CountDownLatch(1) - - val bobEventsListener = object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - // noop - } - - override fun onNewTimelineEvents(eventIds: List) { - // noop - } - - override fun onTimelineUpdated(snapshot: List) { - val messages = snapshot.filter { it.root.getClearType() == EventType.MESSAGE } - .groupBy { it.root.senderId!! } - - // Alice has sent 2 messages and Bob has sent 3 messages - if (messages[aliceSession.myUserId]?.size == 2 && messages[bobSession.myUserId]?.size == 3) { - lock.countDown() - } - } - } - - val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20)) - bobTimeline.start() - bobTimeline.addListener(bobEventsListener) - // Alice sends a message - roomFromAlicePOV.sendTextMessage(messagesFromAlice[0]) + mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1) +// roomFromAlicePOV.sendTextMessage(messagesFromAlice[0]) // Bob send 3 messages - roomFromBobPOV.sendTextMessage(messagesFromBob[0]) - roomFromBobPOV.sendTextMessage(messagesFromBob[1]) - roomFromBobPOV.sendTextMessage(messagesFromBob[2]) - + mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1) + mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1) + mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1) // Alice sends a message - roomFromAlicePOV.sendTextMessage(messagesFromAlice[1]) - - mTestHelper.await(lock) - - bobTimeline.removeListener(bobEventsListener) - bobTimeline.dispose() + mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1) return cryptoTestData } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 121d9fb401..34be1b8d05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto import android.content.Context import androidx.lifecycle.LiveData +import androidx.paging.PagedList import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService @@ -40,6 +41,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import kotlin.jvm.Throws interface CryptoService { @@ -142,10 +144,13 @@ interface CryptoService { fun removeSessionListener(listener: NewSessionListener) fun getOutgoingRoomKeyRequests(): List + fun getOutgoingRoomKeyRequestsPaged(): LiveData> fun getIncomingRoomKeyRequests(): List + fun getIncomingRoomKeyRequestsPaged(): LiveData> - fun getGossipingEventsTrail(): List + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEvents(): List // For testing shared session fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt new file mode 100644 index 0000000000..e8a70615e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 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.crypto + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.query.whereType +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.util.fetchCopied +import javax.inject.Inject + +/** + * The crypto module needs some information regarding rooms that are stored + * in the session DB, this class encapsulate this functionality + */ +internal class CryptoSessionInfoProvider @Inject constructor( + @SessionDatabase private val monarchy: Monarchy +) { + + fun isRoomEncrypted(roomId: String): Boolean { + val encryptionEvent = monarchy.fetchCopied { realm -> + EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) + .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") + .isNotNull(EventEntityFields.STATE_KEY) // should be an empty key + .findFirst() + } + return encryptionEvent != null + } + + /** + * @param allActive if true return joined as well as invited, if false, only joined + */ + fun getRoomUserIds(roomId: String, allActive: Boolean): List { + var userIds: List = emptyList() + monarchy.doWithRealm { realm -> + userIds = if (allActive) { + RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() + } else { + RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds() + } + } + return userIds + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index b78afe6d41..326eac8f91 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -17,12 +17,10 @@ package org.matrix.android.sdk.internal.crypto import android.content.Context -import android.os.Handler -import android.os.Looper import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData +import androidx.paging.PagedList import com.squareup.moshi.Types -import com.zhuinden.monarchy.Monarchy import dagger.Lazy import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -51,9 +49,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension @@ -68,7 +64,6 @@ import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent -import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent @@ -82,21 +77,15 @@ import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.EventEntityFields -import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask -import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskThread @@ -104,11 +93,11 @@ import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.util.fetchCopied import org.matrix.olm.OlmManager import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject +import kotlin.jvm.Throws import kotlin.math.max /** @@ -171,28 +160,16 @@ internal class DefaultCryptoService @Inject constructor( private val setDeviceNameTask: SetDeviceNameTask, private val uploadKeysTask: UploadKeysTask, private val loadRoomMembersTask: LoadRoomMembersTask, - @SessionDatabase private val monarchy: Monarchy, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val taskExecutor: TaskExecutor, private val cryptoCoroutineScope: CoroutineScope, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val sendToDeviceTask: SendToDeviceTask, - private val messageEncrypter: MessageEncrypter + private val eventDecryptor: EventDecryptor ) : CryptoService { - init { - verificationService.cryptoService = this - } - - private val uiHandler = Handler(Looper.getMainLooper()) - private val isStarting = AtomicBoolean(false) private val isStarted = AtomicBoolean(false) - // The date of the last time we forced establishment - // of a new session for each user:device. - private val lastNewSessionForcedDates = MXUsersDevicesMap() - fun onStateEvent(roomId: String, event: Event) { when (event.getClearType()) { EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) @@ -209,6 +186,8 @@ internal class DefaultCryptoService @Inject constructor( } } + val gossipingBuffer = mutableListOf() + override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { setDeviceNameTask .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { @@ -410,7 +389,7 @@ internal class DefaultCryptoService @Inject constructor( */ fun close() = runBlocking(coroutineDispatchers.crypto) { cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) - + incomingGossipingRequestManager.close() olmDevice.release() cryptoStore.close() } @@ -452,6 +431,13 @@ internal class DefaultCryptoService @Inject constructor( incomingGossipingRequestManager.processReceivedGossipingRequests() } } + + tryOrNull { + gossipingBuffer.toList().let { + cryptoStore.saveGossipingEvents(it) + } + gossipingBuffer.clear() + } } } @@ -612,13 +598,7 @@ internal class DefaultCryptoService @Inject constructor( * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM */ override fun isRoomEncrypted(roomId: String): Boolean { - val encryptionEvent = monarchy.fetchCopied { realm -> - EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) - .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") - .isNotNull(EventEntityFields.STATE_KEY) - .findFirst() - } - return encryptionEvent != null + return cryptoSessionInfoProvider.isRoomEncrypted(roomId) } /** @@ -660,11 +640,8 @@ internal class DefaultCryptoService @Inject constructor( eventType: String, roomId: String, callback: MatrixCallback) { + // moved to crypto scope to have uptodate values cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { -// if (!isStarted()) { -// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init") -// internalStart(false) -// } val userIds = getRoomUserIds(roomId) var alg = roomEncryptorsStore.get(roomId) if (alg == null) { @@ -720,14 +697,7 @@ internal class DefaultCryptoService @Inject constructor( * @param callback the callback to return data or null */ override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { - cryptoCoroutineScope.launch { - val result = runCatching { - withContext(coroutineDispatchers.crypto) { - internalDecryptEvent(event, timeline) - } - } - result.foldToCallback(callback) - } + eventDecryptor.decryptEventAsync(event, timeline, callback) } /** @@ -739,42 +709,7 @@ internal class DefaultCryptoService @Inject constructor( */ @Throws(MXCryptoError::class) private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - val eventContent = event.content - if (eventContent == null) { - Timber.e("## CRYPTO | decryptEvent : empty event content") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) - } else { - val algorithm = eventContent["algorithm"]?.toString() - val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) - if (alg == null) { - val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) - Timber.e("## CRYPTO | decryptEvent() : $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) - } else { - try { - return alg.decryptEvent(event, timeline) - } catch (mxCryptoError: MXCryptoError) { - Timber.d("## CRYPTO | 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() - cryptoStore.getUserDevices(event.senderId ?: "") - ?.values - ?.firstOrNull { it.identityKey() == olmContent?.senderKey } - ?.let { - markOlmSessionForUnwedging(event.senderId ?: "", it) - } - ?: run { - Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device") - } - } - } - throw mxCryptoError - } - } - } + return eventDecryptor.decryptEvent(event, timeline) } /** @@ -796,19 +731,19 @@ internal class DefaultCryptoService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { when (event.getClearType()) { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { - cryptoStore.saveGossipingEvent(event) + gossipingBuffer.add(event) // Keys are imported directly, not waiting for end of sync onRoomKeyEvent(event) } EventType.REQUEST_SECRET, EventType.ROOM_KEY_REQUEST -> { // save audit trail - cryptoStore.saveGossipingEvent(event) + gossipingBuffer.add(event) // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete) incomingGossipingRequestManager.onGossipingRequestEvent(event) } EventType.SEND_SECRET -> { - cryptoStore.saveGossipingEvent(event) + gossipingBuffer.add(event) onSecretSendReceived(event) } EventType.ROOM_KEY_WITHHELD -> { @@ -828,7 +763,7 @@ internal class DefaultCryptoService @Inject constructor( */ private fun onRoomKeyEvent(event: Event) { val roomKeyContent = event.getClearContent().toModel() ?: return - Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>") + Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields") return @@ -935,19 +870,9 @@ internal class DefaultCryptoService @Inject constructor( } private fun getRoomUserIds(roomId: String): List { - var userIds: List = emptyList() - monarchy.doWithRealm { realm -> - // Check whether the event content must be encrypted for the invited members. - val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() - && shouldEncryptForInvitedMembers(roomId) - - userIds = if (encryptForInvitedMembers) { - RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() - } else { - RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds() - } - } - return userIds + val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() + && shouldEncryptForInvitedMembers(roomId) + return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers) } /** @@ -1257,38 +1182,38 @@ internal class DefaultCryptoService @Inject constructor( incomingGossipingRequestManager.removeRoomKeysRequestListener(listener) } - private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { - val deviceKey = deviceInfo.identityKey() - - val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 - val now = System.currentTimeMillis() - if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { - Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") - return - } - - Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") - lastNewSessionForcedDates.setObject(senderId, deviceKey, now) - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) - - // Now send a blank message on that session so the other side knows about it. - // (The keyshare request is sent in the clear so that won't do) - // We send this first such that, as long as the toDevice messages arrive in the - // same order we sent them, the other end will get this first, set up the new session, - // then get the keyshare request and send the key over this new session (because it - // is the session it has most recently received a message on). - val payloadJson = mapOf("type" to EventType.DUMMY) - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) - Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - sendToDeviceTask.execute(sendToDeviceParams) - } - } +// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { +// val deviceKey = deviceInfo.identityKey() +// +// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 +// val now = System.currentTimeMillis() +// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { +// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") +// return +// } +// +// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") +// lastNewSessionForcedDates.setObject(senderId, deviceKey, now) +// +// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) +// +// // Now send a blank message on that session so the other side knows about it. +// // (The keyshare request is sent in the clear so that won't do) +// // We send this first such that, as long as the toDevice messages arrive in the +// // same order we sent them, the other end will get this first, set up the new session, +// // then get the keyshare request and send the key over this new session (because it +// // is the session it has most recently received a message on). +// val payloadJson = mapOf("type" to EventType.DUMMY) +// +// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) +// val sendToDeviceMap = MXUsersDevicesMap() +// sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) +// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") +// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) +// sendToDeviceTask.execute(sendToDeviceParams) +// } +// } /** * Provides the list of unknown devices @@ -1339,14 +1264,26 @@ internal class DefaultCryptoService @Inject constructor( return cryptoStore.getOutgoingRoomKeyRequests() } + override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { + return cryptoStore.getOutgoingRoomKeyRequestsPaged() + } + + override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + return cryptoStore.getIncomingRoomKeyRequestsPaged() + } + override fun getIncomingRoomKeyRequests(): List { return cryptoStore.getIncomingRoomKeyRequests() } - override fun getGossipingEventsTrail(): List { + override fun getGossipingEventsTrail(): LiveData> { return cryptoStore.getGossipingEventsTrail() } + override fun getGossipingEvents(): List { + return cryptoStore.getGossipingEvents() + } + override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { return cryptoStore.getSharedWithInfo(roomId, sessionId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index ab30d3052d..42df6b354b 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -377,7 +377,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } // Update devices trust for these users - dispatchDeviceChange(downloadUsers) + // dispatchDeviceChange(downloadUsers) return onKeysDownloadSucceed(filteredUsers, response.failures) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt new file mode 100644 index 0000000000..38488f1ca7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2020 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.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +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.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.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent +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.MatrixCoroutineDispatchers +import timber.log.Timber +import javax.inject.Inject +import kotlin.jvm.Throws + +@SessionScope +internal class EventDecryptor @Inject constructor( + private val cryptoCoroutineScope: CoroutineScope, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val roomDecryptorProvider: RoomDecryptorProvider, + private val messageEncrypter: MessageEncrypter, + private val sendToDeviceTask: SendToDeviceTask, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val cryptoStore: IMXCryptoStore +) { + + // The date of the last time we forced establishment + // of a new session for each user:device. + private val lastNewSessionForcedDates = MXUsersDevicesMap() + + /** + * 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 throw in case of error + */ + @Throws(MXCryptoError::class) + 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 + */ + fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { + // is it needed to do that on the crypto scope?? + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { + internalDecryptEvent(event, timeline) + }.foldToCallback(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 fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + val eventContent = event.content + if (eventContent == null) { + Timber.e("## CRYPTO | decryptEvent : empty event content") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) + } else { + val algorithm = eventContent["algorithm"]?.toString() + val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) + if (alg == null) { + val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) + Timber.e("## CRYPTO | decryptEvent() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) + } else { + try { + return alg.decryptEvent(event, timeline) + } catch (mxCryptoError: MXCryptoError) { + Timber.d("## CRYPTO | 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 + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + val olmContent = event.content.toModel() + cryptoStore.getUserDevices(event.senderId ?: "") + ?.values + ?.firstOrNull { it.identityKey() == olmContent?.senderKey } + ?.let { + markOlmSessionForUnwedging(event.senderId ?: "", it) + } + ?: run { + Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device") + } + } + } + } + throw mxCryptoError + } + } + } + } + + // coroutineDispatchers.crypto scope + private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { + val deviceKey = deviceInfo.identityKey() + + val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 + val now = System.currentTimeMillis() + if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { + Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") + return + } + + Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") + lastNewSessionForcedDates.setObject(senderId, deviceKey, now) + + // offload this from crypto thread (?) + cryptoCoroutineScope.launch(coroutineDispatchers.computation) { + ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) + + // Now send a blank message on that session so the other side knows about it. + // (The keyshare request is sent in the clear so that won't do) + // We send this first such that, as long as the toDevice messages arrive in the + // same order we sent them, the other end will get this first, set up the new session, + // then get the keyshare request and send the key over this new session (because it + // is the session it has most recently received a message on). + val payloadJson = mapOf("type" to EventType.DUMMY) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) + Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") + withContext(coroutineDispatchers.io) { + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + sendToDeviceTask.execute(sendToDeviceParams) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt new file mode 100644 index 0000000000..06c667ee4a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2020 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.crypto + +import android.util.LruCache +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import timber.log.Timber +import java.util.Timer +import java.util.TimerTask +import javax.inject.Inject + +/** + * Allows to cache and batch store operations on inbound group session store. + * Because it is used in the decrypt flow, that can be called quite rapidly + */ +internal class InboundGroupSessionStore @Inject constructor( + private val store: IMXCryptoStore, + private val cryptoCoroutineScope: CoroutineScope, + private val coroutineDispatchers: MatrixCoroutineDispatchers) { + + private data class CacheKey( + val sessionId: String, + val senderKey: String + ) + + private val sessionCache = object : LruCache(30) { + override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) { + if (evicted && oldValue != null) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}") + store.storeInboundGroupSessions(listOf(oldValue)) + } + } + } + } + + private val timer = Timer() + private var timerTask: TimerTask? = null + + private val dirtySession = mutableListOf() + + @Synchronized + fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? { + synchronized(sessionCache) { + val known = sessionCache[CacheKey(sessionId, senderKey)] + Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}") + return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also { + Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}") + sessionCache.put(CacheKey(sessionId, senderKey), it) + } + } + } + + @Synchronized + fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2) { + Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}") + // We want to batch this a bit for performances + dirtySession.add(wrapper) + + timerTask?.cancel() + timerTask = object : TimerTask() { + override fun run() { + batchSave() + } + } + timer.schedule(timerTask!!, 2_000) + } + + @Synchronized + private fun batchSave() { + val toSave = mutableListOf().apply { addAll(dirtySession) } + dirtySession.clear() + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}") + tryOrNull { + store.storeInboundGroupSessions(toSave) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt index 8869e73432..97ae0b9d83 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber +import java.util.concurrent.Executors import javax.inject.Inject @SessionScope @@ -52,6 +53,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope) { + private val executor = Executors.newSingleThreadExecutor() // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations // we received in the current sync. private val receivedGossipingRequests = ArrayList() @@ -64,6 +66,10 @@ internal class IncomingGossipingRequestManager @Inject constructor( receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests()) } + fun close() { + executor.shutdownNow() + } + // Recently verified devices (map of deviceId and timestamp) private val recentlyVerifiedDevices = HashMap() @@ -99,7 +105,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( fun onGossipingRequestEvent(event: Event) { Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}") val roomKeyShare = event.getClearContent().toModel() - val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + // val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } when (roomKeyShare?.action) { GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { if (event.getClearType() == EventType.REQUEST_SECRET) { @@ -108,8 +114,8 @@ internal class IncomingGossipingRequestManager @Inject constructor( // ignore, it was sent by me as * Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") } else { - // save in DB - cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) +// // save in DB +// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) receivedGossipingRequests.add(it) } } @@ -119,7 +125,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( // ignore, it was sent by me as * Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") } else { - cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) +// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) receivedGossipingRequests.add(it) } } @@ -144,13 +150,8 @@ internal class IncomingGossipingRequestManager @Inject constructor( fun processReceivedGossipingRequests() { val roomKeyRequestsToProcess = receivedGossipingRequests.toList() receivedGossipingRequests.clear() - for (request in roomKeyRequestsToProcess) { - if (request is IncomingRoomKeyRequest) { - processIncomingRoomKeyRequest(request) - } else if (request is IncomingSecretShareRequest) { - processIncomingSecretShareRequest(request) - } - } + + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process") var receivedRequestCancellations: List? = null @@ -161,24 +162,35 @@ internal class IncomingGossipingRequestManager @Inject constructor( } } - receivedRequestCancellations?.forEach { request -> - Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request") - // we should probably only notify the app of cancellations we told it - // about, but we don't currently have a record of that, so we just pass - // everything through. - if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) { - // ignore remote echo - return@forEach + executor.execute { + cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess) + for (request in roomKeyRequestsToProcess) { + if (request is IncomingRoomKeyRequest) { + processIncomingRoomKeyRequest(request) + } else if (request is IncomingSecretShareRequest) { + processIncomingSecretShareRequest(request) + } } - val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "") - if (matchingIncoming == null) { - // ignore that? - return@forEach - } else { - // If it was accepted from this device, keep the information, do not mark as cancelled - if (matchingIncoming.state != GossipingRequestState.ACCEPTED) { - onRoomKeyRequestCancellation(request) - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER) + + receivedRequestCancellations?.forEach { request -> + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request") + // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) { + // ignore remote echo + return@forEach + } + val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "") + if (matchingIncoming == null) { + // ignore that? + return@forEach + } else { + // If it was accepted from this device, keep the information, do not mark as cancelled + if (matchingIncoming.state != GossipingRequestState.ACCEPTED) { + onRoomKeyRequestCancellation(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER) + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 7a546993b8..1a4d1136c8 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -44,7 +44,9 @@ internal class MXOlmDevice @Inject constructor( /** * The store where crypto data is saved. */ - private val store: IMXCryptoStore) { + private val store: IMXCryptoStore, + private val inboundGroupSessionStore: InboundGroupSessionStore + ) { /** * @return the Curve25519 key for the account. @@ -657,7 +659,7 @@ internal class MXOlmDevice @Inject constructor( timelineSet.add(messageIndexKey) } - store.storeInboundGroupSessions(listOf(session)) + inboundGroupSessionStore.storeInBoundGroupSession(session) val payload = try { val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) @@ -745,7 +747,7 @@ internal class MXOlmDevice @Inject constructor( throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) } - val session = store.getInboundGroupSession(sessionId, senderKey) + val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey) if (session != null) { // Check that the room id matches the original one for the session. This stops diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt index efda663230..c86f2be0a3 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt @@ -88,7 +88,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor( * @param requestBody requestBody */ fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.computation) { cancelRoomKeyRequest(requestBody, false) } } @@ -99,7 +99,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor( * @param requestBody requestBody */ fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.computation) { cancelRoomKeyRequest(requestBody, true) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 466722788d..e55cf37118 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -16,10 +16,13 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.session.crypto.MXCryptoError 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.EventType import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM @@ -39,6 +42,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.convertToUTF8 import timber.log.Timber @@ -54,7 +58,9 @@ internal class MXMegolmEncryption( private val sendToDeviceTask: SendToDeviceTask, private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope ) : IMXEncrypting { // OutboundSessionInfo. Null if we haven't yet started setting one up. Note @@ -84,15 +90,18 @@ internal class MXMegolmEncryption( } private fun notifyWithheldForSession(devices: MXUsersDevicesMap, outboundSession: MXOutboundSessionInfo) { - mutableListOf>().apply { - devices.forEach { userId, deviceId, withheldCode -> - this.add(UserDevice(userId, deviceId) to withheldCode) + // offload to computation thread + cryptoCoroutineScope.launch(coroutineDispatchers.computation) { + mutableListOf>().apply { + devices.forEach { userId, deviceId, withheldCode -> + this.add(UserDevice(userId, deviceId) to withheldCode) + } + }.groupBy( + { it.second }, + { it.first } + ).forEach { (code, targets) -> + notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code) } - }.groupBy( - { it.second }, - { it.first } - ).forEach { (code, targets) -> - notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code) } } @@ -247,6 +256,15 @@ internal class MXMegolmEncryption( for ((userId, devicesToShareWith) in devicesByUser) { for ((deviceId) in devicesToShareWith) { session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex) + cryptoStore.saveGossipingEvent(Event( + type = EventType.ROOM_KEY, + senderId = credentials.userId, + content = submap.apply { + this["session_key"] = "" + // we add a fake key for trail + this["_dest"] = "$userId|$deviceId" + } + )) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt index 8f651692fc..f0cc15fb63 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm +import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice @@ -26,6 +27,7 @@ import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepo import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject internal class MXMegolmEncryptionFactory @Inject constructor( @@ -38,7 +40,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor( private val sendToDeviceTask: SendToDeviceTask, private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - private val taskExecutor: TaskExecutor) { + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope) { fun create(roomId: String): MXMegolmEncryption { return MXMegolmEncryption( @@ -52,7 +56,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor( sendToDeviceTask, messageEncrypter, warnOnUnknownDevicesRepository, - taskExecutor + taskExecutor, + coroutineDispatchers, + cryptoCoroutineScope ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index b5056a0efd..1871dba0e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.internal.crypto.crosssigning import androidx.lifecycle.LiveData +import androidx.work.BackoffPolicy +import androidx.work.ExistingWorkPolicy import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService @@ -39,15 +41,20 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.withoutPrefix import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmUtility import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Inject @SessionScope internal class DefaultCrossSigningService @Inject constructor( @UserId private val userId: String, + @SessionId private val sessionId: String, private val cryptoStore: IMXCryptoStore, private val deviceListManager: DeviceListManager, private val initializeCrossSigningTask: InitializeCrossSigningTask, @@ -55,7 +62,7 @@ internal class DefaultCrossSigningService @Inject constructor( private val taskExecutor: TaskExecutor, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope, - private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { + private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { private var olmUtility: OlmUtility? = null @@ -360,6 +367,12 @@ internal class DefaultCrossSigningService @Inject constructor( // First let's get my user key val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId)) + + return UserTrustResult.Success + } + + fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult { val myUserKey = myCrossSigningInfo?.userKey() ?: return UserTrustResult.CrossSigningNotConfigured(userId) @@ -368,15 +381,15 @@ internal class DefaultCrossSigningService @Inject constructor( } // Let's get the other user master key - val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey() - ?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId) + val otherMasterKey = otherInfo?.masterKey() + ?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "") val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures ?.get(userId) // Signatures made by me ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey") + Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey") return UserTrustResult.KeyNotSigned(otherMasterKey) } @@ -396,6 +409,15 @@ internal class DefaultCrossSigningService @Inject constructor( // and that MSK is trusted (i know the private key, or is signed by a trusted device) val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId)) + } + + fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List?): UserTrustResult { + // Special case when it's me, + // I have to check that MSK -> USK -> SSK + // and that MSK is trusted (i know the private key, or is signed by a trusted device) +// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + val myMasterKey = myCrossSigningInfo?.masterKey() ?: return UserTrustResult.CrossSigningNotConfigured(userId) @@ -423,7 +445,7 @@ internal class DefaultCrossSigningService @Inject constructor( // Maybe it's signed by a locally trusted device? myMasterKey.signatures?.get(userId)?.forEach { (key, value) -> val potentialDeviceId = key.withoutPrefix("ed25519:") - val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId) + val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId) if (potentialDevice != null && potentialDevice.isVerified) { // Check signature validity? try { @@ -561,6 +583,8 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoStore.markMyMasterKeyAsLocallyTrusted(true) checkSelfTrust() + // re-verify all trusts + onUsersDeviceUpdate(listOf(userId)) } } @@ -666,6 +690,55 @@ internal class DefaultCrossSigningService @Inject constructor( return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) } + fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo) : DeviceTrustResult { + val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified() + myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + + if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) + + otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId)) + + // TODO should we force verification ? + if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys)) + + // Check if the trust chain is valid + /* + * ┏━━━━━━━━┓ ┏━━━━━━━━┓ + * ┃ ALICE ┃ ┃ BOB ┃ + * ┗━━━━━━━━┛ ┗━━━━━━━━┛ + * MSK ┌────────────▶MSK + * │ + * │ │ │ + * │ SSK │ └──▶ SSK ──────────────────┐ + * │ │ │ + * │ │ USK │ + * └──▶ USK ────────────┘ (not visible by │ + * Alice) │ + * ▼ + * ┌──────────────┐ + * │ BOB's Device │ + * └──────────────┘ + */ + + val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") + ?: return legacyFallbackTrust( + locallyTrusted, + DeviceTrustResult.MissingDeviceSignature(otherDevice.deviceId, otherKeys.selfSigningKey() + ?.unpaddedBase64PublicKey + ?: "" + ) + ) + + // Check bob's device is signed by bob's SSK + try { + olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable()) + } catch (e: Throwable) { + return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e)) + } + + return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) + } + private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult { return if (locallyTrusted == true) { DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true)) @@ -675,36 +748,18 @@ internal class DefaultCrossSigningService @Inject constructor( } override fun onUsersDeviceUpdate(userIds: List) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users") - userIds.forEach { otherUserId -> - checkUserTrust(otherUserId).let { - Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") - setUserKeysAsTrusted(otherUserId, it.isVerified()) - } - } - } + Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds") + val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds) + val workerData = WorkerParamsFactory.toData(workerParams) - // now check device trust - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - userIds.forEach { otherUserId -> - // TODO if my keys have changes, i should recheck all devices of all users? - val devices = cryptoStore.getUserDeviceList(otherUserId) - devices?.forEach { device -> - val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") - cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) - } + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setInputData(workerData) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) + .build() - if (otherUserId == userId) { - // It's me, i should check if a newly trusted device is signing my master key - // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust - setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified()) - } - } - - eventBus.post(CryptoToSessionUserTrustChange(userIds)) - } + workManagerProvider.workManager + .beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) + .enqueue() } private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt deleted file mode 100644 index 271b9e52d3..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020 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.crypto.crosssigning - -data class SessionToCryptoRoomMembersUpdate( - val roomId: String, - val isDirect: Boolean, - val userIds: List -) - -data class CryptoToSessionUserTrustChange( - val userIds: List -) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt deleted file mode 100644 index 05ceba5965..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2020 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.crypto.crosssigning - -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity -import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.session.SessionLifecycleObserver -import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater -import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.util.createBackgroundHandler -import io.realm.Realm -import io.realm.RealmConfiguration -import kotlinx.coroutines.android.asCoroutineDispatcher -import kotlinx.coroutines.launch -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import timber.log.Timber -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import javax.inject.Inject - -internal class ShieldTrustUpdater @Inject constructor( - private val eventBus: EventBus, - private val computeTrustTask: ComputeTrustTask, - private val taskExecutor: TaskExecutor, - @SessionDatabase private val sessionRealmConfiguration: RealmConfiguration, - private val roomSummaryUpdater: RoomSummaryUpdater -) : SessionLifecycleObserver { - - companion object { - private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD") - private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher() - } - - private val backgroundSessionRealm = AtomicReference() - - private val isStarted = AtomicBoolean() - - override fun onStart() { - if (isStarted.compareAndSet(false, true)) { - eventBus.register(this) - BACKGROUND_HANDLER.post { - backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration)) - } - } - } - - override fun onStop() { - if (isStarted.compareAndSet(true, false)) { - eventBus.unregister(this) - BACKGROUND_HANDLER.post { - backgroundSessionRealm.getAndSet(null).also { - it?.close() - } - } - } - } - - @Subscribe - fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) { - if (!isStarted.get()) { - return - } - taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) { - val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect)) - // We need to send that back to session base - backgroundSessionRealm.get()?.executeTransaction { realm -> - roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust) - } - } - } - - @Subscribe - fun onTrustUpdate(update: CryptoToSessionUserTrustChange) { - if (!isStarted.get()) { - return - } - onCryptoDevicesChange(update.userIds) - } - - private fun onCryptoDevicesChange(users: List) { - taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) { - val realm = backgroundSessionRealm.get() ?: return@launch - val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java) - .`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray()) - .distinct(RoomMemberSummaryEntityFields.ROOM_ID) - .findAll() - .map { it.roomId } - - distinctRoomIds.forEach { roomId -> - val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - if (roomSummary?.isEncrypted.orFalse()) { - val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() - try { - val updatedTrust = computeTrustTask.execute( - ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true) - ) - realm.executeTransaction { - roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust) - } - } catch (failure: Throwable) { - Timber.e(failure) - } - } - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt new file mode 100644 index 0000000000..f28fe7d642 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -0,0 +1,322 @@ +/* + * Copyright 2020 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.crypto.crosssigning + +import android.content.Context +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.where +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import timber.log.Timber +import javax.inject.Inject + +internal class UpdateTrustWorker(context: Context, + params: WorkerParameters) + : SessionSafeCoroutineWorker(context, params, Params::class.java) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + override val lastFailureMessage: String? = null, + val updatedUserIds: List + ) : SessionWorkerParams + + @Inject lateinit var crossSigningService: DefaultCrossSigningService + + // It breaks the crypto store contract, but we need to batch things :/ + @CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration + @UserId @Inject lateinit var myUserId: String + @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper + @SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration + + // @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater + @Inject lateinit var cryptoStore: IMXCryptoStore + + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + + override suspend fun doSafeWork(params: Params): Result { + var userList = params.updatedUserIds + // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user, + // or a new device?) So we check all again :/ + + Timber.d("## CrossSigning - Updating trust for $userList") + + // First we check that the users MSK are trusted by mine + // After that we check the trust chain for each devices of each users + Realm.getInstance(realmConfiguration).use { realm -> + realm.executeTransaction { + // By mapping here to model, this object is not live + // I should update it if needed + var myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId) + .findFirst()?.let { mapCrossSigningInfoEntity(it) } + + var myTrustResult: UserTrustResult? = null + + if (userList.contains(myUserId)) { + Timber.d("## CrossSigning - Clear all trust as a change on my user was detected") + // i am in the list.. but i don't know exactly the delta of change :/ + // If it's my cross signing keys we should refresh all trust + // do it anyway ? + userList = realm.where(CrossSigningInfoEntity::class.java) + .findAll().mapNotNull { it.userId } + Timber.d("## CrossSigning - Updating trust for all $userList") + + // check right now my keys and mark it as trusted as other trust depends on it + val myDevices = realm.where() + .equalTo(UserEntityFields.USER_ID, myUserId) + .findFirst() + ?.devices + ?.map { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices).also { + updateCrossSigningKeysTrust(realm, myUserId, it.isVerified()) + // update model reference + myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId) + .findFirst()?.let { mapCrossSigningInfoEntity(it) } + } + } + + val otherInfos = userList.map { + it to realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, it) + .findFirst()?.let { mapCrossSigningInfoEntity(it) } + } + .toMap() + + val trusts = otherInfos.map { infoEntry -> + infoEntry.key to when (infoEntry.key) { + myUserId -> myTrustResult + else -> { + crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, infoEntry.value).also { + Timber.d("## CrossSigning - user:${infoEntry.key} result:$it") + } + } + } + }.toMap() + + // TODO! if it's me and my keys has changed... I have to reset trust for everyone! + // i have all the new trusts, update DB + trusts.forEach { + val verified = it.value?.isVerified() == true + updateCrossSigningKeysTrust(realm, it.key, verified) + } + + // Ok so now we have to check device trust for all these users.. + Timber.v("## CrossSigning - Updating devices cross trust users ${trusts.keys}") + trusts.keys.forEach { + val devicesEntities = realm.where() + .equalTo(UserEntityFields.USER_ID, it) + .findFirst() + ?.devices + + val trustMap = devicesEntities?.map { device -> + // get up to date from DB has could have been updated + val otherInfo = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, it) + .findFirst()?.let { mapCrossSigningInfoEntity(it) } + device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device)) + }?.toMap() + + // Update trust if needed + devicesEntities?.forEach { device -> + val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified() + Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}") + if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) { + Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified") + // need to save + val trustEntity = device.trustLevelEntity + if (trustEntity == null) { + realm.createObject(TrustLevelEntity::class.java).let { + it.locallyVerified = false + it.crossSignedVerified = crossSignedVerified + device.trustLevelEntity = it + } + } else { + trustEntity.crossSignedVerified = crossSignedVerified + } + } + } + } + } + } + + // So Cross Signing keys trust is updated, device trust is updated + // We can now update room shields? in the session DB? + + Timber.d("## CrossSigning - Updating shields for impacted rooms...") + Realm.getInstance(sessionRealmConfiguration).use { it -> + it.executeTransaction { realm -> + val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java) + .`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray()) + .distinct(RoomMemberSummaryEntityFields.ROOM_ID) + .findAll() + .map { it.roomId } + Timber.d("## CrossSigning - ... impacted rooms $distinctRoomIds") + distinctRoomIds.forEach { roomId -> + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + if (roomSummary?.isEncrypted == true) { + Timber.d("## CrossSigning - Check shield state for room $roomId") + val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() + try { + val updatedTrust = computeRoomShield(allActiveRoomMembers, roomSummary) + if (roomSummary.roomEncryptionTrustLevel != updatedTrust) { + Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust") + roomSummary.roomEncryptionTrustLevel = updatedTrust + } + } catch (failure: Throwable) { + Timber.e(failure) + } + } + } + } + } + + return Result.success() + } + + private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) { + val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst() + xInfoEntity?.crossSigningKeys?.forEach { info -> + // optimization to avoid trigger updates when there is no change.. + if (info.trustLevelEntity?.isVerified() != verified) { + Timber.d("## CrossSigning - Trust change for $userId : $verified") + val level = info.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = verified + newLevel.crossSignedVerified = verified + info.trustLevelEntity = newLevel + } else { + level.locallyVerified = verified + level.crossSignedVerified = verified + } + } + } + } + + private fun computeRoomShield(activeMemberUserIds: List, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel { + Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds") + // The set of “all users” depends on the type of room: + // For regular / topic rooms, all users including yourself, are considered when decorating a room + // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room + val listToCheck = if (roomSummaryEntity.isDirect) { + activeMemberUserIds.filter { it != myUserId } + } else { + activeMemberUserIds + } + + val allTrustedUserIds = listToCheck + .filter { userId -> + Realm.getInstance(realmConfiguration).use { + it.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst()?.let { mapCrossSigningInfoEntity(it) }?.isTrusted() == true + } + } + val myCrossKeys = Realm.getInstance(realmConfiguration).use { + it.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId) + .findFirst()?.let { mapCrossSigningInfoEntity(it) } + } + + return if (allTrustedUserIds.isEmpty()) { + RoomEncryptionTrustLevel.Default + } else { + // If one of the verified user as an untrusted device -> warning + // If all devices of all verified users are trusted -> green + // else -> black + allTrustedUserIds + .mapNotNull { uid -> + Realm.getInstance(realmConfiguration).use { + it.where() + .equalTo(UserEntityFields.USER_ID, uid) + .findFirst() + ?.devices + ?.map { + CryptoMapper.mapToModel(it) + } + } + } + .flatten() + .let { allDevices -> + Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }}") + if (myCrossKeys != null) { + allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() } + } else { + // Legacy method + allDevices.any { !it.isVerified } + } + } + .let { hasWarning -> + if (hasWarning) { + RoomEncryptionTrustLevel.Warning + } else { + if (listToCheck.size == allTrustedUserIds.size) { + // all users are trusted and all devices are verified + RoomEncryptionTrustLevel.Trusted + } else { + RoomEncryptionTrustLevel.Default + } + } + } + } + } + + private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { + val userId = xsignInfo.userId ?: "" + return MXCrossSigningInfo( + userId = userId, + crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { + crossSigningKeysMapper.map(userId, it) + } + ) + } + + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 64579c1b67..3abbf9b16e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -234,7 +234,10 @@ internal class DefaultKeysBackupService @Inject constructor( this.callback = object : MatrixCallback { override fun onSuccess(data: KeysVersion) { // Reset backup markers. - cryptoStore.resetBackupMarkers() + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + // move tx out of UI thread + cryptoStore.resetBackupMarkers() + } val keyBackupVersion = KeysVersionResult( algorithm = createKeysBackupVersionBody.algorithm, @@ -596,7 +599,9 @@ internal class DefaultKeysBackupService @Inject constructor( val importResult = awaitCallback { restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) } - cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + withContext(coroutineDispatchers.crypto) { + cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + } Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}") } else { Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") @@ -685,7 +690,7 @@ internal class DefaultKeysBackupService @Inject constructor( // Get backed up keys from the homeserver val data = getKeys(sessionId, roomId, keysVersionResult.version!!) - withContext(coroutineDispatchers.crypto) { + withContext(coroutineDispatchers.computation) { val sessionsData = ArrayList() // Restore that data var sessionsFromHsCount = 0 @@ -1123,7 +1128,9 @@ internal class DefaultKeysBackupService @Inject constructor( if (retrievedMegolmBackupAuthData != null) { keysBackupVersion = keysVersionResult - cryptoStore.setKeyBackupVersion(keysVersionResult.version) + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.setKeyBackupVersion(keysVersionResult.version) + } onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 0ae1e69124..72d541d4df 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.store import androidx.lifecycle.LiveData +import androidx.paging.PagedList import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Optional @@ -126,7 +127,10 @@ internal interface IMXCryptoStore { fun getPendingIncomingRoomKeyRequests(): List fun getPendingIncomingGossipingRequests(): List + fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) + + fun storeIncomingGossipingRequests(requests: List) // fun getPendingIncomingSecretShareRequests(): List /** @@ -364,6 +368,7 @@ internal interface IMXCryptoStore { fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? fun saveGossipingEvent(event: Event) + fun saveGossipingEvents(events: List) fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { updateGossipingRequestState( @@ -441,10 +446,13 @@ internal interface IMXCryptoStore { // Dev tools fun getOutgoingRoomKeyRequests(): List + fun getOutgoingRoomKeyRequestsPaged(): LiveData> fun getOutgoingSecretKeyRequests(): List fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? fun getIncomingRoomKeyRequests(): List - fun getGossipingEventsTrail(): List + fun getIncomingRoomKeyRequestsPaged(): LiveData> + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEvents(): List fun setDeviceKeysUploaded(uploaded: Boolean) fun getDeviceKeysUploaded(): Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index b25349cba9..5d19e6d607 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.store.db import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmConfiguration @@ -62,6 +64,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFie import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity @@ -998,7 +1001,50 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getGossipingEventsTrail(): List { + override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING) + } + val dataSourceFactory = realmDataSourceFactory.map { + it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + ?: IncomingRoomKeyRequest( + requestBody = null, + deviceId = "", + userId = "", + requestId = "", + state = GossipingRequestState.NONE, + localCreationTimestamp = 0 + ) + } + return monarchy.findAllPagedWithChanges(realmDataSourceFactory, + LivePagedListBuilder(dataSourceFactory, + PagedList.Config.Builder() + .setPageSize(20) + .setEnablePlaceholders(false) + .setPrefetchDistance(1) + .build()) + ) + } + + override fun getGossipingEventsTrail(): LiveData> { + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm.where().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) + } + val dataSourceFactory = realmDataSourceFactory.map { it.toModel() } + val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory, + LivePagedListBuilder(dataSourceFactory, + PagedList.Config.Builder() + .setPageSize(20) + .setEnablePlaceholders(false) + .setPrefetchDistance(1) + .build()) + ) + return trail + } + + override fun getGossipingEvents(): List { return monarchy.fetchAllCopiedSync { realm -> realm.where() }.map { @@ -1066,24 +1112,43 @@ internal class RealmCryptoStore @Inject constructor( return request } - override fun saveGossipingEvent(event: Event) { + override fun saveGossipingEvents(events: List) { val now = System.currentTimeMillis() - val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now - val entity = GossipingEventEntity( - type = event.type, - sender = event.senderId, - ageLocalTs = ageLocalTs, - content = ContentMapper.map(event.content) - ).apply { - sendState = SendState.SYNCED - decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) - decryptionErrorCode = event.mCryptoError?.name - } - doRealmTransaction(realmConfiguration) { realm -> - realm.insertOrUpdate(entity) + monarchy.writeAsync { realm -> + events.forEach { event -> + val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now + val entity = GossipingEventEntity( + type = event.type, + sender = event.senderId, + ageLocalTs = ageLocalTs, + content = ContentMapper.map(event.content) + ).apply { + sendState = SendState.SYNCED + decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) + decryptionErrorCode = event.mCryptoError?.name + } + realm.insertOrUpdate(entity) + } } } + override fun saveGossipingEvent(event: Event) { + monarchy.writeAsync { realm -> + val now = System.currentTimeMillis() + val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now + val entity = GossipingEventEntity( + type = event.type, + sender = event.senderId, + ageLocalTs = ageLocalTs, + content = ContentMapper.map(event.content) + ).apply { + sendState = SendState.SYNCED + decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) + decryptionErrorCode = event.mCryptoError?.name + } + realm.insertOrUpdate(entity) + } + } // override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { // val statesIndex = states.map { it.ordinal }.toTypedArray() // return doRealmQueryAndCopy(realmConfiguration) { realm -> @@ -1284,6 +1349,28 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun storeIncomingGossipingRequests(requests: List) { + doRealmTransactionAsync(realmConfiguration) { realm -> + requests.forEach { request -> + // After a clear cache, we might have a + realm.createObject(IncomingGossipingRequestEntity::class.java).let { + it.otherDeviceId = request.deviceId + it.otherUserId = request.userId + it.requestId = request.requestId ?: "" + it.requestState = GossipingRequestState.PENDING + it.localCreationTimestamp = request.localCreationTimestamp ?: System.currentTimeMillis() + if (request is IncomingSecretShareRequest) { + it.type = GossipRequestType.SECRET + it.requestedInfoStr = request.secretName + } else if (request is IncomingRoomKeyRequest) { + it.type = GossipRequestType.KEY + it.requestedInfoStr = request.requestBody?.toJson() + } + } + } + } + } + // override fun getPendingIncomingSecretShareRequests(): List { // return doRealmQueryAndCopyList(realmConfiguration) { // it.where() @@ -1417,6 +1504,27 @@ internal class RealmCryptoStore @Inject constructor( .filterNotNull() } + override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + } + val dataSourceFactory = realmDataSourceFactory.map { + it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + ?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED) + } + val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory, + LivePagedListBuilder(dataSourceFactory, + PagedList.Config.Builder() + .setPageSize(20) + .setEnablePlaceholders(false) + .setPrefetchDistance(1) + .build()) + ) + return trail + } + override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { return doWithRealm(realmConfiguration) { realm -> val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt index 1b88fbe9cc..56b267decd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -33,14 +33,13 @@ internal interface EncryptEventTask : Task { data class Params(val roomId: String, val event: Event, /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ - val keepKeys: List? = null, - val crypto: CryptoService + val keepKeys: List? = null ) } internal class DefaultEncryptEventTask @Inject constructor( -// private val crypto: CryptoService - private val localEchoRepository: LocalEchoRepository + private val localEchoRepository: LocalEchoRepository, + private val cryptoService: CryptoService ) : EncryptEventTask { override suspend fun execute(params: EncryptEventTask.Params): Event { // don't want to wait for any query @@ -60,7 +59,7 @@ internal class DefaultEncryptEventTask @Inject constructor( // try { // let it throws awaitCallback { - params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) }.let { result -> val modifiedContent = HashMap(result.eventContent) params.keepKeys?.forEach { toKeep -> @@ -81,7 +80,7 @@ internal class DefaultEncryptEventTask @Inject constructor( ).toContent(), forwardingCurve25519KeyChain = emptyList(), senderCurve25519Key = result.eventContent["sender_key"] as? String, - claimedEd25519Key = params.crypto.getMyDevice().fingerprint() + claimedEd25519Key = cryptoService.getMyDevice().fingerprint() ) } else { null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 1a712036c8..8b739c4b64 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.crypto.tasks import org.greenrobot.eventbus.EventBus -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.network.executeRequest @@ -29,8 +28,7 @@ import javax.inject.Inject internal interface SendEventTask : Task { data class Params( val event: Event, - val encrypt: Boolean, - val cryptoService: CryptoService? + val encrypt: Boolean ) } @@ -68,8 +66,7 @@ internal class DefaultSendEventTask @Inject constructor( return encryptEventTask.execute(EncryptEventTask.Params( params.event.roomId ?: "", params.event, - listOf("m.relates_to"), - params.cryptoService!! + listOf("m.relates_to") )) } return params.event diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index 782300c7b0..cedb7a6618 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -15,21 +15,20 @@ */ package org.matrix.android.sdk.internal.crypto.tasks -import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.task.Task -import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface SendVerificationMessageTask : Task { data class Params( - val event: Event, - val cryptoService: CryptoService? + val event: Event ) } @@ -37,6 +36,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( private val localEchoRepository: LocalEchoRepository, private val encryptEventTask: DefaultEncryptEventTask, private val roomAPI: RoomAPI, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val eventBus: EventBus) : SendVerificationMessageTask { override suspend fun execute(params: SendVerificationMessageTask.Params): String { @@ -62,13 +62,12 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( } private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event { - if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) { + if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) { try { return encryptEventTask.execute(EncryptEventTask.Params( params.event.roomId ?: "", params.event, - listOf("m.relates_to"), - params.cryptoService + listOf("m.relates_to") )) } catch (throwable: Throwable) { // We said it's ok to send verification request in clear diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index c0f4671046..7f02750359 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -20,7 +20,6 @@ import android.os.Handler import android.os.Looper import dagger.Lazy import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME @@ -111,9 +110,6 @@ internal class DefaultVerificationService @Inject constructor( private val uiHandler = Handler(Looper.getMainLooper()) - // Cannot be injected in constructor as it creates a dependency cycle - lateinit var cryptoService: CryptoService - // map [sender : [transaction]] private val txMap = HashMap>() @@ -129,7 +125,8 @@ internal class DefaultVerificationService @Inject constructor( // Event received from the sync fun onToDeviceEvent(event: Event) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.d("## SAS onToDeviceEvent ${event.getClearType()}") + cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { when (event.getClearType()) { EventType.KEY_VERIFICATION_START -> { onStartRequestReceived(event) @@ -163,7 +160,7 @@ internal class DefaultVerificationService @Inject constructor( } fun onRoomEvent(event: Event) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { when (event.getClearType()) { EventType.KEY_VERIFICATION_START -> { onRoomStartRequestReceived(event) @@ -240,6 +237,7 @@ internal class DefaultVerificationService @Inject constructor( } private fun dispatchRequestAdded(tx: PendingVerificationRequest) { + Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId}") uiHandler.post { listeners.forEach { try { @@ -303,11 +301,14 @@ internal class DefaultVerificationService @Inject constructor( // We don't want to block here val otherDeviceId = validRequestInfo.fromDevice + Timber.v("## SAS onRequestReceived from $senderId and device $otherDeviceId, txId:${validRequestInfo.transactionId}") + cryptoCoroutineScope.launch { if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) { Timber.e("## Verification device $otherDeviceId is not known") } } + Timber.v("## SAS onRequestReceived .. checkKeysAreDownloaded launched") // Remember this request val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } @@ -1203,7 +1204,9 @@ internal class DefaultVerificationService @Inject constructor( // TODO refactor this with the DM one Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") - val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId } + val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId) + ?.values?.map { it.deviceId } ?: emptyList() + val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } val transport = verificationTransportToDeviceFactory.createTransport(null) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt index fa7cd2e6f9..538d7b56e9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt @@ -20,7 +20,6 @@ import androidx.work.Data import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker @@ -47,7 +46,6 @@ internal class SendVerificationMessageWorker(context: Context, @Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask @Inject lateinit var localEchoRepository: LocalEchoRepository - @Inject lateinit var cryptoService: CryptoService @Inject lateinit var cancelSendTracker: CancelSendTracker override fun injectWith(injector: SessionComponent) { @@ -70,8 +68,7 @@ internal class SendVerificationMessageWorker(context: Context, return try { val resultEventId = sendVerificationMessageTask.execute( SendVerificationMessageTask.Params( - event = localEvent, - cryptoService = cryptoService + event = localEvent ) ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt index 4994325625..74827eeb2a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt @@ -15,7 +15,6 @@ */ package org.matrix.android.sdk.internal.crypto.verification -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.events.model.Event @@ -34,12 +33,13 @@ import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import io.realm.Realm +import org.matrix.android.sdk.internal.crypto.EventDecryptor import timber.log.Timber import java.util.ArrayList import javax.inject.Inject internal class VerificationMessageProcessor @Inject constructor( - private val cryptoService: CryptoService, + private val eventDecryptor: EventDecryptor, private val verificationService: DefaultVerificationService, @UserId private val userId: String, @DeviceId private val deviceId: String? @@ -82,7 +82,7 @@ internal class VerificationMessageProcessor @Inject constructor( // TODO use a global event decryptor? attache to session and that listen to new sessionId? // for now decrypt sync try { - val result = cryptoService.decryptEvent(event, "") + val result = eventDecryptor.decryptEvent(event, "") event.mxDecryptionResult = OlmDecryptionResult( payload = result.clearEvent, senderKey = result.senderCurve25519Key, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt index 11a877e7c4..71f978c03c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -17,10 +17,6 @@ package org.matrix.android.sdk.internal.database import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.session.crypto.CryptoService -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity @@ -31,12 +27,13 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import io.realm.RealmConfiguration import io.realm.RealmResults import kotlinx.coroutines.launch +import org.matrix.android.sdk.internal.crypto.EventDecryptor import timber.log.Timber import javax.inject.Inject internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>, - private val cryptoService: CryptoService) + private val eventDecryptor: EventDecryptor) : RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { @@ -74,7 +71,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real return@forEach } val domainEvent = event.asDomain() - decryptIfNeeded(domainEvent) +// decryptIfNeeded(domainEvent) processors.filter { it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType) }.forEach { @@ -89,22 +86,22 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real } } - private fun decryptIfNeeded(event: Event) { - if (event.isEncrypted() && event.mxDecryptionResult == null) { - try { - val result = cryptoService.decryptEvent(event, event.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) { - Timber.v("Failed to decrypt event") - // TODO -> we should keep track of this and retry, or some processing will never be handled - } - } - } +// private fun decryptIfNeeded(event: Event) { +// if (event.isEncrypted() && event.mxDecryptionResult == null) { +// try { +// val result = eventDecryptor.decryptEvent(event, event.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) { +// Timber.v("Failed to decrypt event") +// // TODO -> we should keep track of this and retry, or some processing will never be handled +// } +// } +// } private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean { return processors.any { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 359dd265d8..7e182525a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -65,7 +65,6 @@ import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor -import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.session.sync.job.SyncThread import org.matrix.android.sdk.internal.session.sync.job.SyncWorker @@ -115,7 +114,6 @@ internal class DefaultSession @Inject constructor( private val accountDataService: Lazy, private val _sharedSecretStorageService: Lazy, private val accountService: Lazy, - private val timelineEventDecryptor: TimelineEventDecryptor, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val defaultIdentityService: DefaultIdentityService, private val integrationManagerService: IntegrationManagerService, @@ -162,7 +160,6 @@ internal class DefaultSession @Inject constructor( lifecycleObservers.forEach { it.onStart() } } eventBus.register(this) - timelineEventDecryptor.start() eventSenderProcessor.start() } @@ -200,7 +197,7 @@ internal class DefaultSession @Inject constructor( override fun close() { assert(isOpen) stopSync() - timelineEventDecryptor.destroy() + // timelineEventDecryptor.destroy() uiHandler.post { lifecycleObservers.forEach { it.onStop() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index ed586d35f8..e6fd5a7a0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker import org.matrix.android.sdk.internal.crypto.CryptoModule import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker import org.matrix.android.sdk.internal.crypto.SendGossipWorker +import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker import org.matrix.android.sdk.internal.di.MatrixComponent import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule @@ -128,6 +129,8 @@ internal interface SessionComponent { fun inject(worker: SendGossipWorker) + fun inject(worker: UpdateTrustWorker) + @Component.Factory interface Factory { fun create( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 102a34d9de..32949d60c4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.typing.TypingUsersTracker -import org.matrix.android.sdk.internal.crypto.crosssigning.ShieldTrustUpdater import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask @@ -333,10 +332,6 @@ internal abstract class SessionModule { @IntoSet abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver - @Binds - @IntoSet - abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver - @Binds @IntoSet abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 9ff0deec89..d090ba5296 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -15,7 +15,7 @@ */ package org.matrix.android.sdk.internal.session.room -import org.matrix.android.sdk.api.session.crypto.CryptoService +import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -47,7 +47,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor -import io.realm.Realm import timber.log.Timber import javax.inject.Inject @@ -78,9 +77,8 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio return newState } -internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String, - private val cryptoService: CryptoService -) : EventInsertLiveProcessor { +internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String) + : EventInsertLiveProcessor { private val allowedTypes = listOf( EventType.MESSAGE, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 38c542e07e..a7f3f83980 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -21,7 +21,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.message.MessageType @@ -31,13 +30,13 @@ import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.task.TaskExecutor @@ -47,11 +46,9 @@ import timber.log.Timber internal class DefaultRelationService @AssistedInject constructor( @Assisted private val roomId: String, - @SessionId private val sessionId: String, -// private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon, private val eventSenderProcessor: EventSenderProcessor, private val eventFactory: LocalEchoEventFactory, - private val cryptoService: CryptoService, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val fetchEditHistoryTask: FetchEditHistoryTask, private val timelineEventMapper: TimelineEventMapper, @@ -122,7 +119,7 @@ internal class DefaultRelationService @AssistedInject constructor( val event = eventFactory .createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText) .also { saveLocalEcho(it) } - return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId)) + return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) } override fun editReply(replyToEdit: TimelineEvent, @@ -139,11 +136,11 @@ internal class DefaultRelationService @AssistedInject constructor( compatibilityBodyText ) .also { saveLocalEcho(it) } - return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId)) + return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) } override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) { - val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId) + val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId) fetchEditHistoryTask .configureWith(params) { this.callback = callback @@ -156,7 +153,7 @@ internal class DefaultRelationService @AssistedInject constructor( ?.also { saveLocalEcho(it) } ?: return null - return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId)) + return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) } override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 5c395c1907..b13ce15da6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -25,7 +25,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.content.ContentAttachmentData -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage @@ -45,6 +44,7 @@ import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.CancelableBag import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.content.UploadContentWorker @@ -64,7 +64,7 @@ internal class DefaultSendService @AssistedInject constructor( private val workManagerProvider: WorkManagerProvider, @SessionId private val sessionId: String, private val localEchoEventFactory: LocalEchoEventFactory, - private val cryptoService: CryptoService, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository, private val eventSenderProcessor: EventSenderProcessor, @@ -251,7 +251,7 @@ internal class DefaultSendService @AssistedInject constructor( private fun internalSendMedia(allLocalEchoes: List, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable { val cancelableBag = CancelableBag() - allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) } + allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) } .apply { keys.forEach { isRoomEncrypted -> // Should never be empty @@ -282,7 +282,7 @@ internal class DefaultSendService @AssistedInject constructor( } private fun sendEvent(event: Event): Cancelable { - return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(event.roomId!!)) + return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!)) } private fun createLocalEcho(event: Event) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index 2c835ff56c..37a429d242 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -87,7 +87,7 @@ internal class SendEventWorker(context: Context, Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}") return try { - sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId), cryptoService)) + sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId))) Result.success() } catch (exception: Throwable) { if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt index 09da0908f9..21a4145a9d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt @@ -38,7 +38,7 @@ internal class SendEventQueuedTask( override fun toString() = "[SendEventRunnableTask ${event.eventId}]" override suspend fun execute() { - sendEventTask.execute(SendEventTask.Params(event, encrypt, cryptoService)) + sendEventTask.execute(SendEventTask.Params(event, encrypt)) } override fun onTaskFailed() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index f9a27c367c..8c71604183 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -16,10 +16,8 @@ package org.matrix.android.sdk.internal.session.room.summary -import dagger.Lazy import io.realm.Realm -import org.greenrobot.eventbus.EventBus -import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership @@ -28,9 +26,11 @@ import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomNameContent import org.matrix.android.sdk.api.session.room.model.RoomTopicContent import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate +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 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntityFields @@ -46,7 +46,6 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper -import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications import timber.log.Timber @@ -56,8 +55,8 @@ internal class RoomSummaryUpdater @Inject constructor( @UserId private val userId: String, private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomAvatarResolver: RoomAvatarResolver, - private val timelineEventDecryptor: Lazy, - private val eventBus: EventBus) { + private val eventDecryptor: EventDecryptor, + private val crossSigningService: DefaultCrossSigningService) { fun update(realm: Realm, roomId: String, @@ -126,9 +125,14 @@ internal class RoomSummaryUpdater @Inject constructor( } roomSummaryEntity.updateHasFailedSending() - if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) { + val root = latestPreviewableEvent?.root + if (root?.type == EventType.ENCRYPTED && root.decryptionResultJson == null) { Timber.v("Should decrypt ${latestPreviewableEvent.eventId}") - timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, "")) + // mmm i want to decrypt now or is it ok to do it async? + tryOrNull { + eventDecryptor.decryptEvent(root.asDomain(), "") + // eventDecryptor.decryptEventAsync(root.asDomain(), "", NoOpMatrixCallback()) + } } if (updateMembers) { @@ -142,7 +146,8 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) if (roomSummaryEntity.isEncrypted) { - eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId)) + // mmm maybe we could only refresh shield instead of checking trust also? + crossSigningService.onUsersDeviceUpdate(roomSummaryEntity.otherMemberIds.toList()) } } } @@ -156,13 +161,4 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.updateHasFailedSending() roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) } - - fun updateShieldTrust(realm: Realm, - roomId: String, - trust: RoomEncryptionTrustLevel?) { - val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) - if (roomSummaryEntity.isEncrypted) { - roomSummaryEntity.roomEncryptionTrustLevel = trust - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 1e5c3600d3..f720a4ede0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -630,7 +630,7 @@ internal class DefaultTimeline( if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { - timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(it, timelineID)) } + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineID)) } } val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt index e91487eab0..3517f26c5d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -15,24 +15,22 @@ */ package org.matrix.android.sdk.internal.session.room.timeline +import io.realm.Realm +import io.realm.RealmConfiguration import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.internal.crypto.NewSessionListener import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent -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 import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.session.SessionScope -import io.realm.Realm -import io.realm.RealmConfiguration import timber.log.Timber import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import javax.inject.Inject -@SessionScope internal class TimelineEventDecryptor @Inject constructor( @SessionDatabase private val realmConfiguration: RealmConfiguration, @@ -83,14 +81,14 @@ internal class TimelineEventDecryptor @Inject constructor( synchronized(unknownSessionsFailure) { for (requests in unknownSessionsFailure.values) { if (request in requests) { - Timber.d("Skip Decryption request for event ${request.eventId}, unknown session") + Timber.d("Skip Decryption request for event ${request.event.eventId}, unknown session") return } } } synchronized(existingRequests) { if (!existingRequests.add(request)) { - Timber.d("Skip Decryption request for event ${request.eventId}, already requested") + Timber.d("Skip Decryption request for event ${request.event.eventId}, already requested") return } } @@ -101,25 +99,29 @@ internal class TimelineEventDecryptor @Inject constructor( } } - private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction { - val eventId = request.eventId + private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) { + val event = request.event val timelineId = request.timelineId - Timber.v("Decryption request for event $eventId") - val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst() - ?: return@executeTransaction Unit.also { - Timber.d("Decryption request for unknown message") - } - val event = eventEntity.asDomain() try { - val result = cryptoService.decryptEvent(event, timelineId) - Timber.v("Successfully decrypted event $eventId") - eventEntity.setDecryptionResult(result) + val result = cryptoService.decryptEvent(request.event, timelineId) + Timber.v("Successfully decrypted event ${event.eventId}") + realm.executeTransaction { + EventEntity.where(it, eventId = event.eventId ?: "") + .findFirst() + ?.setDecryptionResult(result) + } } catch (e: MXCryptoError) { - Timber.v(e, "Failed to decrypt event $eventId") + 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 - eventEntity.decryptionErrorCode = e.errorType.name - eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + realm.executeTransaction { + EventEntity.where(it, eventId = event.eventId ?: "") + .findFirst() + ?.let { + it.decryptionErrorCode = e.errorType.name + it.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + } + } event.content?.toModel()?.let { content -> content.sessionId?.let { sessionId -> synchronized(unknownSessionsFailure) { @@ -130,7 +132,7 @@ internal class TimelineEventDecryptor @Inject constructor( } } } catch (t: Throwable) { - Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}") + Timber.e("Failed to decrypt event ${event.eventId}, ${t.localizedMessage}") } finally { synchronized(existingRequests) { existingRequests.remove(request) @@ -139,7 +141,7 @@ internal class TimelineEventDecryptor @Inject constructor( } data class DecryptionRequest( - val eventId: String, + val event: Event, val timelineId: String ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index 8589889b30..b1b2f65dc2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.internal.session.sync +import io.realm.Realm +import io.realm.kotlin.createObject +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.R import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event @@ -54,16 +57,12 @@ import org.matrix.android.sdk.internal.session.room.read.FullyReadContent import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection -import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync import org.matrix.android.sdk.internal.session.sync.model.RoomSync import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse -import io.realm.Realm -import io.realm.kotlin.createObject -import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject @@ -76,8 +75,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val roomTypingUsersHandler: RoomTypingUsersHandler, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, @UserId private val userId: String, - private val eventBus: EventBus, - private val timelineEventDecryptor: TimelineEventDecryptor) { + private val eventBus: EventBus) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index cfd7865269..74cba5e796 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -54,7 +54,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private val networkConnectivityChecker: NetworkConnectivityChecker, private val backgroundDetectionObserver: BackgroundDetectionObserver, private val activeCallHandler: ActiveCallHandler -) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { +) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { private var state: SyncState = SyncState.Idle private var liveState = MutableLiveData(state) diff --git a/tools/check/check_code_quality.sh b/tools/check/check_code_quality.sh index e855440e81..0b4272cbfe 100755 --- a/tools/check/check_code_quality.sh +++ b/tools/check/check_code_quality.sh @@ -91,7 +91,6 @@ ${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt ./vector/src/main/res/color \ ./vector/src/main/res/layout \ ./vector/src/main/res/values \ - ./vector/src/main/res/values-v21 \ ./vector/src/main/res/xml resultForbiddenStringInResource=$? diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index b5552e4d62..e553b5e0d3 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.os.Bundle import android.os.Parcelable import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Lifecycle import com.bumptech.glide.Glide import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder @@ -205,13 +206,15 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity { } private fun displayError(failure: Throwable) { - AlertDialog.Builder(this) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(failure)) - .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() } - .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() } - .setCancelable(false) - .show() + if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(failure)) + .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() } + .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() } + .setCancelable(false) + .show() + } } private fun startNextActivityAndFinish() { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 0a1b631344..e42eb6de6f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.util.toMatrixItem +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -116,6 +117,7 @@ class IncomingVerificationRequestHandler @Inject constructor( } override fun verificationRequestCreated(pr: PendingVerificationRequest) { + Timber.v("## SAS verificationRequestCreated ${pr.transactionId}") // For incoming request we should prompt (if not in activity where this request apply) if (pr.isIncoming) { val name = session?.getUser(pr.otherUserId)?.displayName diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt index e26736de7e..3f38e4ef15 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt @@ -69,6 +69,9 @@ class BugReportActivity : VectorBaseActivity() { bug_report_button_include_crash_logs.isChecked = false bug_report_button_include_crash_logs.isVisible = false + bug_report_button_include_key_share_history.isChecked = false + bug_report_button_include_key_share_history.isVisible = false + // Keep the screenshot } else { supportActionBar?.setTitle(R.string.title_activity_bug_report) @@ -121,6 +124,7 @@ class BugReportActivity : VectorBaseActivity() { forSuggestion, bug_report_button_include_logs.isChecked, bug_report_button_include_crash_logs.isChecked, + bug_report_button_include_key_share_history.isChecked, bug_report_button_include_screenshot.isChecked, bug_report_edit_text.text.toString(), object : BugReporter.IMXBugReportListener { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt index e7382a17e9..95fe16ea51 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt @@ -33,6 +33,7 @@ import im.vector.app.core.extensions.getAllChildFragments import im.vector.app.core.extensions.toOnOff import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.settings.devtools.GossipingEventsSerializer import im.vector.app.features.settings.locale.SystemLocaleProvider import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.version.VersionProvider @@ -74,6 +75,7 @@ class BugReporter @Inject constructor( private const val LOG_CAT_FILENAME = "logcat.log" private const val LOG_CAT_SCREENSHOT_FILENAME = "screenshot.png" private const val CRASH_FILENAME = "crash.log" + private const val KEY_REQUESTS_FILENAME = "keyRequests.log" private const val BUFFER_SIZE = 1024 * 1024 * 50 } @@ -143,6 +145,7 @@ class BugReporter @Inject constructor( * @param forSuggestion true to send a suggestion * @param withDevicesLogs true to include the device log * @param withCrashLogs true to include the crash logs + * @param withKeyRequestHistory true to include the crash logs * @param withScreenshot true to include the screenshot * @param theBugDescription the bug description * @param listener the listener @@ -152,6 +155,7 @@ class BugReporter @Inject constructor( forSuggestion: Boolean, withDevicesLogs: Boolean, withCrashLogs: Boolean, + withKeyRequestHistory: Boolean, withScreenshot: Boolean, theBugDescription: String, listener: IMXBugReportListener?) { @@ -207,6 +211,22 @@ class BugReporter @Inject constructor( } } + activeSessionHolder.getSafeActiveSession() + ?.takeIf { !mIsCancelled && withKeyRequestHistory } + ?.cryptoService() + ?.getGossipingEvents() + ?.let { GossipingEventsSerializer().serialize(it) } + ?.toByteArray() + ?.let { rawByteArray -> + File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME) + .also { + it.outputStream() + .use { os -> os.write(rawByteArray) } + } + } + ?.let { compressFile(it) } + ?.let { gzippedFiles.add(it) } + var deviceId = "undefined" var userId = "undefined" var olmVersion = "undefined" diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt deleted file mode 100644 index cf93bc14e7..0000000000 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (c) 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.settings.devtools - -import com.airbnb.epoxy.TypedEpoxyController -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized -import im.vector.app.R -import im.vector.app.core.date.DateFormatKind -import im.vector.app.core.date.VectorDateFormatter -import im.vector.app.core.epoxy.loadingItem -import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.StringProvider -import im.vector.app.core.ui.list.GenericItem -import im.vector.app.core.ui.list.genericFooterItem -import im.vector.app.core.ui.list.genericItem -import im.vector.app.core.ui.list.genericItemHeader -import me.gujun.android.span.span -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.toModel -import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent -import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent -import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent -import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject -import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest -import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest -import javax.inject.Inject - -class GossipingEventsEpoxyController @Inject constructor( - private val stringProvider: StringProvider, - private val vectorDateFormatter: VectorDateFormatter, - private val colorProvider: ColorProvider -) : TypedEpoxyController() { - - interface InteractionListener { - fun didTap(event: Event) - } - - var interactionListener: InteractionListener? = null - - override fun buildModels(data: GossipingEventsPaperTrailState?) { - when (val async = data?.events) { - is Uninitialized, - is Loading -> { - loadingItem { - id("loadingOutgoing") - loadingText(stringProvider.getString(R.string.loading)) - } - } - is Fail -> { - genericItem { - id("failOutgoing") - title(async.error.localizedMessage) - } - } - is Success -> { - val eventList = async.invoke() - if (eventList.isEmpty()) { - genericFooterItem { - id("empty") - text(stringProvider.getString(R.string.no_result_placeholder)) - } - return - } - - eventList.forEachIndexed { _, event -> - genericItem { - id(event.hashCode()) - itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } }) - title( - if (event.isEncrypted()) { - "${event.getClearType()} [encrypted]" - } else { - event.type - } - ) - description( - span { - +vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME) - span("\nfrom: ") { - textStyle = "bold" - } - +"${event.senderId}" - apply { - if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { - val content = event.getClearContent().toModel() - span("\nreqId:") { - textStyle = "bold" - } - +" ${content?.requestId}" - span("\naction:") { - textStyle = "bold" - } - +" ${content?.action}" - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - span("\nsessionId:") { - textStyle = "bold" - } - +" ${content.body?.sessionId}" - } - span("\nrequestedBy: ") { - textStyle = "bold" - } - +"${content?.requestingDeviceId}" - } else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { - val encryptedContent = event.content.toModel() - val content = event.getClearContent().toModel() - if (event.mxDecryptionResult == null) { - span("**Failed to Decrypt** ${event.mCryptoError}") { - textColor = colorProvider.getColor(R.color.vector_error_color) - } - } - span("\nsessionId:") { - textStyle = "bold" - } - +" ${content?.sessionId}" - span("\nFrom Device (sender key):") { - textStyle = "bold" - } - +" ${encryptedContent?.senderKey}" - } else if (event.getClearType() == EventType.SEND_SECRET) { - val content = event.getClearContent().toModel() - - span("\nrequestId:") { - textStyle = "bold" - } - +" ${content?.requestId}" - span("\nFrom Device:") { - textStyle = "bold" - } - +" ${event.mxDecryptionResult?.payload?.get("sender_device")}" - } else if (event.getClearType() == EventType.REQUEST_SECRET) { - val content = event.getClearContent().toModel() - span("\nreqId:") { - textStyle = "bold" - } - +" ${content?.requestId}" - span("\naction:") { - textStyle = "bold" - } - +" ${content?.action}" - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - span("\nsecretName:") { - textStyle = "bold" - } - +" ${content.secretName}" - } - span("\nrequestedBy: ") { - textStyle = "bold" - } - +"${content?.requestingDeviceId}" - } - } - } - ) - } - } - } - } - } - - private fun buildOutgoing(data: KeyRequestListViewState?) { - data?.outgoingRoomKeyRequests?.let { async -> - when (async) { - is Uninitialized, - is Loading -> { - loadingItem { - id("loadingOutgoing") - loadingText(stringProvider.getString(R.string.loading)) - } - } - is Fail -> { - genericItem { - id("failOutgoing") - title(async.error.localizedMessage) - } - } - is Success -> { - if (async.invoke().isEmpty()) { - genericFooterItem { - id("empty") - text(stringProvider.getString(R.string.no_result_placeholder)) - } - return - } - - val requestList = async.invoke().groupBy { it.roomId } - - requestList.forEach { - genericItemHeader { - id(it.key) - text("roomId: ${it.key}") - } - it.value.forEach { roomKeyRequest -> - genericItem { - id(roomKeyRequest.requestId) - title(roomKeyRequest.requestId) - description( - span { - span("sessionId:\n") { - textStyle = "bold" - } - +"${roomKeyRequest.sessionId}" - span("\nstate:") { - textStyle = "bold" - } - +"\n${roomKeyRequest.state.name}" - } - ) - } - } - } - } - }.exhaustive - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt index e2c855a9e3..0ceb8e148d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt @@ -33,16 +33,19 @@ import javax.inject.Inject class GossipingEventsPaperTrailFragment @Inject constructor( val viewModelFactory: GossipingEventsPaperTrailViewModel.Factory, - private val epoxyController: GossipingEventsEpoxyController, + private val epoxyController: GossipingTrailPagedEpoxyController, private val colorProvider: ColorProvider -) : VectorBaseFragment(), GossipingEventsEpoxyController.InteractionListener { +) : VectorBaseFragment(), GossipingTrailPagedEpoxyController.InteractionListener { override fun getLayoutResId() = R.layout.fragment_generic_recycler private val viewModel: GossipingEventsPaperTrailViewModel by fragmentViewModel(GossipingEventsPaperTrailViewModel::class) override fun invalidate() = withState(viewModel) { state -> - epoxyController.setData(state) + state.events.invoke()?.let { + epoxyController.submitList(it) + } + Unit } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt index d903725b22..4249ef09fa 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt @@ -16,13 +16,12 @@ package im.vector.app.features.settings.devtools -import androidx.lifecycle.viewModelScope +import androidx.paging.PagedList import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted @@ -30,12 +29,12 @@ import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.platform.EmptyAction 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.events.model.Event +import org.matrix.android.sdk.rx.asObservable data class GossipingEventsPaperTrailState( - val events: Async> = Uninitialized + val events: Async> = Uninitialized ) : MvRxState class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState, @@ -50,14 +49,10 @@ class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted i setState { copy(events = Loading()) } - viewModelScope.launch { - session.cryptoService().getGossipingEventsTrail().let { - val sorted = it.sortedByDescending { it.ageLocalTs } - setState { - copy(events = Success(sorted)) + session.cryptoService().getGossipingEventsTrail().asObservable() + .execute { + copy(events = it) } - } - } } override fun handle(action: EmptyAction) {} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt new file mode 100644 index 0000000000..d18a6c2ba8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devtools + +import im.vector.app.core.resources.DateProvider +import me.gujun.android.span.span +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.toModel +import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent +import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent +import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent +import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest +import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest +import org.threeten.bp.format.DateTimeFormatter + +class GossipingEventsSerializer { + private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") + + fun serialize(eventList: List): String { + return buildString { + eventList.forEach { + val clearType = it.getClearType() + append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ") + when (clearType) { + EventType.ROOM_KEY_REQUEST -> { + val content = it.getClearContent().toModel() + append("reqId:${content?.requestId} action:${content?.action} ") + if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { + append("sessionId: ${content.body?.sessionId} ") + } + append("requestedBy: ${content?.requestingDeviceId}") + } + EventType.FORWARDED_ROOM_KEY -> { + val encryptedContent = it.content.toModel() + val content = it.getClearContent().toModel() + + append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}") + span("\nFrom Device (sender key):") { + textStyle = "bold" + } + } + EventType.ROOM_KEY -> { + val content = it.getClearContent() + append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}") + } + EventType.SEND_SECRET -> { + val content = it.getClearContent().toModel() + append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}") + } + EventType.REQUEST_SECRET -> { + val content = it.getClearContent().toModel() + append("reqId:${content?.requestId} action:${content?.action} ") + if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { + append("secretName:${content.secretName} ") + } + append("requestedBy:${content?.requestingDeviceId}") + } + EventType.ENCRYPTED -> { + append("Failed to Decrypt") + } + else -> { + append("??") + } + } + append("\n") + } + } + } + + private fun getFormattedDate(ageLocalTs: Long?): String { + return ageLocalTs + ?.let { DateProvider.toLocalDateTime(it) } + ?.let { full24DateFormatter.format(it) } + ?: "?" + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt new file mode 100644 index 0000000000..603c67d074 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devtools + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.paging.PagedListEpoxyController +import im.vector.app.R +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.GenericItem +import im.vector.app.core.ui.list.GenericItem_ +import im.vector.app.core.utils.createUIHandler +import me.gujun.android.span.span +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.toModel +import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent +import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent +import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent +import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject +import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest +import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest +import javax.inject.Inject + +class GossipingTrailPagedEpoxyController @Inject constructor( + private val stringProvider: StringProvider, + private val vectorDateFormatter: VectorDateFormatter, + private val colorProvider: ColorProvider +) : PagedListEpoxyController( + // Important it must match the PageList builder notify Looper + modelBuildingHandler = createUIHandler() +) { + + interface InteractionListener { + fun didTap(event: Event) + } + + var interactionListener: InteractionListener? = null + + override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> { + val event = item ?: return GenericItem_().apply { id(currentPosition) } + return GenericItem_().apply { + id(event.hashCode()) + itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } }) + title( + if (event.isEncrypted()) { + "${event.getClearType()} [encrypted]" + } else { + event.type + } + ) + description( + span { + +vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME) + span("\nfrom: ") { + textStyle = "bold" + } + +"${event.senderId}" + apply { + if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { + val content = event.getClearContent().toModel() + span("\nreqId:") { + textStyle = "bold" + } + +" ${content?.requestId}" + span("\naction:") { + textStyle = "bold" + } + +" ${content?.action}" + if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { + span("\nsessionId:") { + textStyle = "bold" + } + +" ${content.body?.sessionId}" + } + span("\nrequestedBy: ") { + textStyle = "bold" + } + +"${content?.requestingDeviceId}" + } else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + val encryptedContent = event.content.toModel() + val content = event.getClearContent().toModel() + if (event.mxDecryptionResult == null) { + span("**Failed to Decrypt** ${event.mCryptoError}") { + textColor = colorProvider.getColor(R.color.vector_error_color) + } + } + span("\nsessionId:") { + textStyle = "bold" + } + +" ${content?.sessionId}" + span("\nFrom Device (sender key):") { + textStyle = "bold" + } + +" ${encryptedContent?.senderKey}" + } else if (event.getClearType() == EventType.ROOM_KEY) { + // it's a bit of a fake event for trail reasons + val content = event.getClearContent() + span("\nsessionId:") { + textStyle = "bold" + } + +" ${content?.get("session_id")}" + span("\nroomId:") { + textStyle = "bold" + } + +" ${content?.get("room_id")}" + span("\nTo :") { + textStyle = "bold" + } + +" ${content?.get("_dest") ?: "me"}" + } else if (event.getClearType() == EventType.SEND_SECRET) { + val content = event.getClearContent().toModel() + + span("\nrequestId:") { + textStyle = "bold" + } + +" ${content?.requestId}" + span("\nFrom Device:") { + textStyle = "bold" + } + +" ${event.mxDecryptionResult?.payload?.get("sender_device")}" + } else if (event.getClearType() == EventType.REQUEST_SECRET) { + val content = event.getClearContent().toModel() + span("\nreqId:") { + textStyle = "bold" + } + +" ${content?.requestId}" + span("\naction:") { + textStyle = "bold" + } + +" ${content?.action}" + if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { + span("\nsecretName:") { + textStyle = "bold" + } + +" ${content.secretName}" + } + span("\nrequestedBy: ") { + textStyle = "bold" + } + +"${content?.requestingDeviceId}" + } else if (event.getClearType() == EventType.ENCRYPTED) { + span("**Failed to Decrypt** ${event.mCryptoError}") { + textColor = colorProvider.getColor(R.color.vector_error_color) + } + } + } + } + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt index c7b95ddf78..35f46d9c74 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt @@ -24,14 +24,12 @@ import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.resources.ColorProvider import kotlinx.android.synthetic.main.fragment_generic_recycler.* import javax.inject.Inject class IncomingKeyRequestListFragment @Inject constructor( val viewModelFactory: KeyRequestListViewModel.Factory, - private val epoxyController: KeyRequestEpoxyController, - private val colorProvider: ColorProvider + private val epoxyController: IncomingKeyRequestPagedController ) : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_generic_recycler @@ -39,8 +37,10 @@ class IncomingKeyRequestListFragment @Inject constructor( private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class) override fun invalidate() = withState(viewModel) { state -> - epoxyController.outgoing = false - epoxyController.setData(state) + state.incomingRequests.invoke()?.let { + epoxyController.submitList(it) + } + Unit } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt new file mode 100644 index 0000000000..c2bdcfbd04 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devtools + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.paging.PagedListEpoxyController +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.ui.list.GenericItem_ +import im.vector.app.core.utils.createUIHandler +import me.gujun.android.span.span +import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest +import javax.inject.Inject + +class IncomingKeyRequestPagedController @Inject constructor( + private val vectorDateFormatter: VectorDateFormatter +) : PagedListEpoxyController( + // Important it must match the PageList builder notify Looper + modelBuildingHandler = createUIHandler() +) { + + interface InteractionListener { + // fun didTap(data: UserAccountData) + } + + var interactionListener: InteractionListener? = null + + override fun buildItemModel(currentPosition: Int, item: IncomingRoomKeyRequest?): EpoxyModel<*> { + val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) } + + return GenericItem_().apply { + id(roomKeyRequest.requestId) + title(roomKeyRequest.requestId) + description( + span { + span("From: ") { + textStyle = "bold" + } + span("${roomKeyRequest.userId}") + +vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME) + span("\nsessionId:") { + textStyle = "bold" + } + +"${roomKeyRequest.requestBody?.sessionId}" + span("\nFrom device:") { + textStyle = "bold" + } + +"${roomKeyRequest.deviceId}" + span("\nstate: ") { + textStyle = "bold" + } + +roomKeyRequest.state.name + } + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestEpoxyController.kt deleted file mode 100644 index 5907b55b31..0000000000 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestEpoxyController.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.settings.devtools - -import com.airbnb.epoxy.TypedEpoxyController -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized -import im.vector.app.R -import im.vector.app.core.epoxy.loadingItem -import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.resources.StringProvider -import im.vector.app.core.ui.list.genericFooterItem -import im.vector.app.core.ui.list.genericItem -import im.vector.app.core.ui.list.genericItemHeader -import me.gujun.android.span.span -import javax.inject.Inject - -class KeyRequestEpoxyController @Inject constructor( - private val stringProvider: StringProvider -) : TypedEpoxyController() { - - interface InteractionListener { - // fun didTap(data: UserAccountData) - } - - var outgoing = true - - var interactionListener: InteractionListener? = null - - override fun buildModels(data: KeyRequestListViewState?) { - if (outgoing) { - buildOutgoing(data) - } else { - buildIncoming(data) - } - } - - private fun buildIncoming(data: KeyRequestListViewState?) { - data?.incomingRequests?.let { async -> - when (async) { - is Uninitialized, - is Loading -> { - loadingItem { - id("loadingOutgoing") - loadingText(stringProvider.getString(R.string.loading)) - } - } - is Fail -> { - genericItem { - id("failOutgoing") - title(async.error.localizedMessage) - } - } - is Success -> { - if (async.invoke().isEmpty()) { - genericFooterItem { - id("empty") - text(stringProvider.getString(R.string.no_result_placeholder)) - } - return - } - val requestList = async.invoke().groupBy { it.userId } - - requestList.forEach { - genericItemHeader { - id(it.key) - text("From user: ${it.key}") - } - it.value.forEach { roomKeyRequest -> - genericItem { - id(roomKeyRequest.requestId) - title(roomKeyRequest.requestId) - description( - span { - span("sessionId:") { - textStyle = "bold" - } - span("\nFrom device:") { - textStyle = "bold" - } - +"${roomKeyRequest.deviceId}" - +"\n${roomKeyRequest.state.name}" - } - ) - } - } - } - } - }.exhaustive - } - } - - private fun buildOutgoing(data: KeyRequestListViewState?) { - data?.outgoingRoomKeyRequests?.let { async -> - when (async) { - is Uninitialized, - is Loading -> { - loadingItem { - id("loadingOutgoing") - loadingText(stringProvider.getString(R.string.loading)) - } - } - is Fail -> { - genericItem { - id("failOutgoing") - title(async.error.localizedMessage) - } - } - is Success -> { - if (async.invoke().isEmpty()) { - genericFooterItem { - id("empty") - text(stringProvider.getString(R.string.no_result_placeholder)) - } - return - } - - val requestList = async.invoke().groupBy { it.roomId } - - requestList.forEach { - genericItemHeader { - id(it.key) - text("roomId: ${it.key}") - } - it.value.forEach { roomKeyRequest -> - genericItem { - id(roomKeyRequest.requestId) - title(roomKeyRequest.requestId) - description( - span { - span("sessionId:\n") { - textStyle = "bold" - } - +"${roomKeyRequest.sessionId}" - span("\nstate:") { - textStyle = "bold" - } - +"\n${roomKeyRequest.state.name}" - } - ) - } - } - } - } - }.exhaustive - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt index 22093763e1..0b0b923a48 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt @@ -17,11 +17,11 @@ package im.vector.app.features.settings.devtools import androidx.lifecycle.viewModelScope +import androidx.paging.PagedList import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted @@ -33,10 +33,11 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest +import org.matrix.android.sdk.rx.asObservable data class KeyRequestListViewState( - val incomingRequests: Async> = Uninitialized, - val outgoingRoomKeyRequests: Async> = Uninitialized + val incomingRequests: Async> = Uninitialized, + val outgoingRoomKeyRequests: Async> = Uninitialized ) : MvRxState class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState, @@ -49,20 +50,16 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState fun refresh() { viewModelScope.launch { - session.cryptoService().getOutgoingRoomKeyRequests().let { - setState { - copy( - outgoingRoomKeyRequests = Success(it) - ) - } - } - session.cryptoService().getIncomingRoomKeyRequests().let { - setState { - copy( - incomingRequests = Success(it) - ) - } - } + session.cryptoService().getOutgoingRoomKeyRequestsPaged().asObservable() + .execute { + copy(outgoingRoomKeyRequests = it) + } + + session.cryptoService().getIncomingRoomKeyRequestsPaged() + .asObservable() + .execute { + copy(incomingRequests = it) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt new file mode 100644 index 0000000000..a7d5e8f1ac --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devtools + +import android.net.Uri +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.platform.VectorViewModelAction +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session + +sealed class KeyRequestAction : VectorViewModelAction { + data class ExportAudit(val uri: Uri) : KeyRequestAction() +} + +sealed class KeyRequestEvents : VectorViewEvents { + data class SaveAudit(val uri: Uri, val raw: String) : KeyRequestEvents() +} + +data class KeyRequestViewState( + val exporting: Async = Uninitialized +) : MvRxState + +class KeyRequestViewModel @AssistedInject constructor( + @Assisted initialState: KeyRequestViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: KeyRequestViewState): KeyRequestViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: KeyRequestViewState): KeyRequestViewModel? { + val fragment: KeyRequestsFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + override fun handle(action: KeyRequestAction) { + when (action) { + is KeyRequestAction.ExportAudit -> exportAudit(action) + }.exhaustive + } + + private fun exportAudit(action: KeyRequestAction.ExportAudit) { + setState { + copy(exporting = Loading()) + } + viewModelScope.launch(Dispatchers.IO) { + try { + // this can take long + val eventList = session.cryptoService().getGossipingEvents() + // clean it a bit to + val raw = GossipingEventsSerializer().serialize(eventList) + setState { + copy(exporting = Success(Unit)) + } + _viewEvents.post(KeyRequestEvents.SaveAudit(action.uri, raw)) + } catch (error: Throwable) { + setState { + copy(exporting = Fail(error)) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt index 08016c66ac..ff91d9bba3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt @@ -16,20 +16,31 @@ package im.vector.app.features.settings.devtools +import android.app.Activity import android.os.Bundle +import android.view.MenuItem import android.view.View +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import com.google.android.material.tabs.TabLayoutMediator import im.vector.app.R +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.selectTxtFileToWrite import kotlinx.android.synthetic.main.fragment_devtool_keyrequests.* +import org.matrix.android.sdk.api.extensions.tryOrNull import javax.inject.Inject -class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { +class KeyRequestsFragment @Inject constructor( + val viewModelFactory: KeyRequestViewModel.Factory) : VectorBaseFragment() { override fun getLayoutResId(): Int = R.layout.fragment_devtool_keyrequests @@ -40,6 +51,10 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { private var mPagerAdapter: KeyReqPagerAdapter? = null + private val viewModel: KeyRequestViewModel by fragmentViewModel() + + override fun getMenuRes(): Int = R.menu.menu_audit + private val pageAdapterListener = object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { invalidateOptionsMenu() @@ -53,6 +68,13 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { } } + override fun invalidate() = withState(viewModel) { + when (it.exporting) { + is Loading -> exportWaitingView.isVisible = true + else -> exportWaitingView.isVisible = false + } + } + override fun onDestroy() { invalidateOptionsMenu() super.onDestroy() @@ -77,6 +99,17 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { } } }.attach() + + viewModel.observeViewEvents { + when (it) { + is KeyRequestEvents.SaveAudit -> { + tryOrNull { + requireContext().contentResolver?.openOutputStream(it.uri) + ?.use { os -> os.write(it.raw.toByteArray()) } + } + } + }.exhaustive + } } override fun onDestroyView() { @@ -85,6 +118,28 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { super.onDestroyView() } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.audit_export) { + selectTxtFileToWrite( + activity = requireActivity(), + activityResultLauncher = epxortAuditForActivityResult, + defaultFileName = "audit-export_${System.currentTimeMillis()}.txt", + chooserHint = "Export Audit" + ) + return true + } + return super.onOptionsItemSelected(item) + } + + private val epxortAuditForActivityResult = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + val uri = activityResult.data?.data + if (uri != null) { + viewModel.handle(KeyRequestAction.ExportAudit(uri)) + } + } + } + private inner class KeyReqPagerAdapter(fa: Fragment) : FragmentStateAdapter(fa) { override fun getItemCount(): Int = 3 diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt index 60e73fb74d..a82b5dd6c9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt @@ -24,21 +24,19 @@ import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.resources.ColorProvider import kotlinx.android.synthetic.main.fragment_generic_recycler.* import javax.inject.Inject class OutgoingKeyRequestListFragment @Inject constructor( val viewModelFactory: KeyRequestListViewModel.Factory, - private val epoxyController: KeyRequestEpoxyController, - private val colorProvider: ColorProvider + private val epoxyController: OutgoingKeyRequestPagedController ) : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_generic_recycler private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class) override fun invalidate() = withState(viewModel) { state -> - epoxyController.setData(state) + epoxyController.submitList(state.outgoingRoomKeyRequests.invoke()) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt new file mode 100644 index 0000000000..c2a3bc9827 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.settings.devtools + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.paging.PagedListEpoxyController +import im.vector.app.core.ui.list.GenericItem_ +import im.vector.app.core.utils.createUIHandler +import me.gujun.android.span.span +import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest +import javax.inject.Inject + +class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController( + // Important it must match the PageList builder notify Looper + modelBuildingHandler = createUIHandler() +) { + + interface InteractionListener { + // fun didTap(data: UserAccountData) + } + + var interactionListener: InteractionListener? = null + + override fun buildItemModel(currentPosition: Int, item: OutgoingRoomKeyRequest?): EpoxyModel<*> { + val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) } + + return GenericItem_().apply { + id(roomKeyRequest.requestId) + title(roomKeyRequest.requestId) + description( + span { + span("roomId: ") { + textStyle = "bold" + } + +"${roomKeyRequest.roomId}" + + span("\nsessionId: ") { + textStyle = "bold" + } + +"${roomKeyRequest.sessionId}" + span("\nstate: ") { + textStyle = "bold" + } + +roomKeyRequest.state.name + } + ) + } + } +} diff --git a/vector/src/main/res/layout/activity_bug_report.xml b/vector/src/main/res/layout/activity_bug_report.xml index 3bfb1f5429..34169f44f8 100644 --- a/vector/src/main/res/layout/activity_bug_report.xml +++ b/vector/src/main/res/layout/activity_bug_report.xml @@ -125,6 +125,15 @@ android:checked="true" android:text="@string/send_bug_report_include_crash_logs" /> + + - + android:layout_height="0dp" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_audit.xml b/vector/src/main/res/menu/menu_audit.xml new file mode 100644 index 0000000000..1c0d2f9989 --- /dev/null +++ b/vector/src/main/res/menu/menu_audit.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 16ebca5f20..c5a11d5ab4 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -192,6 +192,7 @@ Send logs Send crash logs + Send key share requests history Send screenshot Report bug Please describe the bug. What did you do? What did you expect to happen? What actually happened? @@ -2305,6 +2306,7 @@ Element Android Key Requests + Export Audit Unlock encrypted messages history