diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index bc286a75be..1e2aa8621d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -432,6 +432,7 @@ class KeyShareTests : InstrumentedTest { .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) // /!\ Stop initial alice session syncing so that it can't reply + aliceSession.cryptoService().enableKeyGossiping(false) aliceSession.stopSync() // Let's now try to request @@ -440,6 +441,7 @@ class KeyShareTests : InstrumentedTest { // Should get a reply from bob and not from alice commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { + // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}") val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } val result = bobReply?.result @@ -453,6 +455,7 @@ class KeyShareTests : InstrumentedTest { assertEquals("The request should not be canceled", OutgoingRoomKeyRequestState.SENT, outgoingReq.state) // let's wake up alice + aliceSession.cryptoService().enableKeyGossiping(true) aliceSession.startSync(true) // We should now get a reply from first session diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt index a0e1011aba..9507ddda65 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt @@ -37,5 +37,5 @@ data class MXCryptoConfig constructor( * Currently megolm keys are requested to the sender device and to all of our devices. * You can limit request only to your sessions by turning this setting to `true` */ - val limitRoomKeyRequestsToMyDevices: Boolean = false + val limitRoomKeyRequestsToMyDevices: Boolean = false, ) 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 98b5235e14..4418b18c73 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 @@ -1192,7 +1192,7 @@ internal class DefaultCryptoService @Inject constructor( */ override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { incomingKeyRequestManager.removeRoomKeysRequestListener(listener) - secretShareManager.addListener(listener) + secretShareManager.removeListener(listener) } /** 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 b91a970fc1..79c5c0bd41 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 @@ -1,918 +1,918 @@ -/* - * 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 androidx.annotation.VisibleForTesting -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE -import org.matrix.android.sdk.api.util.JsonDict -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo -import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper -import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 -import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.JsonCanonicalizer -import org.matrix.android.sdk.internal.util.convertFromUTF8 -import org.matrix.android.sdk.internal.util.convertToUTF8 -import org.matrix.olm.OlmAccount -import org.matrix.olm.OlmException -import org.matrix.olm.OlmMessage -import org.matrix.olm.OlmOutboundGroupSession -import org.matrix.olm.OlmSession -import org.matrix.olm.OlmUtility -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO) - -// The libolm wrapper. -@SessionScope -internal class MXOlmDevice @Inject constructor( - /** - * The store where crypto data is saved. - */ - private val store: IMXCryptoStore, - private val olmSessionStore: OlmSessionStore, - private val inboundGroupSessionStore: InboundGroupSessionStore -) { - - val mutex = Mutex() - - /** - * @return the Curve25519 key for the account. - */ - var deviceCurve25519Key: String? = null - private set - - /** - * @return the Ed25519 key for the account. - */ - var deviceEd25519Key: String? = null - private set - - // The OLM lib utility instance. - private var olmUtility: OlmUtility? = null - - private data class GroupSessionCacheItem( - val groupId: String, - val groupSession: OlmOutboundGroupSession - ) - - // The outbound group session. - // Caches active outbound session to avoid to sync with DB before read - // The key is the session id, the value the . - private val outboundGroupSessionCache: MutableMap = HashMap() - - // Store a set of decrypted message indexes for each group session. - // This partially mitigates a replay attack where a MITM resends a group - // message into the room. - // - // The Matrix SDK exposes events through MXEventTimelines. A developer can open several - // timelines from a same room so that a message can be decrypted several times but from - // a different timeline. - // So, store these message indexes per timeline id. - // - // The first level keys are timeline ids. - // The second level keys are strings of form "||" - private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap() - - init { - // Retrieve the account from the store - try { - store.getOrCreateOlmAccount() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "MXOlmDevice : cannot initialize olmAccount") - } - - try { - olmUtility = OlmUtility() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : OlmUtility failed with error") - olmUtility = null - } - - try { - deviceCurve25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") - } - - try { - deviceEd25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") - } - } - - /** - * @return The current (unused, unpublished) one-time keys for this account. - */ - fun getOneTimeKeys(): Map>? { - try { - return store.doWithOlmAccount { it.oneTimeKeys() } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed") - } - - return null - } - - /** - * @return The maximum number of one-time keys the olm account can store. - */ - fun getMaxNumberOfOneTimeKeys(): Long { - return store.doWithOlmAccount { it.maxOneTimeKeys() } - } - - /** - * Returns an unpublished fallback key - * A call to markKeysAsPublished will mark it as published and this - * call will return null (until a call to generateFallbackKey is made) - */ - fun getFallbackKey(): MutableMap>? { - try { - return store.doWithOlmAccount { it.fallbackKey() } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## getFallbackKey() : failed") - } - return null - } - - /** - * Generates a new fallback key if there is not already - * an unpublished one. - * @return true if a new key was generated - */ - fun generateFallbackKeyIfNeeded(): Boolean { - try { - if (!hasUnpublishedFallbackKey()) { - store.doWithOlmAccount { - it.generateFallbackKey() - store.saveOlmAccount() - } - return true - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## generateFallbackKey() : failed") - } - return false - } - - internal fun hasUnpublishedFallbackKey(): Boolean { - return getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty().isNotEmpty() - } - - fun forgetFallbackKey() { - try { - store.doWithOlmAccount { - it.forgetFallbackKey() - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## forgetFallbackKey() : failed") - } - } - - /** - * Release the instance - */ - fun release() { - olmUtility?.releaseUtility() - outboundGroupSessionCache.values.forEach { - it.groupSession.releaseSession() - } - outboundGroupSessionCache.clear() - inboundGroupSessionStore.clear() - olmSessionStore.clear() - } - - /** - * Signs a message with the ed25519 key for this account. - * - * @param message the message to be signed. - * @return the base64-encoded signature. - */ - fun signMessage(message: String): String? { - try { - return store.doWithOlmAccount { it.signMessage(message) } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## signMessage() : failed") - } - - return null - } - - /** - * Marks all of the one-time keys as published. - */ - fun markKeysAsPublished() { - try { - store.doWithOlmAccount { - it.markOneTimeKeysAsPublished() - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## markKeysAsPublished() : failed") - } - } - - /** - * Generate some new one-time keys - * - * @param numKeys number of keys to generate - */ - fun generateOneTimeKeys(numKeys: Int) { - try { - store.doWithOlmAccount { - it.generateOneTimeKeys(numKeys) - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## generateOneTimeKeys() : failed") - } - } - - /** - * Generate a new outbound session. - * The new session will be stored in the MXStore. - * - * @param theirIdentityKey the remote user's Curve25519 identity key - * @param theirOneTimeKey the remote user's one-time Curve25519 key - * @return the session id for the outbound session. - */ - fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? { - Timber.tag(loggerTag.value).d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey") - var olmSession: OlmSession? = null - - try { - olmSession = OlmSession() - store.doWithOlmAccount { olmAccount -> - olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey) - } - - val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) - - // Pretend we've received a message at this point, otherwise - // if we try to send a message to the device, it won't use - // this session - olmSessionWrapper.onMessageReceived() - - olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey) - - val sessionIdentifier = olmSession.sessionIdentifier() - - Timber.tag(loggerTag.value).v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier") - return sessionIdentifier - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createOutboundSession() failed") - - olmSession?.releaseSession() - } - - return null - } - - /** - * Generate a new inbound session, given an incoming message. - * - * @param theirDeviceIdentityKey the remote user's Curve25519 identity key. - * @param messageType the message_type field from the received message (must be 0). - * @param ciphertext base64-encoded body from the received message. - * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session. - */ - fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map? { - Timber.tag(loggerTag.value).d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey") - - var olmSession: OlmSession? = null - - try { - try { - olmSession = OlmSession() - store.doWithOlmAccount { olmAccount -> - olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext) - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : the session creation failed") - return null - } - - Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") - - try { - store.doWithOlmAccount { olmAccount -> - olmAccount.removeOneTimeKeys(olmSession) - store.saveOlmAccount() - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed") - } - - val olmMessage = OlmMessage() - olmMessage.mCipherText = ciphertext - olmMessage.mType = messageType.toLong() - - var payloadString: String? = null - - try { - payloadString = olmSession.decryptMessage(olmMessage) - - val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) - // This counts as a received message: set last received message time to now - olmSessionWrapper.onMessageReceived() - - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : decryptMessage failed") - } - - val res = HashMap() - - if (!payloadString.isNullOrEmpty()) { - res["payload"] = payloadString - } - - val sessionIdentifier = olmSession.sessionIdentifier() - - if (!sessionIdentifier.isNullOrEmpty()) { - res["session_id"] = sessionIdentifier - } - - return res - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() : OlmSession creation failed") - - olmSession?.releaseSession() - } - - return null - } - - /** - * Get a list of known session IDs for the given device. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @return a list of known session ids for the device. - */ - fun getSessionIds(theirDeviceIdentityKey: String): List { - return olmSessionStore.getDeviceSessionIds(theirDeviceIdentityKey) - } - - /** - * Get the right olm session id for encrypting messages to the given identity key. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @return the session id, or null if no established session. - */ - fun getSessionId(theirDeviceIdentityKey: String): String? { - return olmSessionStore.getLastUsedSessionId(theirDeviceIdentityKey) - } - - /** - * Encrypt an outgoing message using an existing session. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @param sessionId the id of the active session - * @param payloadString the payload to be encrypted and sent - * @return the cipher text - */ - suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? { - val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) - - if (olmSessionWrapper != null) { - try { - Timber.tag(loggerTag.value).v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId") - - val olmMessage = olmSessionWrapper.mutex.withLock { - olmSessionWrapper.olmSession.encryptMessage(payloadString) - } - return mapOf( - "body" to olmMessage.mCipherText, - "type" to olmMessage.mType, - ).also { - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - } - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId") - return null - } - } else { - Timber.tag(loggerTag.value).e("## encryptMessage() : Failed to encrypt unknown session $sessionId") - return null - } - } - - /** - * Decrypt an incoming message using an existing session. - * - * @param ciphertext the base64-encoded body from the received message. - * @param messageType message_type field from the received message. - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @param sessionId the id of the active session. - * @return the decrypted payload. - */ - @kotlin.jvm.Throws - suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? { - var payloadString: String? = null - - val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) - - if (null != olmSessionWrapper) { - val olmMessage = OlmMessage() - olmMessage.mCipherText = ciphertext - olmMessage.mType = messageType.toLong() - - payloadString = - olmSessionWrapper.mutex.withLock { - olmSessionWrapper.olmSession.decryptMessage(olmMessage).also { - olmSessionWrapper.onMessageReceived() - } - } - olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) - } - - return payloadString - } - - /** - * Determine if an incoming messages is a prekey message matching an existing session. - * - * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. - * @param sessionId the id of the active session. - * @param messageType message_type field from the received message. - * @param ciphertext the base64-encoded body from the received message. - * @return YES if the received message is a prekey message which matchesthe given session. - */ - fun matchesSession(theirDeviceIdentityKey: String, sessionId: String, messageType: Int, ciphertext: String): Boolean { - if (messageType != 0) { - return false - } - - val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) - return null != olmSessionWrapper && olmSessionWrapper.olmSession.matchesInboundSession(ciphertext) - } - - // Outbound group session - - /** - * Generate a new outbound group session. - * - * @return the session id for the outbound session. - */ - fun createOutboundGroupSessionForRoom(roomId: String): String? { - var session: OlmOutboundGroupSession? = null - try { - session = OlmOutboundGroupSession() - outboundGroupSessionCache[session.sessionIdentifier()] = GroupSessionCacheItem(roomId, session) - store.storeCurrentOutboundGroupSessionForRoom(roomId, session) - return session.sessionIdentifier() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "createOutboundGroupSession") - - session?.releaseSession() - } - - return null - } - - fun storeOutboundGroupSessionForRoom(roomId: String, sessionId: String) { - outboundGroupSessionCache[sessionId]?.let { - store.storeCurrentOutboundGroupSessionForRoom(roomId, it.groupSession) - } - } - - fun restoreOutboundGroupSessionForRoom(roomId: String): MXOutboundSessionInfo? { - val restoredOutboundGroupSession = store.getCurrentOutboundGroupSessionForRoom(roomId) - if (restoredOutboundGroupSession != null) { - val sessionId = restoredOutboundGroupSession.outboundGroupSession.sessionIdentifier() - // cache it - outboundGroupSessionCache[sessionId] = GroupSessionCacheItem(roomId, restoredOutboundGroupSession.outboundGroupSession) - - return MXOutboundSessionInfo( - sessionId = sessionId, - sharedWithHelper = SharedWithHelper(roomId, sessionId, store), - restoredOutboundGroupSession.creationTime - ) - } - return null - } - - fun discardOutboundGroupSessionForRoom(roomId: String) { - val toDiscard = outboundGroupSessionCache.filter { - it.value.groupId == roomId - } - toDiscard.forEach { (sessionId, cacheItem) -> - cacheItem.groupSession.releaseSession() - outboundGroupSessionCache.remove(sessionId) - } - store.storeCurrentOutboundGroupSessionForRoom(roomId, null) - } - - /** - * Get the current session key of an outbound group session. - * - * @param sessionId the id of the outbound group session. - * @return the base64-encoded secret key. - */ - fun getSessionKey(sessionId: String): String? { - if (sessionId.isNotEmpty()) { - try { - return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey() - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## getSessionKey() : failed") - } - } - return null - } - - /** - * Get the current message index of an outbound group session. - * - * @param sessionId the id of the outbound group session. - * @return the current chain index. - */ - fun getMessageIndex(sessionId: String): Int { - return if (sessionId.isNotEmpty()) { - outboundGroupSessionCache[sessionId]!!.groupSession.messageIndex() - } else 0 - } - - /** - * Encrypt an outgoing message with an outbound group session. - * - * @param sessionId the id of the outbound group session. - * @param payloadString the payload to be encrypted and sent. - * @return ciphertext - */ - fun encryptGroupMessage(sessionId: String, payloadString: String): String? { - if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) { - try { - return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString) - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## encryptGroupMessage() : failed") - } - } - return null - } - - // Inbound group session - - sealed class AddSessionResult { - data class Imported(val ratchetIndex: Int) : AddSessionResult() - abstract class Failure : AddSessionResult() - object NotImported : Failure() - data class NotImportedHigherIndex(val newIndex: Int) : AddSessionResult() - } - - /** - * Add an inbound group session to the session store. - * - * @param sessionId the session identifier. - * @param sessionKey base64-encoded secret key. - * @param roomId the id of the room in which this session will be used. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us. - * @param keysClaimed Other keys the sender claims. - * @param exportFormat true if the megolm keys are in export format - * @return true if the operation succeeds. - */ - fun addInboundGroupSession(sessionId: String, - sessionKey: String, - roomId: String, - senderKey: String, - forwardingCurve25519KeyChain: List, - keysClaimed: Map, - exportFormat: Boolean): AddSessionResult { - val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) - val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } - val existingSession = existingSessionHolder?.wrapper - // If we have an existing one we should check if the new one is not better - if (existingSession != null) { - Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") - try { - val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also { - // This is quite unexpected, could throw if native was released? - Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") - candidateSession.olmInboundGroupSession?.releaseSession() - // Probably should discard it? - } - val newKnownFirstIndex = candidateSession.firstKnownIndex - // If our existing session is better we keep it - if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") - candidateSession.olmInboundGroupSession?.releaseSession() - return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) - } - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") - candidateSession.olmInboundGroupSession?.releaseSession() - return AddSessionResult.NotImported - } - } - - Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") - - // sanity check on the new session - val candidateOlmInboundSession = candidateSession.olmInboundGroupSession - if (null == candidateOlmInboundSession) { - Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session ") - return AddSessionResult.NotImported - } - - try { - if (candidateOlmInboundSession.sessionIdentifier() != sessionId) { - Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") - candidateOlmInboundSession.releaseSession() - return AddSessionResult.NotImported - } - } catch (e: Throwable) { - candidateOlmInboundSession.releaseSession() - Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") - return AddSessionResult.NotImported - } - - candidateSession.senderKey = senderKey - candidateSession.roomId = roomId - candidateSession.keysClaimed = keysClaimed - candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain - - if (existingSession != null) { - inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey) - } else { - inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey) - } - - return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0) - } - - /** - * Import an inbound group sessions to the session store. - * - * @param megolmSessionsData the megolm sessions data - * @return the successfully imported sessions. - */ - fun importInboundGroupSessions(megolmSessionsData: List): List { - val sessions = ArrayList(megolmSessionsData.size) - - for (megolmSessionData in megolmSessionsData) { - val sessionId = megolmSessionData.sessionId ?: continue - val senderKey = megolmSessionData.senderKey ?: continue - val roomId = megolmSessionData.roomId - - var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null - - try { - candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData) - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") - } - - // sanity check - if (candidateSessionToImport?.olmInboundGroupSession == null) { - Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session") - continue - } - - val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession - try { - if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) { - Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") - candidateOlmInboundGroupSession?.releaseSession() - continue - } - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed") - candidateOlmInboundGroupSession?.releaseSession() - continue - } - - val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } - val existingSession = existingSessionHolder?.wrapper - - if (existingSession == null) { - // Session does not already exist, add it - Timber.tag(loggerTag.value).d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId") - sessions.add(candidateSessionToImport) - } else { - Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") - val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex } - val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex } - - if (existingFirstKnown == null || candidateFirstKnownIndex == null) { - // should not happen? - candidateSessionToImport.olmInboundGroupSession?.releaseSession() - Timber.tag(loggerTag.value) - .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") - } else { - if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) { - // Ignore this, keep existing - candidateOlmInboundGroupSession.releaseSession() - } else { - // update cache with better session - inboundGroupSessionStore.replaceGroupSession( - existingSessionHolder, - InboundGroupSessionHolder(candidateSessionToImport), - sessionId, - senderKey - ) - sessions.add(candidateSessionToImport) - } - } - } - } - - store.storeInboundGroupSessions(sessions) - - return sessions - } - - /** - * Decrypt a received message with an inbound group session. - * - * @param body the base64-encoded body of the encrypted message. - * @param roomId the room in which the message was received. - * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @return the decrypting result. Nil if the sessionId is unknown. - */ - @Throws(MXCryptoError::class) - suspend fun decryptGroupMessage(body: String, - roomId: String, - timeline: String?, - sessionId: String, - senderKey: String): OlmDecryptionResult { - val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId) - val wrapper = sessionHolder.wrapper - val inboundGroupSession = wrapper.olmInboundGroupSession - ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null") - // Check that the room id matches the original one for the session. This stops - // the HS pretending a message was targeting a different room. - if (roomId == wrapper.roomId) { - val decryptResult = try { - sessionHolder.mutex.withLock { - inboundGroupSession.decryptMessage(body) - } - } catch (e: OlmException) { - Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed") - throw MXCryptoError.OlmError(e) - } - - if (timeline?.isNotBlank() == true) { - val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() } - - val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex - - if (timelineSet.contains(messageIndexKey)) { - val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) - Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) - } - - timelineSet.add(messageIndexKey) - } - - inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey) - val payload = try { - val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) - val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) - adapter.fromJson(payloadString) - } catch (e: Exception) { - Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) - } - - return OlmDecryptionResult( - payload, - wrapper.keysClaimed, - senderKey, - wrapper.forwardingCurve25519KeyChain - ) - } else { - val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId) - Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason) - } - } - - /** - * Reset replay attack data for the given timeline. - * - * @param timeline the id of the timeline. - */ - fun resetReplayAttackCheckInTimeline(timeline: String?) { - if (null != timeline) { - inboundGroupSessionMessageIndexes.remove(timeline) - } - } - -// Utilities - - /** - * Verify an ed25519 signature on a JSON object. - * - * @param key the ed25519 key. - * @param jsonDictionary the JSON object which was signed. - * @param signature the base64-encoded signature to be checked. - * @throws Exception the exception - */ - @Throws(Exception::class) - fun verifySignature(key: String, jsonDictionary: Map, signature: String) { - // Check signature on the canonical version of the JSON - olmUtility!!.verifyEd25519Signature(signature, key, JsonCanonicalizer.getCanonicalJson(Map::class.java, jsonDictionary)) - } - - /** - * Calculate the SHA-256 hash of the input and encodes it as base64. - * - * @param message the message to hash. - * @return the base64-encoded hash value. - */ - fun sha256(message: String): String { - return olmUtility!!.sha256(convertToUTF8(message)) - } - - /** - * Search an OlmSession - * - * @param theirDeviceIdentityKey the device key - * @param sessionId the session Id - * @return the olm session - */ - private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? { - // sanity check - return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else { - olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey) - } - } - - /** - * Extract an InboundGroupSession from the session store and do some check. - * inboundGroupSessionWithIdError describes the failure reason. - * - * @param roomId the room where the session is used. - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @return the inbound group session. - */ - fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder { - if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) { - throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) - } - - val holder = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey) - val session = holder?.wrapper - - if (session != null) { - // Check that the room id matches the original one for the session. This stops - // the HS pretending a message was targeting a different room. - if (roomId != session.roomId) { - val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) - Timber.tag(loggerTag.value).e("## getInboundGroupSession() : $errorDescription") - throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) - } else { - return holder - } - } else { - Timber.tag(loggerTag.value).w("## getInboundGroupSession() : UISI $sessionId") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) - } - } - - /** - * Determine if we have the keys for a given megolm session. - * - * @param roomId room in which the message was received - * @param senderKey base64-encoded curve25519 key of the sender - * @param sessionId session identifier - * @return true if the unbound session keys are known. - */ - fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean { - return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess - } - - @VisibleForTesting - fun clearOlmSessionCache() { - olmSessionStore.clear() - } -} +/* + * 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 androidx.annotation.VisibleForTesting +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.convertFromUTF8 +import org.matrix.android.sdk.internal.util.convertToUTF8 +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmException +import org.matrix.olm.OlmMessage +import org.matrix.olm.OlmOutboundGroupSession +import org.matrix.olm.OlmSession +import org.matrix.olm.OlmUtility +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO) + +// The libolm wrapper. +@SessionScope +internal class MXOlmDevice @Inject constructor( + /** + * The store where crypto data is saved. + */ + private val store: IMXCryptoStore, + private val olmSessionStore: OlmSessionStore, + private val inboundGroupSessionStore: InboundGroupSessionStore +) { + + val mutex = Mutex() + + /** + * @return the Curve25519 key for the account. + */ + var deviceCurve25519Key: String? = null + private set + + /** + * @return the Ed25519 key for the account. + */ + var deviceEd25519Key: String? = null + private set + + // The OLM lib utility instance. + private var olmUtility: OlmUtility? = null + + private data class GroupSessionCacheItem( + val groupId: String, + val groupSession: OlmOutboundGroupSession + ) + + // The outbound group session. + // Caches active outbound session to avoid to sync with DB before read + // The key is the session id, the value the . + private val outboundGroupSessionCache: MutableMap = HashMap() + + // Store a set of decrypted message indexes for each group session. + // This partially mitigates a replay attack where a MITM resends a group + // message into the room. + // + // The Matrix SDK exposes events through MXEventTimelines. A developer can open several + // timelines from a same room so that a message can be decrypted several times but from + // a different timeline. + // So, store these message indexes per timeline id. + // + // The first level keys are timeline ids. + // The second level keys are strings of form "||" + private val inboundGroupSessionMessageIndexes: MutableMap> = HashMap() + + init { + // Retrieve the account from the store + try { + store.getOrCreateOlmAccount() + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "MXOlmDevice : cannot initialize olmAccount") + } + + try { + olmUtility = OlmUtility() + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : OlmUtility failed with error") + olmUtility = null + } + + try { + deviceCurve25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error") + } + + try { + deviceEd25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error") + } + } + + /** + * @return The current (unused, unpublished) one-time keys for this account. + */ + fun getOneTimeKeys(): Map>? { + try { + return store.doWithOlmAccount { it.oneTimeKeys() } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed") + } + + return null + } + + /** + * @return The maximum number of one-time keys the olm account can store. + */ + fun getMaxNumberOfOneTimeKeys(): Long { + return store.doWithOlmAccount { it.maxOneTimeKeys() } + } + + /** + * Returns an unpublished fallback key + * A call to markKeysAsPublished will mark it as published and this + * call will return null (until a call to generateFallbackKey is made) + */ + fun getFallbackKey(): MutableMap>? { + try { + return store.doWithOlmAccount { it.fallbackKey() } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e("## getFallbackKey() : failed") + } + return null + } + + /** + * Generates a new fallback key if there is not already + * an unpublished one. + * @return true if a new key was generated + */ + fun generateFallbackKeyIfNeeded(): Boolean { + try { + if (!hasUnpublishedFallbackKey()) { + store.doWithOlmAccount { + it.generateFallbackKey() + store.saveOlmAccount() + } + return true + } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e("## generateFallbackKey() : failed") + } + return false + } + + internal fun hasUnpublishedFallbackKey(): Boolean { + return getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty().isNotEmpty() + } + + fun forgetFallbackKey() { + try { + store.doWithOlmAccount { + it.forgetFallbackKey() + store.saveOlmAccount() + } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e("## forgetFallbackKey() : failed") + } + } + + /** + * Release the instance + */ + fun release() { + olmUtility?.releaseUtility() + outboundGroupSessionCache.values.forEach { + it.groupSession.releaseSession() + } + outboundGroupSessionCache.clear() + inboundGroupSessionStore.clear() + olmSessionStore.clear() + } + + /** + * Signs a message with the ed25519 key for this account. + * + * @param message the message to be signed. + * @return the base64-encoded signature. + */ + fun signMessage(message: String): String? { + try { + return store.doWithOlmAccount { it.signMessage(message) } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## signMessage() : failed") + } + + return null + } + + /** + * Marks all of the one-time keys as published. + */ + fun markKeysAsPublished() { + try { + store.doWithOlmAccount { + it.markOneTimeKeysAsPublished() + store.saveOlmAccount() + } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## markKeysAsPublished() : failed") + } + } + + /** + * Generate some new one-time keys + * + * @param numKeys number of keys to generate + */ + fun generateOneTimeKeys(numKeys: Int) { + try { + store.doWithOlmAccount { + it.generateOneTimeKeys(numKeys) + store.saveOlmAccount() + } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## generateOneTimeKeys() : failed") + } + } + + /** + * Generate a new outbound session. + * The new session will be stored in the MXStore. + * + * @param theirIdentityKey the remote user's Curve25519 identity key + * @param theirOneTimeKey the remote user's one-time Curve25519 key + * @return the session id for the outbound session. + */ + fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? { + Timber.tag(loggerTag.value).d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey") + var olmSession: OlmSession? = null + + try { + olmSession = OlmSession() + store.doWithOlmAccount { olmAccount -> + olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey) + } + + val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) + + // Pretend we've received a message at this point, otherwise + // if we try to send a message to the device, it won't use + // this session + olmSessionWrapper.onMessageReceived() + + olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey) + + val sessionIdentifier = olmSession.sessionIdentifier() + + Timber.tag(loggerTag.value).v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier") + return sessionIdentifier + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## createOutboundSession() failed") + + olmSession?.releaseSession() + } + + return null + } + + /** + * Generate a new inbound session, given an incoming message. + * + * @param theirDeviceIdentityKey the remote user's Curve25519 identity key. + * @param messageType the message_type field from the received message (must be 0). + * @param ciphertext base64-encoded body from the received message. + * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session. + */ + fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map? { + Timber.tag(loggerTag.value).d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey") + + var olmSession: OlmSession? = null + + try { + try { + olmSession = OlmSession() + store.doWithOlmAccount { olmAccount -> + olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext) + } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## createInboundSession() : the session creation failed") + return null + } + + Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}") + + try { + store.doWithOlmAccount { olmAccount -> + olmAccount.removeOneTimeKeys(olmSession) + store.saveOlmAccount() + } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed") + } + + val olmMessage = OlmMessage() + olmMessage.mCipherText = ciphertext + olmMessage.mType = messageType.toLong() + + var payloadString: String? = null + + try { + payloadString = olmSession.decryptMessage(olmMessage) + + val olmSessionWrapper = OlmSessionWrapper(olmSession, 0) + // This counts as a received message: set last received message time to now + olmSessionWrapper.onMessageReceived() + + olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## createInboundSession() : decryptMessage failed") + } + + val res = HashMap() + + if (!payloadString.isNullOrEmpty()) { + res["payload"] = payloadString + } + + val sessionIdentifier = olmSession.sessionIdentifier() + + if (!sessionIdentifier.isNullOrEmpty()) { + res["session_id"] = sessionIdentifier + } + + return res + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## createInboundSession() : OlmSession creation failed") + + olmSession?.releaseSession() + } + + return null + } + + /** + * Get a list of known session IDs for the given device. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @return a list of known session ids for the device. + */ + fun getSessionIds(theirDeviceIdentityKey: String): List { + return olmSessionStore.getDeviceSessionIds(theirDeviceIdentityKey) + } + + /** + * Get the right olm session id for encrypting messages to the given identity key. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @return the session id, or null if no established session. + */ + fun getSessionId(theirDeviceIdentityKey: String): String? { + return olmSessionStore.getLastUsedSessionId(theirDeviceIdentityKey) + } + + /** + * Encrypt an outgoing message using an existing session. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session + * @param payloadString the payload to be encrypted and sent + * @return the cipher text + */ + suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? { + val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) + + if (olmSessionWrapper != null) { + try { + Timber.tag(loggerTag.value).v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId") + + val olmMessage = olmSessionWrapper.mutex.withLock { + olmSessionWrapper.olmSession.encryptMessage(payloadString) + } + return mapOf( + "body" to olmMessage.mCipherText, + "type" to olmMessage.mType, + ).also { + olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + } + } catch (e: Throwable) { + Timber.tag(loggerTag.value).e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId") + return null + } + } else { + Timber.tag(loggerTag.value).e("## encryptMessage() : Failed to encrypt unknown session $sessionId") + return null + } + } + + /** + * Decrypt an incoming message using an existing session. + * + * @param ciphertext the base64-encoded body from the received message. + * @param messageType message_type field from the received message. + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session. + * @return the decrypted payload. + */ + @kotlin.jvm.Throws + suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? { + var payloadString: String? = null + + val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) + + if (null != olmSessionWrapper) { + val olmMessage = OlmMessage() + olmMessage.mCipherText = ciphertext + olmMessage.mType = messageType.toLong() + + payloadString = + olmSessionWrapper.mutex.withLock { + olmSessionWrapper.olmSession.decryptMessage(olmMessage).also { + olmSessionWrapper.onMessageReceived() + } + } + olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey) + } + + return payloadString + } + + /** + * Determine if an incoming messages is a prekey message matching an existing session. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session. + * @param messageType message_type field from the received message. + * @param ciphertext the base64-encoded body from the received message. + * @return YES if the received message is a prekey message which matchesthe given session. + */ + fun matchesSession(theirDeviceIdentityKey: String, sessionId: String, messageType: Int, ciphertext: String): Boolean { + if (messageType != 0) { + return false + } + + val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId) + return null != olmSessionWrapper && olmSessionWrapper.olmSession.matchesInboundSession(ciphertext) + } + + // Outbound group session + + /** + * Generate a new outbound group session. + * + * @return the session id for the outbound session. + */ + fun createOutboundGroupSessionForRoom(roomId: String): String? { + var session: OlmOutboundGroupSession? = null + try { + session = OlmOutboundGroupSession() + outboundGroupSessionCache[session.sessionIdentifier()] = GroupSessionCacheItem(roomId, session) + store.storeCurrentOutboundGroupSessionForRoom(roomId, session) + return session.sessionIdentifier() + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "createOutboundGroupSession") + + session?.releaseSession() + } + + return null + } + + fun storeOutboundGroupSessionForRoom(roomId: String, sessionId: String) { + outboundGroupSessionCache[sessionId]?.let { + store.storeCurrentOutboundGroupSessionForRoom(roomId, it.groupSession) + } + } + + fun restoreOutboundGroupSessionForRoom(roomId: String): MXOutboundSessionInfo? { + val restoredOutboundGroupSession = store.getCurrentOutboundGroupSessionForRoom(roomId) + if (restoredOutboundGroupSession != null) { + val sessionId = restoredOutboundGroupSession.outboundGroupSession.sessionIdentifier() + // cache it + outboundGroupSessionCache[sessionId] = GroupSessionCacheItem(roomId, restoredOutboundGroupSession.outboundGroupSession) + + return MXOutboundSessionInfo( + sessionId = sessionId, + sharedWithHelper = SharedWithHelper(roomId, sessionId, store), + restoredOutboundGroupSession.creationTime + ) + } + return null + } + + fun discardOutboundGroupSessionForRoom(roomId: String) { + val toDiscard = outboundGroupSessionCache.filter { + it.value.groupId == roomId + } + toDiscard.forEach { (sessionId, cacheItem) -> + cacheItem.groupSession.releaseSession() + outboundGroupSessionCache.remove(sessionId) + } + store.storeCurrentOutboundGroupSessionForRoom(roomId, null) + } + + /** + * Get the current session key of an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @return the base64-encoded secret key. + */ + fun getSessionKey(sessionId: String): String? { + if (sessionId.isNotEmpty()) { + try { + return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey() + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## getSessionKey() : failed") + } + } + return null + } + + /** + * Get the current message index of an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @return the current chain index. + */ + fun getMessageIndex(sessionId: String): Int { + return if (sessionId.isNotEmpty()) { + outboundGroupSessionCache[sessionId]!!.groupSession.messageIndex() + } else 0 + } + + /** + * Encrypt an outgoing message with an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @param payloadString the payload to be encrypted and sent. + * @return ciphertext + */ + fun encryptGroupMessage(sessionId: String, payloadString: String): String? { + if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) { + try { + return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString) + } catch (e: Throwable) { + Timber.tag(loggerTag.value).e(e, "## encryptGroupMessage() : failed") + } + } + return null + } + + // Inbound group session + + sealed interface AddSessionResult { + data class Imported(val ratchetIndex: Int) : AddSessionResult + abstract class Failure : AddSessionResult + object NotImported : Failure() + data class NotImportedHigherIndex(val newIndex: Int) : Failure() + } + + /** + * Add an inbound group session to the session store. + * + * @param sessionId the session identifier. + * @param sessionKey base64-encoded secret key. + * @param roomId the id of the room in which this session will be used. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us. + * @param keysClaimed Other keys the sender claims. + * @param exportFormat true if the megolm keys are in export format + * @return true if the operation succeeds. + */ + fun addInboundGroupSession(sessionId: String, + sessionKey: String, + roomId: String, + senderKey: String, + forwardingCurve25519KeyChain: List, + keysClaimed: Map, + exportFormat: Boolean): AddSessionResult { + val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) + val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } + val existingSession = existingSessionHolder?.wrapper + // If we have an existing one we should check if the new one is not better + if (existingSession != null) { + Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") + try { + val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also { + // This is quite unexpected, could throw if native was released? + Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") + candidateSession.olmInboundGroupSession?.releaseSession() + // Probably should discard it? + } + val newKnownFirstIndex = candidateSession.firstKnownIndex + // If our existing session is better we keep it + if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") + candidateSession.olmInboundGroupSession?.releaseSession() + return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) + } + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") + candidateSession.olmInboundGroupSession?.releaseSession() + return AddSessionResult.NotImported + } + } + + Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") + + // sanity check on the new session + val candidateOlmInboundSession = candidateSession.olmInboundGroupSession + if (null == candidateOlmInboundSession) { + Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session ") + return AddSessionResult.NotImported + } + + try { + if (candidateOlmInboundSession.sessionIdentifier() != sessionId) { + Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") + candidateOlmInboundSession.releaseSession() + return AddSessionResult.NotImported + } + } catch (e: Throwable) { + candidateOlmInboundSession.releaseSession() + Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") + return AddSessionResult.NotImported + } + + candidateSession.senderKey = senderKey + candidateSession.roomId = roomId + candidateSession.keysClaimed = keysClaimed + candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain + + if (existingSession != null) { + inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey) + } else { + inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey) + } + + return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0) + } + + /** + * Import an inbound group sessions to the session store. + * + * @param megolmSessionsData the megolm sessions data + * @return the successfully imported sessions. + */ + fun importInboundGroupSessions(megolmSessionsData: List): List { + val sessions = ArrayList(megolmSessionsData.size) + + for (megolmSessionData in megolmSessionsData) { + val sessionId = megolmSessionData.sessionId ?: continue + val senderKey = megolmSessionData.senderKey ?: continue + val roomId = megolmSessionData.roomId + + var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null + + try { + candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData) + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + } + + // sanity check + if (candidateSessionToImport?.olmInboundGroupSession == null) { + Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session") + continue + } + + val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession + try { + if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) { + Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") + candidateOlmInboundGroupSession?.releaseSession() + continue + } + } catch (e: Exception) { + Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed") + candidateOlmInboundGroupSession?.releaseSession() + continue + } + + val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } + val existingSession = existingSessionHolder?.wrapper + + if (existingSession == null) { + // Session does not already exist, add it + Timber.tag(loggerTag.value).d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId") + sessions.add(candidateSessionToImport) + } else { + Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex } + val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex } + + if (existingFirstKnown == null || candidateFirstKnownIndex == null) { + // should not happen? + candidateSessionToImport.olmInboundGroupSession?.releaseSession() + Timber.tag(loggerTag.value) + .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex") + } else { + if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) { + // Ignore this, keep existing + candidateOlmInboundGroupSession.releaseSession() + } else { + // update cache with better session + inboundGroupSessionStore.replaceGroupSession( + existingSessionHolder, + InboundGroupSessionHolder(candidateSessionToImport), + sessionId, + senderKey + ) + sessions.add(candidateSessionToImport) + } + } + } + } + + store.storeInboundGroupSessions(sessions) + + return sessions + } + + /** + * Decrypt a received message with an inbound group session. + * + * @param body the base64-encoded body of the encrypted message. + * @param roomId the room in which the message was received. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return the decrypting result. Nil if the sessionId is unknown. + */ + @Throws(MXCryptoError::class) + suspend fun decryptGroupMessage(body: String, + roomId: String, + timeline: String?, + sessionId: String, + senderKey: String): OlmDecryptionResult { + val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId) + val wrapper = sessionHolder.wrapper + val inboundGroupSession = wrapper.olmInboundGroupSession + ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null") + // Check that the room id matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (roomId == wrapper.roomId) { + val decryptResult = try { + sessionHolder.mutex.withLock { + inboundGroupSession.decryptMessage(body) + } + } catch (e: OlmException) { + Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed") + throw MXCryptoError.OlmError(e) + } + + if (timeline?.isNotBlank() == true) { + val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() } + + val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex + + if (timelineSet.contains(messageIndexKey)) { + val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) + Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) + } + + timelineSet.add(messageIndexKey) + } + + inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey) + val payload = try { + val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) + val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) + adapter.fromJson(payloadString) + } catch (e: Exception) { + Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON) + } + + return OlmDecryptionResult( + payload, + wrapper.keysClaimed, + senderKey, + wrapper.forwardingCurve25519KeyChain + ) + } else { + val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId) + Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason) + } + } + + /** + * Reset replay attack data for the given timeline. + * + * @param timeline the id of the timeline. + */ + fun resetReplayAttackCheckInTimeline(timeline: String?) { + if (null != timeline) { + inboundGroupSessionMessageIndexes.remove(timeline) + } + } + +// Utilities + + /** + * Verify an ed25519 signature on a JSON object. + * + * @param key the ed25519 key. + * @param jsonDictionary the JSON object which was signed. + * @param signature the base64-encoded signature to be checked. + * @throws Exception the exception + */ + @Throws(Exception::class) + fun verifySignature(key: String, jsonDictionary: Map, signature: String) { + // Check signature on the canonical version of the JSON + olmUtility!!.verifyEd25519Signature(signature, key, JsonCanonicalizer.getCanonicalJson(Map::class.java, jsonDictionary)) + } + + /** + * Calculate the SHA-256 hash of the input and encodes it as base64. + * + * @param message the message to hash. + * @return the base64-encoded hash value. + */ + fun sha256(message: String): String { + return olmUtility!!.sha256(convertToUTF8(message)) + } + + /** + * Search an OlmSession + * + * @param theirDeviceIdentityKey the device key + * @param sessionId the session Id + * @return the olm session + */ + private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? { + // sanity check + return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else { + olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey) + } + } + + /** + * Extract an InboundGroupSession from the session store and do some check. + * inboundGroupSessionWithIdError describes the failure reason. + * + * @param roomId the room where the session is used. + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return the inbound group session. + */ + fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder { + if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) { + throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) + } + + val holder = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey) + val session = holder?.wrapper + + if (session != null) { + // Check that the room id matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (roomId != session.roomId) { + val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId) + Timber.tag(loggerTag.value).e("## getInboundGroupSession() : $errorDescription") + throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription) + } else { + return holder + } + } else { + Timber.tag(loggerTag.value).w("## getInboundGroupSession() : UISI $sessionId") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) + } + } + + /** + * Determine if we have the keys for a given megolm session. + * + * @param roomId room in which the message was received + * @param senderKey base64-encoded curve25519 key of the sender + * @param sessionId session identifier + * @return true if the unbound session keys are known. + */ + fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean { + return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess + } + + @VisibleForTesting + fun clearOlmSessionCache() { + olmSessionStore.clear() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt deleted file mode 100644 index 16e520c668..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt +++ /dev/null @@ -1,27 +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 - -import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState - -interface OutgoingGossipingRequest { - var recipients: Map> - var requestId: String - var state: OutgoingRoomKeyRequestState - // transaction id for the cancellation, if any - // var cancellationTxnId: String? -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt index 1e192393a2..09a9868428 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt @@ -1,519 +1,518 @@ -/* - * 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.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.logger.LoggerTag -import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest -import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer -import timber.log.Timber -import java.util.Stack -import java.util.concurrent.Executors -import javax.inject.Inject -import kotlin.system.measureTimeMillis - -private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO) - -/** - * This class is responsible for sending key requests to other devices when a message failed to decrypt. - * It's lifecycle is based on the sync pulse: - * - You can post queries for session, or report when you got a session - * - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices - * If a request failed it will be retried at the end of the next sync - */ -@SessionScope -internal class OutgoingKeyRequestManager @Inject constructor( - @SessionId private val sessionId: String, - @UserId private val myUserId: String, - private val cryptoStore: IMXCryptoStore, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoConfig: MXCryptoConfig, - private val inboundGroupSessionStore: InboundGroupSessionStore, - private val sendToDeviceTask: DefaultSendToDeviceTask, - private val deviceListManager: DeviceListManager, - private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter) { - - private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher) - private val sequencer = SemaphoreCoroutineSequencer() - - // We only have one active key request per session, so we don't request if it's already requested - // But it could make sense to check more the backup, as it's evolving. - // We keep a stack as we consider that the key requested last is more likely to be on screen? - private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack>() - - fun requestKeyForEvent(event: Event, force: Boolean) { - val (targets, body) = getRoomKeyRequestTargetForEvent(event) ?: return - val index = ratchetIndexForMessage(event) ?: 0 - postRoomKeyRequest(body, targets, index, force) - } - - private fun getRoomKeyRequestTargetForEvent(event: Event): Pair>, RoomKeyRequestBody>? { - val sender = event.senderId ?: return null - val encryptedEventContent = event.content.toModel() ?: return null.also { - Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content") - } - if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null - - val senderDevice = encryptedEventContent.deviceId - val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { - mapOf( - myUserId to listOf("*") - ) - } else { - if (event.senderId == myUserId) { - mapOf( - myUserId to listOf("*") - ) - } else { - // for the case where you share the key with a device that has a broken olm session - // The other user might Re-shares a megolm session key with devices if the key has already been - // sent to them. - mapOf( - myUserId to listOf("*"), - - // We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700 - // so in this case query to all - sender to listOf(senderDevice ?: "*") - ) - } - } - - val requestBody = RoomKeyRequestBody( - roomId = event.roomId, - algorithm = encryptedEventContent.algorithm, - senderKey = encryptedEventContent.senderKey, - sessionId = encryptedEventContent.sessionId - ) - return recipients to requestBody - } - - private fun ratchetIndexForMessage(event: Event): Int? { - val encryptedContent = event.content.toModel() ?: return null - if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null - return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let { - tryOrNull { - val megolmVersion = it.read() - if (megolmVersion != 3) return@tryOrNull null - /** Int tag */ - if (it.read() != 8) return@tryOrNull null - it.read() - } - } - } - - fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean = false) { - outgoingRequestScope.launch { - sequencer.post { - internalQueueRequest(requestBody, recipients, fromIndex, force) - } - } - } - - /** - * Typically called when we the session as been imported or received meanwhile - */ - fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) { - outgoingRequestScope.launch { - sequencer.post { - internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex) - } - } - } - - fun onSelfCrossSigningTrustChanged(newTrust: Boolean) { - if (newTrust) { - // we were previously not cross signed, but we are now - // so there is now more chances to get better replies for existing request - // Let's forget about sent request so that next time we try to decrypt we will resend requests - // We don't resend all because we don't want to generate a bulk of traffic - outgoingRequestScope.launch { - sequencer.post { - cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT) - } - - sequencer.post { - delay(1000) - perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true) - } - } - } - } - - fun onRoomKeyForwarded(sessionId: String, - algorithm: String, - roomId: String, - senderKey: String, - fromDevice: String?, - fromIndex: Int, - event: Event) { - Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex") - outgoingRequestScope.launch { - sequencer.post { - cryptoStore.updateOutgoingRoomKeyReply( - roomId = roomId, - sessionId = sessionId, - algorithm = algorithm, - senderKey = senderKey, - fromDevice = fromDevice, - // strip out encrypted stuff as it's just a trail? - event = event.copy( - type = event.getClearType(), - content = mapOf( - "chain_index" to fromIndex - ) - ) - ) - } - } - } - - fun onRoomKeyWithHeld(sessionId: String, - algorithm: String, - roomId: String, - senderKey: String, - fromDevice: String?, - event: Event) { - outgoingRequestScope.launch { - sequencer.post { - Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice") - Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}") - - // We want to store withheld code from the sender of the message (owner of the megolm session), not from - // other devices that might gossip the key. If not the initial reason might be overridden - // by a request to one of our session. - event.getClearContent().toModel()?.let { withheld -> - withContext(coroutineDispatchers.crypto) { - tryOrNull { - deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false) - } - cryptoStore.getUserDeviceList(event.senderId ?: "") - .also { devices -> - Timber.tag(loggerTag.value) - .v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}") - } - ?.firstOrNull { - it.identityKey() == senderKey - } - }.also { - Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}") - }?.let { - if (it.userId == event.senderId) { - if (fromDevice != null) { - if (it.deviceId == fromDevice) { - Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") - cryptoStore.addWithHeldMegolmSession(withheld) - } - } else { - Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") - cryptoStore.addWithHeldMegolmSession(withheld) - } - } - } - } - - // Here we store the replies from a given request - cryptoStore.updateOutgoingRoomKeyReply( - roomId = roomId, - sessionId = sessionId, - algorithm = algorithm, - senderKey = senderKey, - fromDevice = fromDevice, - event = event - ) - } - } - } - - /** - * Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those) - */ - fun requireProcessAllPendingKeyRequests() { - outgoingRequestScope.launch { - sequencer.post { - internalProcessPendingKeyRequests() - } - } - } - - private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) { - // do we have known requests for that session?? - Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId") - val knownRequest = cryptoStore.getOutgoingRoomKeyRequest( - algorithm = MXCRYPTO_ALGORITHM_MEGOLM, - roomId = roomId, - sessionId = sessionId, - senderKey = senderKey - ) - if (knownRequest.isEmpty()) return Unit.also { - Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested") - } - if (knownRequest.size > 1) { - // It's worth logging, there should be only one - Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId") - } - knownRequest.forEach { request -> - when (request.state) { - OutgoingRoomKeyRequestState.UNSENT -> { - if (request.fromIndex >= localKnownChainIndex) { - // we have a good index we can cancel - cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) - } - } - OutgoingRoomKeyRequestState.SENT -> { - // It was already sent, and index satisfied we can cancel - if (request.fromIndex >= localKnownChainIndex) { - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) - } - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { - // It is already marked to be cancelled - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { - if (request.fromIndex >= localKnownChainIndex) { - // we just want to cancel now - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) - } - } - OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { - // was already canceled - // if we need a better index, should we resend? - } - } - } - } - - fun close() { - try { - outgoingRequestScope.cancel("User Terminate") - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear() - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).w("Failed to shutDown request manager") - } - } - - private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean) { - if (!cryptoStore.isKeyGossipingEnabled()) { - // we might want to try backup? - if (requestBody.roomId != null && requestBody.sessionId != null) { - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId) - } - Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled") - return - } - - Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force") - val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody) - Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}") - when (existing?.state) { - null -> { - // create a new one - cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) - } - OutgoingRoomKeyRequestState.UNSENT -> { - // nothing it's new or not yet handled - } - OutgoingRoomKeyRequestState.SENT -> { - // it was already requested - Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested") - if (force) { - // update to UNSENT - Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}") - cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) - } else { - if (existing.roomId != null && existing.sessionId != null) { - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId) - } - } - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { - // request is canceled only if I got the keys so what to do here... - if (force) { - cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) - } - } - OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { - // It's already going to resend - } - OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { - if (force) { - cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId) - cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) - } - } - } - - if (existing != null && existing.fromIndex >= fromIndex) { - // update the required index - cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex) - } - } - - private suspend fun internalProcessPendingKeyRequests() { - val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates()) - Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)") - - measureTimeMillis { - toProcess.forEach { - when (it.state) { - OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it) - OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it) - OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it) - OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, - OutgoingRoomKeyRequestState.SENT -> { - // these are filtered out - } - } - } - }.let { - Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms") - } - - val maxBackupCallsBySync = 60 - var currentCalls = 0 - measureTimeMillis { - while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) { - requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) -> - // we want to rate limit that somehow :/ - perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId) - } - currentCalls++ - } - }.let { - Timber.tag(loggerTag.value).v("Finish querying backup in $it ms") - } - } - - private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) { - // In order to avoid generating to_device traffic, we can first check if the key is backed up - Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}") - val sessionId = request.sessionId ?: return - val roomId = request.roomId ?: return - if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) { - // let's see what's the index - val knownIndex = tryOrNull { - inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex - } - if (knownIndex != null && knownIndex <= request.fromIndex) { - // we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request - Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request") - cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) - return - } - } - - // we need to send the request - val toDeviceContent = RoomKeyShareRequest( - requestingDeviceId = cryptoStore.getDeviceId(), - requestId = request.requestId, - action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, - body = request.requestBody - ) - val contentMap = MXUsersDevicesMap() - request.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - - val params = SendToDeviceTask.Params( - eventType = EventType.ROOM_KEY_REQUEST, - contentMap = contentMap, - transactionId = request.requestId - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.executeRetry(params, 3) - } - Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}") - // The request was sent, so update state - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT) - // TODO update the audit trail - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}") - } - } - - private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean { - Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}") - // we have to cancel this - val toDeviceContent = RoomKeyShareRequest( - requestingDeviceId = cryptoStore.getDeviceId(), - requestId = request.requestId, - action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION - ) - val contentMap = MXUsersDevicesMap() - request.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - - val params = SendToDeviceTask.Params( - eventType = EventType.ROOM_KEY_REQUEST, - contentMap = contentMap, - transactionId = request.requestId - ) - return try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.executeRetry(params, 3) - } - // The request cancellation was sent, we don't delete yet because we want - // to keep trace of the sent replies - cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED) - true - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}") - false - } - } - - private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) { - if (handleRequestToCancel(request)) { - // this will create a new unsent request with no replies that will be process in the following call - cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) - request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) } - } - } -} +/* + * 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.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState +import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.fromBase64 +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer +import timber.log.Timber +import java.util.Stack +import java.util.concurrent.Executors +import javax.inject.Inject +import kotlin.system.measureTimeMillis + +private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO) + +/** + * This class is responsible for sending key requests to other devices when a message failed to decrypt. + * It's lifecycle is based on the sync pulse: + * - You can post queries for session, or report when you got a session + * - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices + * If a request failed it will be retried at the end of the next sync + */ +@SessionScope +internal class OutgoingKeyRequestManager @Inject constructor( + @SessionId private val sessionId: String, + @UserId private val myUserId: String, + private val cryptoStore: IMXCryptoStore, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoConfig: MXCryptoConfig, + private val inboundGroupSessionStore: InboundGroupSessionStore, + private val sendToDeviceTask: SendToDeviceTask, + private val deviceListManager: DeviceListManager, + private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter) { + + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher) + private val sequencer = SemaphoreCoroutineSequencer() + + // We only have one active key request per session, so we don't request if it's already requested + // But it could make sense to check more the backup, as it's evolving. + // We keep a stack as we consider that the key requested last is more likely to be on screen? + private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack>() + + fun requestKeyForEvent(event: Event, force: Boolean) { + val (targets, body) = getRoomKeyRequestTargetForEvent(event) ?: return + val index = ratchetIndexForMessage(event) ?: 0 + postRoomKeyRequest(body, targets, index, force) + } + + private fun getRoomKeyRequestTargetForEvent(event: Event): Pair>, RoomKeyRequestBody>? { + val sender = event.senderId ?: return null + val encryptedEventContent = event.content.toModel() ?: return null.also { + Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content") + } + if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null + + val senderDevice = encryptedEventContent.deviceId + val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { + mapOf( + myUserId to listOf("*") + ) + } else { + if (event.senderId == myUserId) { + mapOf( + myUserId to listOf("*") + ) + } else { + // for the case where you share the key with a device that has a broken olm session + // The other user might Re-shares a megolm session key with devices if the key has already been + // sent to them. + mapOf( + myUserId to listOf("*"), + + // We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700 + // so in this case query to all + sender to listOf(senderDevice ?: "*") + ) + } + } + + val requestBody = RoomKeyRequestBody( + roomId = event.roomId, + algorithm = encryptedEventContent.algorithm, + senderKey = encryptedEventContent.senderKey, + sessionId = encryptedEventContent.sessionId + ) + return recipients to requestBody + } + + private fun ratchetIndexForMessage(event: Event): Int? { + val encryptedContent = event.content.toModel() ?: return null + if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null + return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let { + tryOrNull { + val megolmVersion = it.read() + if (megolmVersion != 3) return@tryOrNull null + /** Int tag */ + if (it.read() != 8) return@tryOrNull null + it.read() + } + } + } + + fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean = false) { + outgoingRequestScope.launch { + sequencer.post { + internalQueueRequest(requestBody, recipients, fromIndex, force) + } + } + } + + /** + * Typically called when we the session as been imported or received meanwhile + */ + fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) { + outgoingRequestScope.launch { + sequencer.post { + internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex) + } + } + } + + fun onSelfCrossSigningTrustChanged(newTrust: Boolean) { + if (newTrust) { + // we were previously not cross signed, but we are now + // so there is now more chances to get better replies for existing request + // Let's forget about sent request so that next time we try to decrypt we will resend requests + // We don't resend all because we don't want to generate a bulk of traffic + outgoingRequestScope.launch { + sequencer.post { + cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT) + } + + sequencer.post { + delay(1000) + perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true) + } + } + } + } + + fun onRoomKeyForwarded(sessionId: String, + algorithm: String, + roomId: String, + senderKey: String, + fromDevice: String?, + fromIndex: Int, + event: Event) { + Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex") + outgoingRequestScope.launch { + sequencer.post { + cryptoStore.updateOutgoingRoomKeyReply( + roomId = roomId, + sessionId = sessionId, + algorithm = algorithm, + senderKey = senderKey, + fromDevice = fromDevice, + // strip out encrypted stuff as it's just a trail? + event = event.copy( + type = event.getClearType(), + content = mapOf( + "chain_index" to fromIndex + ) + ) + ) + } + } + } + + fun onRoomKeyWithHeld(sessionId: String, + algorithm: String, + roomId: String, + senderKey: String, + fromDevice: String?, + event: Event) { + outgoingRequestScope.launch { + sequencer.post { + Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice") + Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}") + + // We want to store withheld code from the sender of the message (owner of the megolm session), not from + // other devices that might gossip the key. If not the initial reason might be overridden + // by a request to one of our session. + event.getClearContent().toModel()?.let { withheld -> + withContext(coroutineDispatchers.crypto) { + tryOrNull { + deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false) + } + cryptoStore.getUserDeviceList(event.senderId ?: "") + .also { devices -> + Timber.tag(loggerTag.value) + .v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}") + } + ?.firstOrNull { + it.identityKey() == senderKey + } + }.also { + Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}") + }?.let { + if (it.userId == event.senderId) { + if (fromDevice != null) { + if (it.deviceId == fromDevice) { + Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") + cryptoStore.addWithHeldMegolmSession(withheld) + } + } else { + Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") + cryptoStore.addWithHeldMegolmSession(withheld) + } + } + } + } + + // Here we store the replies from a given request + cryptoStore.updateOutgoingRoomKeyReply( + roomId = roomId, + sessionId = sessionId, + algorithm = algorithm, + senderKey = senderKey, + fromDevice = fromDevice, + event = event + ) + } + } + } + + /** + * Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those) + */ + fun requireProcessAllPendingKeyRequests() { + outgoingRequestScope.launch { + sequencer.post { + internalProcessPendingKeyRequests() + } + } + } + + private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) { + // do we have known requests for that session?? + Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId") + val knownRequest = cryptoStore.getOutgoingRoomKeyRequest( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey + ) + if (knownRequest.isEmpty()) return Unit.also { + Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested") + } + if (knownRequest.size > 1) { + // It's worth logging, there should be only one + Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId") + } + knownRequest.forEach { request -> + when (request.state) { + OutgoingRoomKeyRequestState.UNSENT -> { + if (request.fromIndex >= localKnownChainIndex) { + // we have a good index we can cancel + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + } + } + OutgoingRoomKeyRequestState.SENT -> { + // It was already sent, and index satisfied we can cancel + if (request.fromIndex >= localKnownChainIndex) { + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { + // It is already marked to be cancelled + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { + if (request.fromIndex >= localKnownChainIndex) { + // we just want to cancel now + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) + } + } + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { + // was already canceled + // if we need a better index, should we resend? + } + } + } + } + + fun close() { + try { + outgoingRequestScope.cancel("User Terminate") + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear() + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).w("Failed to shutDown request manager") + } + } + + private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean) { + if (!cryptoStore.isKeyGossipingEnabled()) { + // we might want to try backup? + if (requestBody.roomId != null && requestBody.sessionId != null) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId) + } + Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled") + return + } + + Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force") + val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody) + Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}") + when (existing?.state) { + null -> { + // create a new one + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) + } + OutgoingRoomKeyRequestState.UNSENT -> { + // nothing it's new or not yet handled + } + OutgoingRoomKeyRequestState.SENT -> { + // it was already requested + Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested") + if (force) { + // update to UNSENT + Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}") + cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) + } else { + if (existing.roomId != null && existing.sessionId != null) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId) + } + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { + // request is canceled only if I got the keys so what to do here... + if (force) { + cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { + // It's already going to resend + } + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { + if (force) { + cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId) + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) + } + } + } + + if (existing != null && existing.fromIndex >= fromIndex) { + // update the required index + cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex) + } + } + + private suspend fun internalProcessPendingKeyRequests() { + val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates()) + Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)") + + measureTimeMillis { + toProcess.forEach { + when (it.state) { + OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it) + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it) + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it) + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, + OutgoingRoomKeyRequestState.SENT -> { + // these are filtered out + } + } + } + }.let { + Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms") + } + + val maxBackupCallsBySync = 60 + var currentCalls = 0 + measureTimeMillis { + while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) -> + // we want to rate limit that somehow :/ + perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId) + } + currentCalls++ + } + }.let { + Timber.tag(loggerTag.value).v("Finish querying backup in $it ms") + } + } + + private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) { + // In order to avoid generating to_device traffic, we can first check if the key is backed up + Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}") + val sessionId = request.sessionId ?: return + val roomId = request.roomId ?: return + if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) { + // let's see what's the index + val knownIndex = tryOrNull { + inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex + } + if (knownIndex != null && knownIndex <= request.fromIndex) { + // we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request + Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request") + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + return + } + } + + // we need to send the request + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = cryptoStore.getDeviceId(), + requestId = request.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + body = request.requestBody + ) + val contentMap = MXUsersDevicesMap() + request.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + val params = SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = request.requestId + ) + try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}") + // The request was sent, so update state + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT) + // TODO update the audit trail + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}") + } + } + + private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean { + Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}") + // we have to cancel this + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = cryptoStore.getDeviceId(), + requestId = request.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION + ) + val contentMap = MXUsersDevicesMap() + request.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + val params = SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = request.requestId + ) + return try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + // The request cancellation was sent, we don't delete yet because we want + // to keep trace of the sent replies + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED) + true + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}") + false + } + } + + private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) { + if (handleRequestToCancel(request)) { + // this will create a new unsent request with no replies that will be process in the following call + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt deleted file mode 100755 index fae7a2f1c4..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt +++ /dev/null @@ -1,39 +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 - -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState - -/** - * Represents an outgoing room key request - */ -@JsonClass(generateAdapter = true) -internal class OutgoingSecretRequest( - // Secret Name - val secretName: String?, - // list of recipients for the request - override var recipients: Map>, - // Unique id for this request. Used for both - // an id within the request for later pairing with a cancellation, and for - // the transaction id when sending the to_device messages to our local - override var requestId: String, - // current state of this request - override var state: OutgoingRoomKeyRequestState) : OutgoingGossipingRequest { - - // transaction id for the cancellation, if any -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt index 8e0def5b76..292ba02ecd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt @@ -66,7 +66,7 @@ internal class PerSessionBackupQueryRateLimiter @Inject constructor( private var backupVersion: KeysVersionResult? = null private var savedKeyBackupKeyInfo: SavedKeyBackupKeyInfo? = null var backupWasCheckedFromServer: Boolean = false - var now = System.currentTimeMillis() + val now = System.currentTimeMillis() fun refreshBackupInfoIfNeeded(force: Boolean = false) { if (backupWasCheckedFromServer && !force) return diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt index 58378e556a..1a8c160d9c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt @@ -28,7 +28,6 @@ import javax.inject.Inject @SessionScope internal class RoomEncryptorsStore @Inject constructor( private val cryptoStore: IMXCryptoStore, - // Repository private val megolmEncryptionFactory: MXMegolmEncryptionFactory, private val olmEncryptionFactory: MXOlmEncryptionFactory, ) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt index b10c77dfd3..dca4b07416 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt @@ -1,298 +1,298 @@ -/* - * Copyright (c) 2022 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.logger.LoggerTag -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 -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.util.toBase64NoPadding -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.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.session.SessionScope -import timber.log.Timber -import javax.inject.Inject - -private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO) - -@SessionScope -internal class SecretShareManager @Inject constructor( - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val cryptoCoroutineScope: CoroutineScope, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers -) { - - companion object { - private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes - } - - /** - * Secret gossiping only occurs during a limited window period after interactive verification. - * We keep track of recent verification in memory for that purpose (no need to persist) - */ - private val recentlyVerifiedDevices = mutableMapOf() - private val verifMutex = Mutex() - - /** - * Secrets are exchanged as part of interactive verification, - * so we can just store in memory. - */ - private val outgoingSecretRequests = mutableListOf() - - // the listeners - private val gossipingRequestListeners: MutableSet = HashSet() - - fun addListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.add(listener) - } - } - - fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.remove(listener) - } - } - - /** - * Called when a session has been verified. - * This information can be used by the manager to decide whether or not to fullfill gossiping requests. - * This should be called as fast as possible after a successful self interactive verification - */ - fun onVerificationCompleteForDevice(deviceId: String) { - // For now we just keep an in memory cache - cryptoCoroutineScope.launch { - verifMutex.withLock { - recentlyVerifiedDevices[deviceId] = System.currentTimeMillis() - } - } - } - - suspend fun handleSecretRequest(toDevice: Event) { - val request = toDevice.getClearContent().toModel() - ?: return Unit.also { - Timber.tag(loggerTag.value) - .w("handleSecretRequest() : malformed request") - } - -// val (action, requestingDeviceId, requestId, secretName) = it - val secretName = request.secretName ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("handleSecretRequest() : Missing secret name") - } - - val userId = toDevice.senderId ?: return Unit.also { - Timber.tag(loggerTag.value) - .v("handleSecretRequest() : Missing secret name") - } - - if (userId != credentials.userId) { - // secrets are only shared between our own devices - Timber.tag(loggerTag.value) - .e("Ignoring secret share request from other users $userId") - return - } - - val deviceId = request.requestingDeviceId - ?: return Unit.also { - Timber.tag(loggerTag.value) - .w("handleSecretRequest() : malformed request norequestingDeviceId ") - } - - val device = cryptoStore.getUserDevice(credentials.userId, deviceId) - ?: return Unit.also { - Timber.tag(loggerTag.value) - .e("Received secret share request from unknown device $deviceId") - } - - val isRequestingDeviceTrusted = device.isVerified - val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId) - if (isRequestingDeviceTrusted && isRecentInteractiveVerification) { - // we can share the secret - - val secretValue = when (secretName) { - MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master - SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned - USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user - KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey - ?.let { - extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() - } - else -> null - } - if (secretValue == null) { - Timber.tag(loggerTag.value) - .i("The secret is unknown $secretName, passing to app layer") - val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() } - toList.onEach { listener -> - listener.onSecretShareRequest(request) - } - return - } - - val payloadJson = mapOf( - "type" to EventType.SEND_SECRET, - "content" to mapOf( - "request_id" to request.requestId, - "secret" to secretValue - ) - ) - - // Is it possible that we don't have an olm session? - val devicesByUser = mapOf(device.userId to listOf(device)) - val usersDeviceMap = try { - ensureOlmSessionsForDevicesAction.handle(devicesByUser) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .w("Can't share secret ${request.secretName}: Failed to establish olm session") - return - } - - val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId) - if (olmSessionResult?.sessionId == null) { - Timber.tag(loggerTag.value) - .w("secret share: no session with this device $deviceId, probably because there were no one-time keys") - return - } - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload) - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - try { - // raise the retries for secret - sendToDeviceTask.executeRetry(sendToDeviceParams, 6) - Timber.tag(loggerTag.value) - .i("successfully shared secret $secretName to ${device.shortDebugString()}") - // TODO add a trail for that in audit logs - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}") - } - } else { - Timber.tag(loggerTag.value) - .d(" Received secret share request from un-authorised device ${device.deviceId}") - } - } - - private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { - val verifTimestamp = verifMutex.withLock { - recentlyVerifiedDevices[deviceId] - } ?: return false - - val age = System.currentTimeMillis() - verifTimestamp - - return age < SECRET_SHARE_WINDOW_DURATION - } - - suspend fun requestSecretTo(deviceId: String, secretName: String) { - val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also { - Timber.tag(loggerTag.value) - .d("Can't request secret for $secretName unknown device $deviceId") - } - val toDeviceContent = SecretShareRequest( - requestingDeviceId = credentials.deviceId, - secretName = secretName, - requestId = createUniqueTxnId() - ) - - verifMutex.withLock { - outgoingSecretRequests.add(toDeviceContent) - } - - val contentMap = MXUsersDevicesMap() - contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent) - - val params = SendToDeviceTask.Params( - eventType = EventType.REQUEST_SECRET, - contentMap = contentMap - ) - try { - withContext(coroutineDispatchers.io) { - sendToDeviceTask.executeRetry(params, 3) - } - Timber.tag(loggerTag.value) - .d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}") - // TODO update the audit trail - } catch (failure: Throwable) { - Timber.tag(loggerTag.value) - .w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}") - } - } - - suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) { - Timber.tag(loggerTag.value) - .i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}") - if (!toDevice.isEncrypted()) { - // secret send messages must be encrypted - Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") - return - } - - // Was that sent by us? - if (toDevice.senderId != credentials.userId) { - Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") - return - } - - val secretContent = toDevice.getClearContent().toModel() ?: return - - val existingRequest = verifMutex.withLock { - outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId } - } - - // As per spec: - // Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to. - if (existingRequest?.secretName == null) { - Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") - return - } - // we don't need to cancel the request as we only request to one device - // just forget about the request now - verifMutex.withLock { - outgoingSecretRequests.remove(existingRequest) - } - - if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) { - // TODO Ask to application layer? - Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK") - } - } -} +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.logger.LoggerTag +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 +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.toBase64NoPadding +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.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO) + +@SessionScope +internal class SecretShareManager @Inject constructor( + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val cryptoCoroutineScope: CoroutineScope, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers +) { + + companion object { + private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes + } + + /** + * Secret gossiping only occurs during a limited window period after interactive verification. + * We keep track of recent verification in memory for that purpose (no need to persist) + */ + private val recentlyVerifiedDevices = mutableMapOf() + private val verifMutex = Mutex() + + /** + * Secrets are exchanged as part of interactive verification, + * so we can just store in memory. + */ + private val outgoingSecretRequests = mutableListOf() + + // the listeners + private val gossipingRequestListeners: MutableSet = HashSet() + + fun addListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.add(listener) + } + } + + fun removeListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.remove(listener) + } + } + + /** + * Called when a session has been verified. + * This information can be used by the manager to decide whether or not to fullfill gossiping requests. + * This should be called as fast as possible after a successful self interactive verification + */ + fun onVerificationCompleteForDevice(deviceId: String) { + // For now we just keep an in memory cache + cryptoCoroutineScope.launch { + verifMutex.withLock { + recentlyVerifiedDevices[deviceId] = System.currentTimeMillis() + } + } + } + + suspend fun handleSecretRequest(toDevice: Event) { + val request = toDevice.getClearContent().toModel() + ?: return Unit.also { + Timber.tag(loggerTag.value) + .w("handleSecretRequest() : malformed request") + } + +// val (action, requestingDeviceId, requestId, secretName) = it + val secretName = request.secretName ?: return Unit.also { + Timber.tag(loggerTag.value) + .v("handleSecretRequest() : Missing secret name") + } + + val userId = toDevice.senderId ?: return Unit.also { + Timber.tag(loggerTag.value) + .v("handleSecretRequest() : Missing senderId") + } + + if (userId != credentials.userId) { + // secrets are only shared between our own devices + Timber.tag(loggerTag.value) + .e("Ignoring secret share request from other users $userId") + return + } + + val deviceId = request.requestingDeviceId + ?: return Unit.also { + Timber.tag(loggerTag.value) + .w("handleSecretRequest() : malformed request norequestingDeviceId ") + } + + val device = cryptoStore.getUserDevice(credentials.userId, deviceId) + ?: return Unit.also { + Timber.tag(loggerTag.value) + .e("Received secret share request from unknown device $deviceId") + } + + val isRequestingDeviceTrusted = device.isVerified + val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId) + if (isRequestingDeviceTrusted && isRecentInteractiveVerification) { + // we can share the secret + + val secretValue = when (secretName) { + MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master + SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned + USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user + KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey + ?.let { + extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() + } + else -> null + } + if (secretValue == null) { + Timber.tag(loggerTag.value) + .i("The secret is unknown $secretName, passing to app layer") + val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() } + toList.onEach { listener -> + listener.onSecretShareRequest(request) + } + return + } + + val payloadJson = mapOf( + "type" to EventType.SEND_SECRET, + "content" to mapOf( + "request_id" to request.requestId, + "secret" to secretValue + ) + ) + + // Is it possible that we don't have an olm session? + val devicesByUser = mapOf(device.userId to listOf(device)) + val usersDeviceMap = try { + ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .w("Can't share secret ${request.secretName}: Failed to establish olm session") + return + } + + val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId) + if (olmSessionResult?.sessionId == null) { + Timber.tag(loggerTag.value) + .w("secret share: no session with this device $deviceId, probably because there were no one-time keys") + return + } + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload) + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + try { + // raise the retries for secret + sendToDeviceTask.executeRetry(sendToDeviceParams, 6) + Timber.tag(loggerTag.value) + .i("successfully shared secret $secretName to ${device.shortDebugString()}") + // TODO add a trail for that in audit logs + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}") + } + } else { + Timber.tag(loggerTag.value) + .d(" Received secret share request from un-authorised device ${device.deviceId}") + } + } + + private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { + val verifTimestamp = verifMutex.withLock { + recentlyVerifiedDevices[deviceId] + } ?: return false + + val age = System.currentTimeMillis() - verifTimestamp + + return age < SECRET_SHARE_WINDOW_DURATION + } + + suspend fun requestSecretTo(deviceId: String, secretName: String) { + val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also { + Timber.tag(loggerTag.value) + .d("Can't request secret for $secretName unknown device $deviceId") + } + val toDeviceContent = SecretShareRequest( + requestingDeviceId = credentials.deviceId, + secretName = secretName, + requestId = createUniqueTxnId() + ) + + verifMutex.withLock { + outgoingSecretRequests.add(toDeviceContent) + } + + val contentMap = MXUsersDevicesMap() + contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent) + + val params = SendToDeviceTask.Params( + eventType = EventType.REQUEST_SECRET, + contentMap = contentMap + ) + try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + Timber.tag(loggerTag.value) + .d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}") + // TODO update the audit trail + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}") + } + } + + suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) { + Timber.tag(loggerTag.value) + .i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}") + if (!toDevice.isEncrypted()) { + // secret send messages must be encrypted + Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") + return + } + + // Was that sent by us? + if (toDevice.senderId != credentials.userId) { + Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") + return + } + + val secretContent = toDevice.getClearContent().toModel() ?: return + + val existingRequest = verifMutex.withLock { + outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId } + } + + // As per spec: + // Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to. + if (existingRequest?.secretName == null) { + Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + return + } + // we don't need to cancel the request as we only request to one device + // just forget about the request now + verifMutex.withLock { + outgoingSecretRequests.remove(existingRequest) + } + + if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) { + // TODO Ask to application layer? + Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 37fb8ba0f9..1daf2d2617 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -229,7 +229,7 @@ internal class MXMegolmDecryption( } Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") - val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, + val addSessionResult = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, roomKeyContent.sessionKey, roomKeyContent.roomId, senderKey, @@ -237,9 +237,9 @@ internal class MXMegolmDecryption( keysClaimed, exportFormat) - when (added) { - is MXOlmDevice.AddSessionResult.Imported -> added.ratchetIndex - is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> added.newIndex + when (addSessionResult) { + is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex + is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex else -> null }?.let { index -> if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { @@ -273,7 +273,7 @@ internal class MXMegolmDecryption( } } - if (added is MXOlmDevice.AddSessionResult.Imported) { + if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) { Timber.tag(loggerTag.value) .d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}") defaultKeysBackupService.maybeBackupKeys() 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 2db3dba50d..7e69c2114a 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 @@ -892,7 +892,8 @@ internal class RealmCryptoStore @Inject constructor( val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier() val key = OlmInboundGroupSessionEntity.createPrimaryKey( sessionIdentifier, - olmInboundGroupSessionWrapper.senderKey) + olmInboundGroupSessionWrapper.senderKey + ) val existing = realm.where() .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) @@ -1049,7 +1050,7 @@ internal class RealmCryptoStore @Inject constructor( .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) }.map { - it.toOutgoingGossipingRequest() + it.toOutgoingKeyRequest() }.firstOrNull { it.requestBody?.algorithm == requestBody.algorithm && it.requestBody?.roomId == requestBody.roomId && @@ -1063,7 +1064,7 @@ internal class RealmCryptoStore @Inject constructor( realm.where() .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) }.map { - it.toOutgoingGossipingRequest() + it.toOutgoingKeyRequest() }.firstOrNull() } @@ -1074,7 +1075,7 @@ internal class RealmCryptoStore @Inject constructor( .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) }.map { - it.toOutgoingGossipingRequest() + it.toOutgoingKeyRequest() }.filter { it.requestBody?.algorithm == algorithm && it.requestBody?.senderKey == senderKey @@ -1088,30 +1089,35 @@ internal class RealmCryptoStore @Inject constructor( val dataSourceFactory = realmDataSourceFactory.map { AuditTrailMapper.map(it) // mm we can't map not null... - ?: AuditTrail( - System.currentTimeMillis(), - TrailType.Unknown, - IncomingKeyRequestInfo( - "", - "", - "", - "", - "", - "", - "", - ) - ) + ?: createUnknownTrail() } - return monarchy.findAllPagedWithChanges(realmDataSourceFactory, - LivePagedListBuilder(dataSourceFactory, + return monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder( + dataSourceFactory, PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(false) .setPrefetchDistance(1) - .build()) + .build() + ) ) } + private fun createUnknownTrail() = AuditTrail( + System.currentTimeMillis(), + TrailType.Unknown, + IncomingKeyRequestInfo( + "", + "", + "", + "", + "", + "", + "", + ) + ) + override fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> realm.where() @@ -1121,28 +1127,19 @@ internal class RealmCryptoStore @Inject constructor( val dataSourceFactory = realmDataSourceFactory.map { entity -> (AuditTrailMapper.map(entity) // mm we can't map not null... - ?: AuditTrail( - System.currentTimeMillis(), - type, - IncomingKeyRequestInfo( - "", - "", - "", - "", - "", - "", - "", - ) - ) + ?: createUnknownTrail() ).let { mapper.invoke(it) } } - return monarchy.findAllPagedWithChanges(realmDataSourceFactory, - LivePagedListBuilder(dataSourceFactory, + return monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder( + dataSourceFactory, PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(false) .setPrefetchDistance(1) - .build()) + .build() + ) ) } @@ -1166,7 +1163,7 @@ internal class RealmCryptoStore @Inject constructor( .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) .findAll() .map { - it.toOutgoingGossipingRequest() + it.toOutgoingKeyRequest() }.also { if (it.size > 1) { // there should be one or zero but not more, worth warning @@ -1188,7 +1185,7 @@ internal class RealmCryptoStore @Inject constructor( this.requestState = OutgoingRoomKeyRequestState.UNSENT this.setRequestBody(requestBody) this.creationTimeStamp = System.currentTimeMillis() - }.toOutgoingGossipingRequest() + }.toOutgoingKeyRequest() } else { request = existing } @@ -1231,7 +1228,7 @@ internal class RealmCryptoStore @Inject constructor( .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) .findAll().firstOrNull { entity -> - entity.toOutgoingGossipingRequest().let { + entity.toOutgoingKeyRequest().let { it.requestBody?.senderKey == senderKey && it.requestBody?.algorithm == algorithm } @@ -1473,7 +1470,7 @@ internal class RealmCryptoStore @Inject constructor( realm .where(OutgoingKeyRequestEntity::class.java) }, { entity -> - entity.toOutgoingGossipingRequest() + entity.toOutgoingKeyRequest() }) .filterNotNull() } @@ -1484,7 +1481,7 @@ internal class RealmCryptoStore @Inject constructor( .where(OutgoingKeyRequestEntity::class.java) .`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray()) }, { entity -> - entity.toOutgoingGossipingRequest() + entity.toOutgoingKeyRequest() }) .filterNotNull() } @@ -1495,15 +1492,18 @@ internal class RealmCryptoStore @Inject constructor( .where(OutgoingKeyRequestEntity::class.java) } val dataSourceFactory = realmDataSourceFactory.map { - it.toOutgoingGossipingRequest() + it.toOutgoingKeyRequest() } - val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory, - LivePagedListBuilder(dataSourceFactory, + val trail = monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + LivePagedListBuilder( + dataSourceFactory, PagedList.Config.Builder() .setPageSize(20) .setEnablePlaceholders(false) .setPrefetchDistance(1) - .build()) + .build() + ) ) return trail } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt index 11c2514845..7a8ba18809 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt @@ -58,7 +58,7 @@ internal open class OutgoingKeyRequestEntity( ) } - fun getRequestedKeyInfo(): RoomKeyRequestBody? = RoomKeyRequestBody.fromJson(requestedInfoStr) + private fun getRequestedKeyInfo(): RoomKeyRequestBody? = RoomKeyRequestBody.fromJson(requestedInfoStr) fun setRequestBody(body: RoomKeyRequestBody) { requestedInfoStr = body.toJson() @@ -92,7 +92,7 @@ internal open class OutgoingKeyRequestEntity( replies.add(newReply) } - fun toOutgoingGossipingRequest(): OutgoingKeyRequest { + fun toOutgoingKeyRequest(): OutgoingKeyRequest { return OutgoingKeyRequest( requestBody = getRequestedKeyInfo(), recipients = getRecipients().orEmpty(), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt index fa7dfd8726..c9f86baa29 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room.membership.joining import io.realm.RealmConfiguration import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult @@ -71,13 +70,11 @@ internal class DefaultJoinRoomTask @Inject constructor( } val joinRoomResponse = try { executeRequest(globalErrorReceiver) { - withContext(coroutineDispatcher.io) { - roomAPI.join( - roomIdOrAlias = params.roomIdOrAlias, - viaServers = params.viaServers.take(3), - params = extraParams - ) - } + roomAPI.join( + roomIdOrAlias = params.roomIdOrAlias, + viaServers = params.viaServers.take(3), + params = extraParams + ) } } catch (failure: Throwable) { roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure))