Merge pull request #2772 from vector-im/feature/ons/share_keys_config
Pre-share session keys when opening a room or start typing
This commit is contained in:
commit
94dc9fca9d
|
@ -10,6 +10,7 @@ Improvements 🙌:
|
||||||
- Improve initial sync performance (#983)
|
- Improve initial sync performance (#983)
|
||||||
- PIP support for Jitsi call (#2418)
|
- PIP support for Jitsi call (#2418)
|
||||||
- Add tooltip for room quick actions
|
- Add tooltip for room quick actions
|
||||||
|
- Pre-share session keys when opening a room or start typing (#2771)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Try to fix crash about UrlPreview (#2640)
|
- Try to fix crash about UrlPreview (#2640)
|
||||||
|
|
|
@ -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<Unit> {
|
||||||
|
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<RoomKeyContent>()
|
||||||
|
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<EncryptedEventContent>()?.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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,7 +51,7 @@ class WithHeldTests : InstrumentedTest {
|
||||||
// =============================
|
// =============================
|
||||||
|
|
||||||
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
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
|
// Initialize cross signing on both
|
||||||
mCryptoTestHelper.initializeCrossSigning(aliceSession)
|
mCryptoTestHelper.initializeCrossSigning(aliceSession)
|
||||||
|
|
|
@ -156,4 +156,10 @@ interface CryptoService {
|
||||||
fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
|
fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
|
||||||
|
|
||||||
fun logDbUsageInfo()
|
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<Unit>)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,4 +30,10 @@ interface RoomCryptoService {
|
||||||
* Enable encryption of the room
|
* Enable encryption of the room
|
||||||
*/
|
*/
|
||||||
suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM)
|
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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.MegolmSessionDataImporter
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||||
|
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
|
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.megolm.MXMegolmEncryptionFactory
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||||
|
@ -97,7 +98,6 @@ import org.matrix.olm.OlmManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.jvm.Throws
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -667,7 +667,12 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
|
|
||||||
override fun discardOutboundSession(roomId: String) {
|
override fun discardOutboundSession(roomId: String) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1290,6 +1295,43 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
cryptoStore.logDbUsageInfo()
|
cryptoStore.logDbUsageInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>) {
|
||||||
|
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
|
* For test only
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
|
@ -105,7 +105,7 @@ internal class EventDecryptor @Inject constructor(
|
||||||
try {
|
try {
|
||||||
return alg.decryptEvent(event, timeline)
|
return alg.decryptEvent(event, timeline)
|
||||||
} catch (mxCryptoError: MXCryptoError) {
|
} 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 (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
||||||
if (mxCryptoError is MXCryptoError.Base
|
if (mxCryptoError is MXCryptoError.Base
|
||||||
&& mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
&& mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
||||||
|
|
|
@ -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.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
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.crosssigning.toBase64NoPadding
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent
|
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent
|
||||||
|
@ -290,6 +291,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
|
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
if (roomEncryptor is IMXGroupEncryption) {
|
||||||
val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey)
|
val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey)
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
|
@ -297,6 +299,9 @@ internal class IncomingGossipingRequestManager @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("## CRYPTO | handleKeyRequestFromOtherUser() from:$userId: Unable to handle IMXGroupEncryption.reshareKey for $alg")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED)
|
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,34 +32,4 @@ internal interface IMXEncrypting {
|
||||||
* @return the encrypted content
|
* @return the encrypted content
|
||||||
*/
|
*/
|
||||||
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
|
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): 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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
|
@ -18,8 +18,6 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
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.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
@ -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.EnsureOlmSessionsForDevicesAction
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||||
|
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
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.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
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.repository.WarnOnUnknownDeviceRepository
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||||
import org.matrix.android.sdk.internal.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.JsonCanonicalizer
|
||||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.internal.util.convertToUTF8
|
import org.matrix.android.sdk.internal.util.convertToUTF8
|
||||||
|
@ -54,14 +51,14 @@ internal class MXMegolmEncryption(
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||||
private val credentials: Credentials,
|
private val userId: String,
|
||||||
|
private val deviceId: String,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||||
private val taskExecutor: TaskExecutor,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : IMXEncrypting {
|
) : IMXEncrypting, IMXGroupEncryption {
|
||||||
|
|
||||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
// 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
|
// 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 :/
|
// annoyingly we have to serialize again the saved outbound session to store message index :/
|
||||||
// if not we would see duplicate message index errors
|
// if not we would see duplicate message index errors
|
||||||
olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId)
|
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)
|
olmDevice.discardOutboundGroupSessionForRoom(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun preshareKey(userIds: List<String>) {
|
||||||
|
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.
|
* Prepare a new session.
|
||||||
*
|
*
|
||||||
|
@ -252,7 +260,7 @@ internal class MXMegolmEncryption(
|
||||||
|
|
||||||
continue
|
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)))
|
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
|
||||||
haveTargets = true
|
haveTargets = true
|
||||||
}
|
}
|
||||||
|
@ -263,12 +271,14 @@ internal class MXMegolmEncryption(
|
||||||
// attempted to share with) rather than the contentMap (those we did
|
// 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
|
// share with), because we don't want to try to claim a one-time-key
|
||||||
// for dead devices on every message.
|
// for dead devices on every message.
|
||||||
|
val gossipingEventBuffer = arrayListOf<Event>()
|
||||||
for ((userId, devicesToShareWith) in devicesByUser) {
|
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||||
for ((deviceId) in devicesToShareWith) {
|
for ((deviceId) in devicesToShareWith) {
|
||||||
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
|
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
|
||||||
cryptoStore.saveGossipingEvent(Event(
|
gossipingEventBuffer.add(
|
||||||
|
Event(
|
||||||
type = EventType.ROOM_KEY,
|
type = EventType.ROOM_KEY,
|
||||||
senderId = credentials.userId,
|
senderId = this.userId,
|
||||||
content = submap.apply {
|
content = submap.apply {
|
||||||
this["session_key"] = ""
|
this["session_key"] = ""
|
||||||
// we add a fake key for trail
|
// we add a fake key for trail
|
||||||
|
@ -278,6 +288,8 @@ internal class MXMegolmEncryption(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cryptoStore.saveGossipingEvents(gossipingEventBuffer)
|
||||||
|
|
||||||
if (haveTargets) {
|
if (haveTargets) {
|
||||||
t0 = System.currentTimeMillis()
|
t0 = System.currentTimeMillis()
|
||||||
Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target")
|
Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target")
|
||||||
|
@ -294,8 +306,11 @@ internal class MXMegolmEncryption(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyKeyWithHeld(targets: List<UserDevice>, sessionId: String, senderKey: String?, code: WithHeldCode) {
|
private suspend fun notifyKeyWithHeld(targets: List<UserDevice>,
|
||||||
Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId ")
|
sessionId: String,
|
||||||
|
senderKey: String?,
|
||||||
|
code: WithHeldCode) {
|
||||||
|
Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId and code $code")
|
||||||
val withHeldContent = RoomKeyWithHeldContent(
|
val withHeldContent = RoomKeyWithHeldContent(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
senderKey = senderKey,
|
senderKey = senderKey,
|
||||||
|
@ -311,14 +326,12 @@ internal class MXMegolmEncryption(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
sendToDeviceTask.configureWith(params) {
|
try {
|
||||||
callback = object : MatrixCallback<Unit> {
|
sendToDeviceTask.execute(params)
|
||||||
override fun onFailure(failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
|
Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* process the pending encryptions
|
* process the pending encryptions
|
||||||
|
@ -343,7 +356,7 @@ internal class MXMegolmEncryption(
|
||||||
|
|
||||||
// Include our device ID so that recipients can send us a
|
// Include our device ID so that recipients can send us a
|
||||||
// m.new_device message if they don't have our session key.
|
// m.new_device message if they don't have our session key.
|
||||||
map["device_id"] = credentials.deviceId!!
|
map["device_id"] = deviceId
|
||||||
session.useCount++
|
session.useCount++
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
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.DeviceListManager
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
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.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.repository.WarnOnUnknownDeviceRepository
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||||
import org.matrix.android.sdk.internal.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 org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -36,29 +36,29 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||||
private val credentials: Credentials,
|
@UserId private val userId: String,
|
||||||
|
@DeviceId private val deviceId: String?,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||||
private val taskExecutor: TaskExecutor,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope) {
|
private val cryptoCoroutineScope: CoroutineScope) {
|
||||||
|
|
||||||
fun create(roomId: String): MXMegolmEncryption {
|
fun create(roomId: String): MXMegolmEncryption {
|
||||||
return MXMegolmEncryption(
|
return MXMegolmEncryption(
|
||||||
roomId,
|
roomId = roomId,
|
||||||
olmDevice,
|
olmDevice = olmDevice,
|
||||||
defaultKeysBackupService,
|
defaultKeysBackupService = defaultKeysBackupService,
|
||||||
cryptoStore,
|
cryptoStore = cryptoStore,
|
||||||
deviceListManager,
|
deviceListManager = deviceListManager,
|
||||||
ensureOlmSessionsForDevicesAction,
|
ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction,
|
||||||
credentials,
|
userId = userId,
|
||||||
sendToDeviceTask,
|
deviceId = deviceId!!,
|
||||||
messageEncrypter,
|
sendToDeviceTask = sendToDeviceTask,
|
||||||
warnOnUnknownDevicesRepository,
|
messageEncrypter = messageEncrypter,
|
||||||
taskExecutor,
|
warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository,
|
||||||
coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
cryptoCoroutineScope
|
cryptoCoroutineScope = cryptoCoroutineScope
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,13 +76,4 @@ internal class MXOlmEncryption(
|
||||||
deviceListManager.downloadKeys(users, false)
|
deviceListManager.downloadKeys(users, false)
|
||||||
ensureOlmSessionsForUsersAction.handle(users)
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.session.search.SearchTask
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
import org.matrix.android.sdk.internal.task.configureWith
|
import org.matrix.android.sdk.internal.task.configureWith
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import java.security.InvalidParameterException
|
import java.security.InvalidParameterException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -104,6 +105,12 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
||||||
return cryptoService.shouldEncryptForInvitedMembers(roomId)
|
return cryptoService.shouldEncryptForInvitedMembers(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun prepareToEncrypt() {
|
||||||
|
awaitCallback<Unit> {
|
||||||
|
cryptoService.prepareToEncrypt(roomId, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun enableEncryption(algorithm: String) {
|
override suspend fun enableEncryption(algorithm: String) {
|
||||||
when {
|
when {
|
||||||
isEncrypted() -> {
|
isEncrypted() -> {
|
||||||
|
|
|
@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1
|
||||||
# android\.text\.TextUtils
|
# 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
|
### 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
|
### Do not import temporary legacy classes
|
||||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||||
|
|
|
@ -136,6 +136,8 @@ android {
|
||||||
buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\""
|
buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\""
|
||||||
resValue "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"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
// Keep abiFilter for the universalApk
|
// Keep abiFilter for the universalApk
|
||||||
|
|
|
@ -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 {
|
override fun getWorkManagerConfiguration(): WorkConfiguration {
|
||||||
return WorkConfiguration.Builder()
|
return WorkConfiguration.Builder()
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -104,4 +104,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||||
|
|
||||||
// Preview URL
|
// Preview URL
|
||||||
data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction()
|
data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction()
|
||||||
|
|
||||||
|
data class ComposerFocusChange(val focused: Boolean) : RoomDetailAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ import com.airbnb.mvrx.args
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.jakewharton.rxbinding3.view.focusChanges
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
import com.jakewharton.rxbinding3.widget.textChanges
|
||||||
import com.vanniktech.emoji.EmojiPopup
|
import com.vanniktech.emoji.EmojiPopup
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
@ -1144,6 +1145,12 @@ class RoomDetailFragment @Inject constructor(
|
||||||
roomDetailViewModel.handle(RoomDetailAction.UserIsTyping(it))
|
roomDetailViewModel.handle(RoomDetailAction.UserIsTyping(it))
|
||||||
}
|
}
|
||||||
.disposeOnDestroyView()
|
.disposeOnDestroyView()
|
||||||
|
|
||||||
|
views.composerLayout.views.composerEditText.focusChanges()
|
||||||
|
.subscribe {
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.ComposerFocusChange(it))
|
||||||
|
}
|
||||||
|
.disposeOnDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendUri(uri: Uri): Boolean {
|
private fun sendUri(uri: Uri): Boolean {
|
||||||
|
|
|
@ -19,15 +19,20 @@ package im.vector.app.features.home.room.detail
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay2.PublishRelay
|
import com.jakewharton.rxrelay2.PublishRelay
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
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.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.command.CommandParser
|
import im.vector.app.features.command.CommandParser
|
||||||
import im.vector.app.features.command.ParsedCommand
|
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.createdirect.DirectRoomHelper
|
||||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
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 trackUnreadMessages = AtomicBoolean(false)
|
||||||
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
||||||
|
|
||||||
|
private var prepareToEncrypt: Async<Unit> = Uninitialized
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
|
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
|
||||||
|
@ -179,6 +187,27 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
callManager.addPstnSupportListener(this)
|
callManager.addPstnSupportListener(this)
|
||||||
callManager.checkForPSTNSupportIfNeeded()
|
callManager.checkForPSTNSupportIfNeeded()
|
||||||
chatEffectManager.delegate = this
|
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() {
|
private fun observePowerLevel() {
|
||||||
|
@ -234,6 +263,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||||
override fun handle(action: RoomDetailAction) {
|
override fun handle(action: RoomDetailAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
||||||
|
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
|
||||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||||
is RoomDetailAction.SendMedia -> handleSendMedia(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) {
|
private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
|
||||||
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>() ?: return
|
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>() ?: return
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue