diff --git a/CHANGES.md b/CHANGES.md index ba3ad08037..8692a2cff8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Improvements 🙌: - Improve initial sync performance (#983) - PIP support for Jitsi call (#2418) - Add tooltip for room quick actions + - Pre-share session keys when opening a room or start typing (#2771) Bugfix 🐛: - Try to fix crash about UrlPreview (#2640) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt new file mode 100644 index 0000000000..122584142e --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt @@ -0,0 +1,103 @@ +/* + * 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.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +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.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent +import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class PreShareKeysTest : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun ensure_outbound_session_happy_path() { + val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val e2eRoomID = testData.roomId + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + + // clear any outbound session + aliceSession.cryptoService().discardOutboundSession(e2eRoomID) + + val preShareCount = bobSession.cryptoService().getGossipingEvents().count { + it.senderId == aliceSession.myUserId + && it.getClearType() == EventType.ROOM_KEY + } + + assertEquals(0, preShareCount, "Bob should not have receive any key from alice at this point") + Log.d("#Test", "Room Key Received from alice $preShareCount") + + // Force presharing of new outbound key + mTestHelper.doSync { + aliceSession.cryptoService().prepareToEncrypt(e2eRoomID, it) + } + + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + val newGossipCount = bobSession.cryptoService().getGossipingEvents().count { + it.senderId == aliceSession.myUserId + && it.getClearType() == EventType.ROOM_KEY + } + newGossipCount > preShareCount + } + } + + val latest = bobSession.cryptoService().getGossipingEvents().lastOrNull { + it.senderId == aliceSession.myUserId + && it.getClearType() == EventType.ROOM_KEY + } + + val content = latest?.getClearContent().toModel() + assertNotNull(content, "Bob should have received and decrypted a room key event from alice") + assertEquals(e2eRoomID, content.roomId, "Wrong room") + val megolmSessionId = content.sessionId!! + + val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId) + .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId) + + assertEquals(0, sharedIndex, "The session received by bob should match what alice sent") + + // Just send a real message as test + val sentEvent = mTestHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first() + + assertEquals(megolmSessionId, sentEvent.root.content.toModel()?.sessionId, "Unexpected megolm session") + mTestHelper.waitWithLatch { latch -> + mTestHelper.retryPeriodicallyWithLatch(latch) { + bobSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE + } + } + + mTestHelper.signOutAndClose(aliceSession) + mTestHelper.signOutAndClose(bobSession) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index 80cc14fcb6..b2516ea2be 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -51,7 +51,7 @@ class WithHeldTests : InstrumentedTest { // ============================= val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - val bobSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) // Initialize cross signing on both mCryptoTestHelper.initializeCrossSigning(aliceSession) 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 eead9b4ab7..1b7a5243e2 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 @@ -156,4 +156,10 @@ interface CryptoService { fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? fun logDbUsageInfo() + + /** + * Perform any background tasks that can be done before a message is ready to + * send, in order to speed up sending of the message. + */ + fun prepareToEncrypt(roomId: String, callback: MatrixCallback) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt index 1251fd9857..6581247b90 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt @@ -30,4 +30,10 @@ interface RoomCryptoService { * Enable encryption of the room */ suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM) + + /** + * Ensures all members of the room are loaded and outbound session keys are shared. + * If this method is not called, CryptoService will ensure it before sending events. + */ + suspend fun prepareToEncrypt() } 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 67229a5eae..17d25736eb 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 @@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory @@ -97,7 +98,6 @@ 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 /** @@ -667,7 +667,12 @@ internal class DefaultCryptoService @Inject constructor( override fun discardOutboundSession(roomId: String) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - roomEncryptorsStore.get(roomId)?.discardSessionKey() + val roomEncryptor = roomEncryptorsStore.get(roomId) + if (roomEncryptor is IMXGroupEncryption) { + roomEncryptor.discardSessionKey() + } else { + Timber.e("## CRYPTO | discardOutboundSession() for:$roomId: Unable to handle IMXGroupEncryption") + } } } @@ -703,7 +708,7 @@ internal class DefaultCryptoService @Inject constructor( */ @Throws(MXCryptoError::class) private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return eventDecryptor.decryptEvent(event, timeline) + return eventDecryptor.decryptEvent(event, timeline) } /** @@ -1290,6 +1295,43 @@ internal class DefaultCryptoService @Inject constructor( cryptoStore.logDbUsageInfo() } + override fun prepareToEncrypt(roomId: String, callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.d("## CRYPTO | prepareToEncrypt() : Check room members up to date") + // Ensure to load all room members + try { + loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + } catch (failure: Throwable) { + Timber.e("## CRYPTO | prepareToEncrypt() : Failed to load room members") + callback.onFailure(failure) + return@launch + } + + val userIds = getRoomUserIds(roomId) + val alg = roomEncryptorsStore.get(roomId) + ?: getEncryptionAlgorithm(roomId) + ?.let { setEncryptionInRoom(roomId, it, false, userIds) } + ?.let { roomEncryptorsStore.get(roomId) } + + if (alg == null) { + val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) + Timber.e("## CRYPTO | prepareToEncrypt() : $reason") + callback.onFailure(IllegalArgumentException("Missing algorithm")) + return@launch + } + + runCatching { + (alg as? IMXGroupEncryption)?.preshareKey(userIds) + }.fold( + { callback.onSuccess(Unit) }, + { + Timber.e("## CRYPTO | prepareToEncrypt() failed.") + callback.onFailure(it) + } + ) + } + } + /* ========================================================================================== * For test only * ========================================================================================== */ 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 index 92b7728890..32324896fa 100644 --- 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 @@ -105,7 +105,7 @@ internal class EventDecryptor @Inject constructor( try { return alg.decryptEvent(event, timeline) } catch (mxCryptoError: MXCryptoError) { - Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") + Timber.v("## 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) { 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 c075c90eb9..4a0a274f4f 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 @@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListen 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.algorithms.IMXGroupEncryption import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent @@ -290,12 +291,16 @@ internal class IncomingGossipingRequestManager @Inject constructor( .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey) + if (roomEncryptor is IMXGroupEncryption) { + val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey) - if (isSuccess) { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + if (isSuccess) { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } else { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS) + } } else { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS) + Timber.e("## CRYPTO | handleKeyRequestFromOtherUser() from:$userId: Unable to handle IMXGroupEncryption.reshareKey for $alg") } } cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt index fc3ea08a21..5294429198 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt @@ -32,34 +32,4 @@ internal interface IMXEncrypting { * @return the encrypted content */ suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List): Content - - /** - * In Megolm, each recipient maintains a record of the ratchet value which allows - * them to decrypt any messages sent in the session after the corresponding point - * in the conversation. If this value is compromised, an attacker can similarly - * decrypt past messages which were encrypted by a key derived from the - * compromised or subsequent ratchet values. This gives 'partial' forward - * secrecy. - * - * To mitigate this issue, the application should offer the user the option to - * discard historical conversations, by winding forward any stored ratchet values, - * or discarding sessions altogether. - */ - fun discardSessionKey() - - /** - * Re-shares a session key with devices if the key has already been - * sent to them. - * - * @param sessionId The id of the outbound session to share. - * @param userId The id of the user who owns the target device. - * @param deviceId The id of the target device. - * @param senderKey The key of the originating device for the session. - * - * @return true in case of success - */ - suspend fun reshareKey(sessionId: String, - userId: String, - deviceId: String, - senderKey: String): Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt new file mode 100644 index 0000000000..1fd5061a65 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt @@ -0,0 +1,52 @@ +/* + * 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.algorithms + +internal interface IMXGroupEncryption { + + /** + * In Megolm, each recipient maintains a record of the ratchet value which allows + * them to decrypt any messages sent in the session after the corresponding point + * in the conversation. If this value is compromised, an attacker can similarly + * decrypt past messages which were encrypted by a key derived from the + * compromised or subsequent ratchet values. This gives 'partial' forward + * secrecy. + * + * To mitigate this issue, the application should offer the user the option to + * discard historical conversations, by winding forward any stored ratchet values, + * or discarding sessions altogether. + */ + fun discardSessionKey() + + suspend fun preshareKey(userIds: List) + + /** + * Re-shares a session key with devices if the key has already been + * sent to them. + * + * @param sessionId The id of the outbound session to share. + * @param userId The id of the user who owns the target device. + * @param deviceId The id of the target device. + * @param senderKey The key of the originating device for the session. + * + * @return true in case of success + */ + suspend fun reshareKey(sessionId: String, + userId: String, + deviceId: String, + senderKey: String): Boolean +} 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 fa8acafb83..6b91c0b859 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 @@ -18,8 +18,6 @@ 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 @@ -30,6 +28,7 @@ import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap @@ -39,8 +38,6 @@ import org.matrix.android.sdk.internal.crypto.model.forEach import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository 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.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 @@ -54,14 +51,14 @@ internal class MXMegolmEncryption( private val cryptoStore: IMXCryptoStore, private val deviceListManager: DeviceListManager, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val credentials: Credentials, + private val userId: String, + private val deviceId: String, private val sendToDeviceTask: SendToDeviceTask, private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - private val taskExecutor: TaskExecutor, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope -) : IMXEncrypting { +) : IMXEncrypting, IMXGroupEncryption { // OutboundSessionInfo. Null if we haven't yet started setting one up. Note // that even if this is non-null, it may not be ready for use (in which @@ -93,6 +90,7 @@ internal class MXMegolmEncryption( // annoyingly we have to serialize again the saved outbound session to store message index :/ // if not we would see duplicate message index errors olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId) + Timber.v("## CRYPTO | encryptEventContent: Finished in ${System.currentTimeMillis() - ts} millis") } } @@ -117,6 +115,16 @@ internal class MXMegolmEncryption( olmDevice.discardOutboundGroupSessionForRoom(roomId) } + override suspend fun preshareKey(userIds: List) { + val ts = System.currentTimeMillis() + Timber.v("## CRYPTO | preshareKey : getDevicesInRoom") + val devices = getDevicesInRoom(userIds) + val outboundSession = ensureOutboundSession(devices.allowedDevices) + + notifyWithheldForSession(devices.withHeldDevices, outboundSession) + + Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis") + } /** * Prepare a new session. * @@ -252,7 +260,7 @@ internal class MXMegolmEncryption( continue } - Timber.i("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") + Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) haveTargets = true } @@ -263,12 +271,14 @@ internal class MXMegolmEncryption( // attempted to share with) rather than the contentMap (those we did // share with), because we don't want to try to claim a one-time-key // for dead devices on every message. + val gossipingEventBuffer = arrayListOf() for ((userId, devicesToShareWith) in devicesByUser) { for ((deviceId) in devicesToShareWith) { session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex) - cryptoStore.saveGossipingEvent(Event( + gossipingEventBuffer.add( + Event( type = EventType.ROOM_KEY, - senderId = credentials.userId, + senderId = this.userId, content = submap.apply { this["session_key"] = "" // we add a fake key for trail @@ -278,6 +288,8 @@ internal class MXMegolmEncryption( } } + cryptoStore.saveGossipingEvents(gossipingEventBuffer) + if (haveTargets) { t0 = System.currentTimeMillis() Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target") @@ -294,8 +306,11 @@ internal class MXMegolmEncryption( } } - private fun notifyKeyWithHeld(targets: List, sessionId: String, senderKey: String?, code: WithHeldCode) { - Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId ") + private suspend fun notifyKeyWithHeld(targets: List, + sessionId: String, + senderKey: String?, + code: WithHeldCode) { + Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId and code $code") val withHeldContent = RoomKeyWithHeldContent( roomId = roomId, senderKey = senderKey, @@ -311,13 +326,11 @@ internal class MXMegolmEncryption( } } ) - sendToDeviceTask.configureWith(params) { - callback = object : MatrixCallback { - override fun onFailure(failure: Throwable) { - Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") - } - } - }.executeBy(taskExecutor) + try { + sendToDeviceTask.execute(params) + } catch (failure: Throwable) { + Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") + } } /** @@ -343,7 +356,7 @@ internal class MXMegolmEncryption( // Include our device ID so that recipients can send us a // m.new_device message if they don't have our session key. - map["device_id"] = credentials.deviceId!! + map["device_id"] = deviceId session.useCount++ return map } 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 f0cc15fb63..9f6312ea97 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 @@ -17,7 +17,6 @@ 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 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction @@ -26,7 +25,8 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupServic import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository 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.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject @@ -36,29 +36,29 @@ internal class MXMegolmEncryptionFactory @Inject constructor( private val cryptoStore: IMXCryptoStore, private val deviceListManager: DeviceListManager, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val credentials: Credentials, + @UserId private val userId: String, + @DeviceId private val deviceId: String?, private val sendToDeviceTask: SendToDeviceTask, private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - private val taskExecutor: TaskExecutor, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope) { fun create(roomId: String): MXMegolmEncryption { return MXMegolmEncryption( - roomId, - olmDevice, - defaultKeysBackupService, - cryptoStore, - deviceListManager, - ensureOlmSessionsForDevicesAction, - credentials, - sendToDeviceTask, - messageEncrypter, - warnOnUnknownDevicesRepository, - taskExecutor, - coroutineDispatchers, - cryptoCoroutineScope + roomId = roomId, + olmDevice = olmDevice, + defaultKeysBackupService = defaultKeysBackupService, + cryptoStore = cryptoStore, + deviceListManager = deviceListManager, + ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction, + userId = userId, + deviceId = deviceId!!, + sendToDeviceTask = sendToDeviceTask, + messageEncrypter = messageEncrypter, + warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository, + coroutineDispatchers = coroutineDispatchers, + cryptoCoroutineScope = cryptoCoroutineScope ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt index 9acc9bc1b2..65f78e11f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -76,13 +76,4 @@ internal class MXOlmEncryption( deviceListManager.downloadKeys(users, false) ensureOlmSessionsForUsersAction.handle(users) } - - override fun discardSessionKey() { - // No need for olm - } - - override suspend fun reshareKey(sessionId: String, userId: String, deviceId: String, senderKey: String): Boolean { - // No need for olm - return false - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 7a819250cf..8e817ec31a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSourc import org.matrix.android.sdk.internal.session.search.SearchTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.util.awaitCallback import java.security.InvalidParameterException import javax.inject.Inject @@ -104,6 +105,12 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, return cryptoService.shouldEncryptForInvitedMembers(roomId) } + override suspend fun prepareToEncrypt() { + awaitCallback { + cryptoService.prepareToEncrypt(roomId, it) + } + } + override suspend fun enableEncryption(algorithm: String) { when { isEncrypted() -> { diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index a5127dc8aa..567e24a849 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===89 +enum class===90 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/build.gradle b/vector/build.gradle index 5543d27d95..dc2a16ccc5 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -136,6 +136,8 @@ android { buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\"" resValue "string", "build_number", "\"${buildNumber}\"" + buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Keep abiFilter for the universalApk diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index f8cda2c417..dd94be77ba 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -205,7 +205,7 @@ class VectorApplication : } } - override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION) + override fun providesMatrixConfiguration() = MatrixConfiguration(applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION) override fun getWorkManagerConfiguration(): WorkConfiguration { return WorkConfiguration.Builder() diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysrequest/OutboundSessionKeySharingStrategy.kt b/vector/src/main/java/im/vector/app/features/crypto/keysrequest/OutboundSessionKeySharingStrategy.kt new file mode 100644 index 0000000000..19c62ed572 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/keysrequest/OutboundSessionKeySharingStrategy.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 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.crypto.keysrequest + +enum class OutboundSessionKeySharingStrategy { + /** + * Keys will be sent for the first time when the first message is sent + */ + WhenSendingEvent, + + /** + * Keys will be sent for the first time when the timeline displayed + */ + WhenEnteringRoom, + + /** + * Keys will be sent for the first time when a typing started + */ + WhenTyping +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 98ad6c454c..efc495a379 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -104,4 +104,6 @@ sealed class RoomDetailAction : VectorViewModelAction { // Preview URL data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction() + + data class ComposerFocusChange(val focused: Boolean) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index c511c9e666..d51ed08083 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -70,6 +70,7 @@ import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding3.view.focusChanges import com.jakewharton.rxbinding3.widget.textChanges import com.vanniktech.emoji.EmojiPopup import im.vector.app.R @@ -1144,6 +1145,12 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.UserIsTyping(it)) } .disposeOnDestroyView() + + views.composerLayout.views.composerEditText.focusChanges() + .subscribe { + roomDetailViewModel.handle(RoomDetailAction.ComposerFocusChange(it)) + } + .disposeOnDestroyView() } private fun sendUri(uri: Uri): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 601891b15a..935b4ca2f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -19,15 +19,20 @@ package im.vector.app.features.home.room.detail import android.net.Uri import androidx.annotation.IdRes 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.MvRxViewModelFactory import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.PublishRelay import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -36,6 +41,7 @@ import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand +import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator @@ -140,6 +146,8 @@ class RoomDetailViewModel @AssistedInject constructor( private var trackUnreadMessages = AtomicBoolean(false) private var mostRecentDisplayedEvent: TimelineEvent? = null + private var prepareToEncrypt: Async = Uninitialized + @AssistedFactory interface Factory { fun create(initialState: RoomDetailViewState): RoomDetailViewModel @@ -179,6 +187,27 @@ class RoomDetailViewModel @AssistedInject constructor( callManager.addPstnSupportListener(this) callManager.checkForPSTNSupportIfNeeded() chatEffectManager.delegate = this + + // Ensure to share the outbound session keys with all members + if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) { + prepareForEncryption() + } + } + + private fun prepareForEncryption() { + // check if there is not already a call made, or if there has been an error + if (prepareToEncrypt.shouldLoad) { + prepareToEncrypt = Loading() + viewModelScope.launch { + runCatching { + room.prepareToEncrypt() + }.fold({ + prepareToEncrypt = Success(Unit) + }, { + prepareToEncrypt = Fail(it) + }) + } + } } private fun observePowerLevel() { @@ -234,6 +263,7 @@ class RoomDetailViewModel @AssistedInject constructor( override fun handle(action: RoomDetailAction) { when (action) { is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) + is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) is RoomDetailAction.SaveDraft -> handleSaveDraft(action) is RoomDetailAction.SendMessage -> handleSendMessage(action) is RoomDetailAction.SendMedia -> handleSendMedia(action) @@ -593,6 +623,16 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { + // Ensure outbound session keys + if (OutboundSessionKeySharingStrategy.WhenTyping == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) { + if (action.focused) { + // Should we add some rate limit here, or do it only once per model lifecycle? + prepareForEncryption() + } + } + } + private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() ?: return