{
- Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}")
- // get the user ids which did not already trigger a keys download
- val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
- if (filteredUsers.isEmpty()) {
- // trigger nothing
- return MXUsersDevicesMap()
- }
- val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
- val response = try {
- downloadKeysForUsersTask.execute(params)
- } catch (throwable: Throwable) {
- Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
- if (throwable is CancellationException) {
- // the crypto module is getting closed, so we cannot access the DB anymore
- Timber.w("The crypto module is closed, ignoring this error")
- } else {
- onKeysDownloadFailed(filteredUsers)
- }
- throw throwable
- }
- Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
- for (userId in filteredUsers) {
- // al devices =
- val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
-
- Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models")
- if (!models.isNullOrEmpty()) {
- val workingCopy = models.toMutableMap()
- for ((deviceId, deviceInfo) in models) {
- // Get the potential previously store device keys for this device
- val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId)
-
- // in some race conditions (like unit tests)
- // the self device must be seen as verified
- if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) {
- deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true)
- }
- // Validate received keys
- if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
- // New device keys are not valid. Do not store them
- workingCopy.remove(deviceId)
- if (null != previouslyStoredDeviceKeys) {
- // But keep old validated ones if any
- workingCopy[deviceId] = previouslyStoredDeviceKeys
- }
- } else if (null != previouslyStoredDeviceKeys) {
- // The verified status is not sync'ed with hs.
- // This is a client side information, valid only for this client.
- // So, transfer its previous value
- workingCopy[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel
- }
- }
- // Update the store
- // Note that devices which aren't in the response will be removed from the stores
- cryptoStore.storeUserDevices(userId, workingCopy)
- }
-
- val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
- Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
- }
- val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
- Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
- }
- val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
- Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
- }
- cryptoStore.storeUserCrossSigningKeys(
- userId,
- masterKey,
- selfSigningKey,
- userSigningKey
- )
- }
-
- // Update devices trust for these users
- // dispatchDeviceChange(downloadUsers)
-
- return onKeysDownloadSucceed(filteredUsers, response.failures)
- }
-
- /**
- * Validate device keys.
- * This method must called on getEncryptingThreadHandler() thread.
- *
- * @param deviceKeys the device keys to validate.
- * @param userId the id of the user of the device.
- * @param deviceId the id of the device.
- * @param previouslyStoredDeviceKeys the device keys we received before for this device
- * @return true if succeeds
- */
- private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean {
- if (null == deviceKeys) {
- Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
- return false
- }
-
- if (null == deviceKeys.keys) {
- Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId")
- return false
- }
-
- if (null == deviceKeys.signatures) {
- Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId")
- return false
- }
-
- // Check that the user_id and device_id in the received deviceKeys are correct
- if (deviceKeys.userId != userId) {
- Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
- return false
- }
-
- if (deviceKeys.deviceId != deviceId) {
- Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
- return false
- }
-
- val signKeyId = "ed25519:" + deviceKeys.deviceId
- val signKey = deviceKeys.keys[signKeyId]
-
- if (null == signKey) {
- Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
- return false
- }
-
- val signatureMap = deviceKeys.signatures[userId]
-
- if (null == signatureMap) {
- Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
- return false
- }
-
- val signature = signatureMap[signKeyId]
-
- if (null == signature) {
- Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
- return false
- }
-
- var isVerified = false
- var errorMessage: String? = null
-
- try {
- olmDevice.verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature)
- isVerified = true
- } catch (e: Exception) {
- errorMessage = e.message
- }
-
- if (!isVerified) {
- Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" +
- deviceKeys.deviceId + " with error " + errorMessage)
- return false
- }
-
- if (null != previouslyStoredDeviceKeys) {
- if (previouslyStoredDeviceKeys.fingerprint() != signKey) {
- // This should only happen if the list has been MITMed; we are
- // best off sticking with the original keys.
- //
- // Should we warn the user about it somehow?
- Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" +
- deviceKeys.deviceId + " has changed : " +
- previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
-
- Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
- Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
-
- return false
- }
- }
-
- return true
- }
-
- /**
- * Start device queries for any users who sent us an m.new_device recently
- * This method must be called on getEncryptingThreadHandler() thread.
- */
- suspend fun refreshOutdatedDeviceLists() {
- Timber.v("## CRYPTO | refreshOutdatedDeviceLists()")
- val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
-
- val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
- TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]
- }
-
- if (users.isEmpty()) {
- return
- }
-
- // update the statuses
- users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS }
-
- cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
- runCatching {
- doKeyDownloadForUsers(users)
- }.fold(
- {
- Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done")
- },
- {
- Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
- }
- )
- }
-
- companion object {
-
- /**
- * State transition diagram for DeviceList.deviceTrackingStatus
- *
- *
- * |
- * stopTrackingDeviceList V
- * +---------------------> NOT_TRACKED
- * | |
- * +<--------------------+ | startTrackingDeviceList
- * | | V
- * | +-------------> PENDING_DOWNLOAD <--------------------+-+
- * | | ^ | | |
- * | | restart download | | start download | | invalidateUserDeviceList
- * | | client failed | | | |
- * | | | V | |
- * | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
- * | | | |
- * +<-------------------+ | download successful |
- * ^ V |
- * +----------------------- UP_TO_DATE ------------------------+
- *
- *
- */
-
- const val TRACKING_STATUS_NOT_TRACKED = -1
- const val TRACKING_STATUS_PENDING_DOWNLOAD = 1
- const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2
- const val TRACKING_STATUS_UP_TO_DATE = 3
- const val TRACKING_STATUS_UNREACHABLE_SERVER = 4
- }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt
new file mode 100644
index 0000000000..db5168d31c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EncryptEventContentUseCase.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto
+
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
+import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.internal.util.time.Clock
+import timber.log.Timber
+import javax.inject.Inject
+
+private val loggerTag = LoggerTag("EncryptEventContentUseCase", LoggerTag.CRYPTO)
+
+internal class EncryptEventContentUseCase @Inject constructor(olmMachineProvider: OlmMachineProvider,
+ private val prepareToEncrypt: PrepareToEncryptUseCase,
+ private val clock: Clock) {
+
+ private val olmMachine = olmMachineProvider.olmMachine
+
+ suspend operator fun invoke(eventContent: Content,
+ eventType: String,
+ roomId: String): MXEncryptEventContentResult {
+ val t0 = clock.epochMillis()
+ prepareToEncrypt(roomId, ensureAllMembersAreLoaded = false)
+ val content = olmMachine.encrypt(roomId, eventType, eventContent)
+ Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms")
+ return MXEncryptEventContentResult(content, EventType.ENCRYPTED)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetRoomUserIdsUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetRoomUserIdsUseCase.kt
new file mode 100644
index 0000000000..75d3e8b0bb
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GetRoomUserIdsUseCase.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto
+
+import javax.inject.Inject
+
+internal class GetRoomUserIdsUseCase @Inject constructor(private val shouldEncryptForInvitedMembers: ShouldEncryptForInvitedMembersUseCase,
+ private val cryptoSessionInfoProvider: CryptoSessionInfoProvider) {
+
+ operator fun invoke(roomId: String): List {
+ return cryptoSessionInfoProvider.getRoomUserIds(roomId, shouldEncryptForInvitedMembers(roomId))
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt
deleted file mode 100644
index 0013c31eea..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt
+++ /dev/null
@@ -1,57 +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 androidx.work.BackoffPolicy
-import androidx.work.Data
-import androidx.work.ExistingWorkPolicy
-import androidx.work.ListenableWorker
-import androidx.work.OneTimeWorkRequest
-import org.matrix.android.sdk.api.util.Cancelable
-import org.matrix.android.sdk.internal.di.WorkManagerProvider
-import org.matrix.android.sdk.internal.session.SessionScope
-import org.matrix.android.sdk.internal.util.CancelableWork
-import org.matrix.android.sdk.internal.worker.startChain
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-
-@SessionScope
-internal class GossipingWorkManager @Inject constructor(
- private val workManagerProvider: WorkManagerProvider
-) {
-
- inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
- return workManagerProvider.matrixOneTimeWorkRequestBuilder()
- .setConstraints(WorkManagerProvider.workConstraints)
- .startChain(startChain)
- .setInputData(data)
- .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
- .build()
- }
-
- // Prevent sending queue to stay broken after app restart
- // The unique queue id will stay the same as long as this object is instanciated
- val queueSuffixApp = System.currentTimeMillis()
-
- fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
- workManagerProvider.workManager
- .beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest)
- .enqueue()
-
- return CancelableWork(workManagerProvider.workManager, workRequest.id)
- }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
index e7a46750b0..28ddf291b2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
@@ -19,8 +19,10 @@ package org.matrix.android.sdk.internal.crypto
import android.util.LruCache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber
@@ -28,6 +30,13 @@ import java.util.Timer
import java.util.TimerTask
import javax.inject.Inject
+internal data class InboundGroupSessionHolder(
+ val wrapper: OlmInboundGroupSessionWrapper2,
+ val mutex: Mutex = Mutex()
+)
+
+private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO)
+
/**
* Allows to cache and batch store operations on inbound group session store.
* Because it is used in the decrypt flow, that can be called quite rapidly
@@ -42,12 +51,13 @@ internal class InboundGroupSessionStore @Inject constructor(
val senderKey: String
)
- private val sessionCache = object : LruCache(30) {
- override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) {
- if (evicted && oldValue != null) {
+ private val sessionCache = object : LruCache(100) {
+ override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) {
+ if (oldValue != null) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}")
- store.storeInboundGroupSessions(listOf(oldValue))
+ Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
+ store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
+ oldValue.wrapper.olmInboundGroupSession?.releaseSession()
}
}
}
@@ -59,27 +69,50 @@ internal class InboundGroupSessionStore @Inject constructor(
private val dirtySession = mutableListOf()
@Synchronized
- fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
- synchronized(sessionCache) {
- val known = sessionCache[CacheKey(sessionId, senderKey)]
- Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}")
- return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
- Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
- sessionCache.put(CacheKey(sessionId, senderKey), it)
- }
- }
+ fun clear() {
+ sessionCache.evictAll()
}
@Synchronized
- fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) {
- Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}")
+ fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? {
+ val known = sessionCache[CacheKey(sessionId, senderKey)]
+ Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession $sessionId in cache ${known != null}")
+ return known
+ ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
+ Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
+ sessionCache.put(CacheKey(sessionId, senderKey), InboundGroupSessionHolder(it))
+ }?.let {
+ InboundGroupSessionHolder(it)
+ }
+ }
+
+ @Synchronized
+ fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+ Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
+ dirtySession.remove(old.wrapper)
+ store.removeInboundGroupSession(sessionId, senderKey)
+ sessionCache.remove(CacheKey(sessionId, senderKey))
+
+ // release removed session
+ old.wrapper.olmInboundGroupSession?.releaseSession()
+
+ internalStoreGroupSession(new, sessionId, senderKey)
+ }
+
+ @Synchronized
+ fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+ internalStoreGroupSession(holder, sessionId, senderKey)
+ }
+
+ private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+ Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
// We want to batch this a bit for performances
- dirtySession.add(wrapper)
+ dirtySession.add(holder.wrapper)
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
// first time seen, put it in memory cache while waiting for batch insert
// If it's already known, no need to update cache it's already there
- sessionCache.put(CacheKey(sessionId, senderKey), wrapper)
+ sessionCache.put(CacheKey(sessionId, senderKey), holder)
}
timerTask?.cancel()
@@ -96,7 +129,7 @@ internal class InboundGroupSessionStore @Inject constructor(
val toSave = mutableListOf