diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt index 669e27edfd..4dc1d04599 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt @@ -125,21 +125,21 @@ class FlowSession(private val session: Session) { } fun liveUserCryptoDevices(userId: String): Flow> { - return session.cryptoService().getLiveCryptoDeviceInfo(userId).asFlow() + return session.cryptoService().getLiveCryptoDeviceInfo(userId) .startWith(session.coroutineDispatchers.io) { session.cryptoService().getCryptoDeviceInfo(userId) } } fun liveCrossSigningInfo(userId: String): Flow> { - return session.cryptoService().crossSigningService().getLiveCrossSigningKeys(userId).asFlow() + return session.cryptoService().crossSigningService().getLiveCrossSigningKeys(userId) .startWith(session.coroutineDispatchers.io) { session.cryptoService().crossSigningService().getUserCrossSigningKeys(userId).toOptional() } } fun liveCrossSigningPrivateKeys(): Flow> { - return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().asFlow() + return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys() .startWith(session.coroutineDispatchers.io) { session.cryptoService().crossSigningService().getCrossSigningPrivateKeys().toOptional() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 699dbf1d92..4244df2614 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.session.crypto import android.content.Context import androidx.lifecycle.LiveData import androidx.paging.PagedList +import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.listeners.ProgressListener @@ -123,9 +124,9 @@ interface CryptoService { fun getCryptoDeviceInfo(userId: String): List - fun getLiveCryptoDeviceInfo(userId: String): LiveData> + fun getLiveCryptoDeviceInfo(userId: String): Flow> - fun getLiveCryptoDeviceInfo(userIds: List): LiveData> + fun getLiveCryptoDeviceInfo(userIds: List): Flow> fun addNewSessionListener(newSessionListener: NewSessionListener) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt index 1d97488ebc..70330171c5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning import androidx.lifecycle.LiveData +import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel @@ -29,31 +30,30 @@ interface CrossSigningService { /** * Is our own device signed by our own cross signing identity */ - fun isCrossSigningVerified(): Boolean + suspend fun isCrossSigningVerified(): Boolean // TODO this isn't used anywhere besides in tests? // Is this the local trust concept that we have for devices? - fun isUserTrusted(otherUserId: String): Boolean + suspend fun isUserTrusted(otherUserId: String): Boolean /** * Will not force a download of the key, but will verify signatures trust chain. * Checks that my trusted user key has signed the other user UserKey */ - fun checkUserTrust(otherUserId: String): UserTrustResult + suspend fun checkUserTrust(otherUserId: String): UserTrustResult /** * Initialize cross signing for this user. * Users needs to enter credentials */ - fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, - callback: MatrixCallback) + suspend fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) /** * Does our own user have a valid cross signing identity uploaded. * * In other words has any of our devices uploaded public cross signing keys to the server. */ - fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null + suspend fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null /** * Inject the private cross signing keys, likely from backup, into our store. @@ -70,17 +70,17 @@ interface CrossSigningService { * * @param otherUserId The ID of the user for which we would like to fetch the cross signing keys. */ - fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? + suspend fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? - fun getLiveCrossSigningKeys(userId: String): LiveData> + fun getLiveCrossSigningKeys(userId: String): Flow> /** Get our own public cross signing keys */ - fun getMyCrossSigningKeys(): MXCrossSigningInfo? + suspend fun getMyCrossSigningKeys(): MXCrossSigningInfo? /** Get our own private cross signing keys */ - fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + suspend fun getCrossSigningPrivateKeys(): PrivateKeysInfo? - fun getLiveCrossSigningPrivateKeys(): LiveData> + fun getLiveCrossSigningPrivateKeys(): Flow> /** * Can we sign our other devices or other users? @@ -93,11 +93,11 @@ interface CrossSigningService { fun allPrivateKeysKnown(): Boolean /** Mark a user identity as trusted and sign and upload signatures of our user-signing key to the server */ - fun trustUser(otherUserId: String, + suspend fun trustUser(otherUserId: String, callback: MatrixCallback) /** Mark our own master key as trusted */ - fun markMyMasterKeyAsTrusted() + suspend fun markMyMasterKeyAsTrusted() /** * Sign one of your devices and upload the signature @@ -114,7 +114,7 @@ interface CrossSigningService { * using the self-signing key for our own devices or using the user-signing key and the master * key of another user. */ - fun checkDeviceTrust(otherUserId: String, + suspend fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, // TODO what is locallyTrusted used for? locallyTrusted: Boolean?): DeviceTrustResult 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 98f65dc34b..4b085c7b79 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 @@ -25,6 +25,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -426,14 +427,12 @@ internal class DefaultCryptoService @Inject constructor( } } - override fun getLiveCryptoDeviceInfo(userId: String): LiveData> { + override fun getLiveCryptoDeviceInfo(userId: String): Flow> { return getLiveCryptoDeviceInfo(listOf(userId)) } - override fun getLiveCryptoDeviceInfo(userIds: List): LiveData> { - return runBlocking { - this@DefaultCryptoService.olmMachine.getLiveDevices(userIds) // ?: LiveDevice(userIds, deviceObserver) - } + override fun getLiveCryptoDeviceInfo(userIds: List): Flow> { + return olmMachine.getLiveDevices(userIds) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index b3cb03d657..1555f8a04a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -16,9 +16,11 @@ package org.matrix.android.sdk.internal.crypto -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor @@ -64,7 +66,7 @@ import uniffi.olm.setLogger import java.io.File import java.nio.charset.Charset import java.util.UUID -import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import uniffi.olm.OlmMachine as InnerMachine import uniffi.olm.ProgressListener as RustProgressListener import uniffi.olm.UserIdentity as RustUserIdentity @@ -81,98 +83,29 @@ private class CryptoProgressListener(private val listener: ProgressListener?) : } } -internal class LiveDevice( - internal var userIds: List, - private var observer: DeviceUpdateObserver -) : MutableLiveData>() { +private data class UserIdentityCollector(val userId: String, val collector: SendChannel>) : SendChannel> by collector +private data class DevicesCollector(val userIds: List, val collector: SendChannel>) : SendChannel> by collector +private typealias PrivateKeysCollector = SendChannel> - override fun onActive() { - observer.addDeviceUpdateListener(this) - } - - override fun onInactive() { - observer.removeDeviceUpdateListener(this) - } -} - -internal class LiveUserIdentity( - internal var userId: String, - private var observer: UserIdentityUpdateObserver -) : MutableLiveData>() { - override fun onActive() { - observer.addUserIdentityUpdateListener(this) - } - - override fun onInactive() { - observer.removeUserIdentityUpdateListener(this) - } -} - -internal class LivePrivateCrossSigningKeys( - private var observer: PrivateCrossSigningKeysUpdateObserver, -) : MutableLiveData>() { - - override fun onActive() { - observer.addUserIdentityUpdateListener(this) - } - - override fun onInactive() { - observer.removeUserIdentityUpdateListener(this) - } +private class FlowCollectors { + val userIdentityCollectors = CopyOnWriteArrayList() + val privateKeyCollectors = CopyOnWriteArrayList() + val deviceCollectors = CopyOnWriteArrayList() } fun setRustLogger() { setLogger(CryptoLogger() as Logger) } -internal class DeviceUpdateObserver { - internal val listeners = ConcurrentHashMap>() - - fun addDeviceUpdateListener(device: LiveDevice) { - listeners[device] = device.userIds - } - - fun removeDeviceUpdateListener(device: LiveDevice) { - listeners.remove(device) - } -} - -internal class UserIdentityUpdateObserver { - internal val listeners = ConcurrentHashMap() - - fun addUserIdentityUpdateListener(userIdentity: LiveUserIdentity) { - listeners[userIdentity] = userIdentity.userId - } - - fun removeUserIdentityUpdateListener(userIdentity: LiveUserIdentity) { - listeners.remove(userIdentity) - } -} - -internal class PrivateCrossSigningKeysUpdateObserver { - internal val listeners = ConcurrentHashMap() - - fun addUserIdentityUpdateListener(liveKeys: LivePrivateCrossSigningKeys) { - listeners[liveKeys] = Unit - } - - fun removeUserIdentityUpdateListener(liveKeys: LivePrivateCrossSigningKeys) { - listeners.remove(liveKeys) - } -} - internal class OlmMachine( user_id: String, device_id: String, path: File, - deviceObserver: DeviceUpdateObserver, private val requestSender: RequestSender, ) { private val inner: InnerMachine = InnerMachine(user_id, device_id, path.toString()) - private val deviceUpdateObserver = deviceObserver - private val userIdentityUpdateObserver = UserIdentityUpdateObserver() - private val privateKeysUpdateObserver = PrivateCrossSigningKeysUpdateObserver() internal val verificationListeners = ArrayList() + private val flowCollectors = FlowCollectors() /** Get our own user ID. */ fun userId(): String { @@ -193,26 +126,24 @@ internal class OlmMachine( return this.inner } - /** Update all of our live device listeners. */ private suspend fun updateLiveDevices() { - for ((liveDevice, users) in deviceUpdateObserver.listeners) { - val devices = getCryptoDeviceInfo(users) - liveDevice.postValue(devices) + for (deviceCollector in flowCollectors.deviceCollectors) { + val devices = getCryptoDeviceInfo(deviceCollector.userIds) + deviceCollector.send(devices) } } private suspend fun updateLiveUserIdentities() { - for ((liveIdentity, userId) in userIdentityUpdateObserver.listeners) { - val identity = getIdentity(userId)?.toMxCrossSigningInfo().toOptional() - liveIdentity.postValue(identity) + for (userIdentityCollector in flowCollectors.userIdentityCollectors) { + val identity = getIdentity(userIdentityCollector.userId)?.toMxCrossSigningInfo() + userIdentityCollector.send(identity.toOptional()) } } private suspend fun updateLivePrivateKeys() { val keys = this.exportCrossSigningKeys().toOptional() - - for (liveKeys in privateKeysUpdateObserver.listeners.keys()) { - liveKeys.postValue(keys) + for (privateKeyCollector in flowCollectors.privateKeyCollectors) { + privateKeyCollector.send(keys) } } @@ -712,20 +643,27 @@ internal class OlmMachine( return getUserDevicesMap(userIds) } - suspend fun getLiveUserIdentity(userId: String): LiveData> { - val identity = this.getIdentity(userId)?.toMxCrossSigningInfo().toOptional() - val liveIdentity = LiveUserIdentity(userId, this.userIdentityUpdateObserver) - liveIdentity.value = identity - - return liveIdentity + fun getLiveUserIdentity(userId: String): Flow> { + return channelFlow { + val userIdentityCollector = UserIdentityCollector(userId, this) + flowCollectors.userIdentityCollectors.add(userIdentityCollector) + val identity = getIdentity(userId)?.toMxCrossSigningInfo().toOptional() + send(identity) + awaitClose { + flowCollectors.userIdentityCollectors.remove(userIdentityCollector) + } + } } - suspend fun getLivePrivateCrossSigningKeys(): LiveData> { - val keys = this.exportCrossSigningKeys().toOptional() - val liveKeys = LivePrivateCrossSigningKeys(this.privateKeysUpdateObserver) - liveKeys.value = keys - - return liveKeys + fun getLivePrivateCrossSigningKeys(): Flow> { + return channelFlow { + flowCollectors.privateKeyCollectors.add(this) + val keys = this@OlmMachine.exportCrossSigningKeys().toOptional() + send(keys) + awaitClose { + flowCollectors.privateKeyCollectors.remove(this) + } + } } /** @@ -738,12 +676,16 @@ internal class OlmMachine( * * @return The list of Devices or an empty list if there aren't any. */ - suspend fun getLiveDevices(userIds: List): LiveData> { - val plainDevices = getCryptoDeviceInfo(userIds) - val devices = LiveDevice(userIds, deviceUpdateObserver) - devices.value = plainDevices - - return devices + fun getLiveDevices(userIds: List): Flow> { + return channelFlow { + val devicesCollector = DevicesCollector(userIds, this) + flowCollectors.deviceCollectors.add(devicesCollector) + val devices = getCryptoDeviceInfo(userIds) + send(devices) + awaitClose { + flowCollectors.deviceCollectors.remove(devicesCollector) + } + } } /** Discard the currently active room key for the given room if there is one. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachineProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachineProvider.kt index 441fe5acd0..6aa59afc69 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachineProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachineProvider.kt @@ -31,7 +31,5 @@ internal class OlmMachineProvider @Inject constructor( requestSender: RequestSender ) { - private val deviceObserver: DeviceUpdateObserver = DeviceUpdateObserver() - - var olmMachine: OlmMachine = OlmMachine(userId, deviceId!!, dataDir, deviceObserver, requestSender) + var olmMachine: OlmMachine = OlmMachine(userId, deviceId!!, dataDir, requestSender) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt index 07918870d3..a1b0fc35e6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCrossSigningService.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.crypto import androidx.lifecycle.LiveData -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor @@ -45,14 +45,14 @@ internal class RustCrossSigningService @Inject constructor( /** * Is our own device signed by our own cross signing identity */ - override fun isCrossSigningVerified(): Boolean { - return when (val identity = runBlocking { olmMachine.getIdentity(olmMachine.userId()) }) { + override suspend fun isCrossSigningVerified(): Boolean { + return when (val identity = olmMachine.getIdentity(olmMachine.userId()) ) { is OwnUserIdentity -> identity.trustsOurOwnDevice() else -> false } } - override fun isUserTrusted(otherUserId: String): Boolean { + override suspend fun isUserTrusted(otherUserId: String): Boolean { // This seems to be used only in tests. return this.checkUserTrust(otherUserId).isVerified() } @@ -61,14 +61,14 @@ internal class RustCrossSigningService @Inject constructor( * Will not force a download of the key, but will verify signatures trust chain. * Checks that my trusted user key has signed the other user UserKey */ - override fun checkUserTrust(otherUserId: String): UserTrustResult { - val identity = runBlocking { olmMachine.getIdentity(olmMachine.userId()) } + override suspend fun checkUserTrust(otherUserId: String): UserTrustResult { + val identity = olmMachine.getIdentity(olmMachine.userId()) // While UserTrustResult has many different states, they are by the callers // converted to a boolean value immediately, thus we don't need to support // all the values. return if (identity != null) { - val verified = runBlocking { identity.verified() } + val verified = identity.verified() if (verified) { UserTrustResult.Success @@ -84,8 +84,8 @@ internal class RustCrossSigningService @Inject constructor( * Initialize cross signing for this user. * Users needs to enter credentials */ - override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback) { - runBlocking { runCatching { olmMachine.bootstrapCrossSigning(uiaInterceptor) }.foldToCallback(callback) } + override suspend fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?) { + olmMachine.bootstrapCrossSigning(uiaInterceptor) } /** @@ -108,26 +108,26 @@ internal class RustCrossSigningService @Inject constructor( * * @param otherUserId The ID of the user for which we would like to fetch the cross signing keys. */ - override fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { - return runBlocking { olmMachine.getIdentity(otherUserId)?.toMxCrossSigningInfo() } + override suspend fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { + return olmMachine.getIdentity(otherUserId)?.toMxCrossSigningInfo() } - override fun getLiveCrossSigningKeys(userId: String): LiveData> { - return runBlocking { olmMachine.getLiveUserIdentity(userId) } + override fun getLiveCrossSigningKeys(userId: String): Flow> { + return olmMachine.getLiveUserIdentity(userId) } /** Get our own public cross signing keys */ - override fun getMyCrossSigningKeys(): MXCrossSigningInfo? { + override suspend fun getMyCrossSigningKeys(): MXCrossSigningInfo? { return getUserCrossSigningKeys(olmMachine.userId()) } /** Get our own private cross signing keys */ - override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { - return runBlocking { olmMachine.exportCrossSigningKeys() } + override suspend fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { + return olmMachine.exportCrossSigningKeys() } - override fun getLiveCrossSigningPrivateKeys(): LiveData> { - return runBlocking { olmMachine.getLivePrivateCrossSigningKeys() } + override fun getLiveCrossSigningPrivateKeys(): Flow> { + return olmMachine.getLivePrivateCrossSigningKeys() } /** @@ -148,12 +148,12 @@ internal class RustCrossSigningService @Inject constructor( } /** Mark a user identity as trusted and sign and upload signatures of our user-signing key to the server */ - override fun trustUser(otherUserId: String, callback: MatrixCallback) { + override suspend fun trustUser(otherUserId: String, callback: MatrixCallback) { // This is only used in a test - val userIdentity = runBlocking { olmMachine.getIdentity(otherUserId) } + val userIdentity = olmMachine.getIdentity(otherUserId) if (userIdentity != null) { - runBlocking { userIdentity.verify() } + userIdentity.verify() callback.onSuccess(Unit) } else { callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) @@ -161,7 +161,7 @@ internal class RustCrossSigningService @Inject constructor( } /** Mark our own master key as trusted */ - override fun markMyMasterKeyAsTrusted() { + override suspend fun markMyMasterKeyAsTrusted() { // This doesn't seem to be used? this.trustUser(this.olmMachine.userId(), NoOpMatrixCallback()) } @@ -171,10 +171,8 @@ internal class RustCrossSigningService @Inject constructor( */ override suspend fun trustDevice(deviceId: String) { val device = olmMachine.getDevice(olmMachine.userId(), deviceId) - - return if (device != null) { + if (device != null) { val verified = device.verify() - if (verified) { return } else { @@ -192,15 +190,15 @@ internal class RustCrossSigningService @Inject constructor( * using the self-signing key for our own devices or using the user-signing key and the master * key of another user. */ - override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { - val device = runBlocking { olmMachine.getDevice(otherUserId, otherDeviceId) } + override suspend fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { + val device = olmMachine.getDevice(otherUserId, otherDeviceId) return if (device != null) { // TODO i don't quite understand the semantics here and there are no docs for // DeviceTrustResult, what do the different result types mean exactly, // do you return success only if the Device is cross signing verified? // what about the local trust if it isn't? why is the local trust passed as an argument here? - DeviceTrustResult.Success(runBlocking { device.trustLevel() }) + DeviceTrustResult.Success(device.trustLevel()) } else { DeviceTrustResult.UnknownDevice(otherDeviceId) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index de627e1f78..23cd54ed99 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -34,7 +34,6 @@ import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey -import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -91,12 +90,7 @@ class BootstrapCrossSigningTask @Inject constructor( ) try { - awaitCallback { - crossSigningService.initializeCrossSigning( - params.userInteractiveAuthInterceptor, - it - ) - } + crossSigningService.initializeCrossSigning(params.userInteractiveAuthInterceptor) if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) { return BootstrapResult.SuccessCrossSigningOnly } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index ca8aeaba85..9d93e2261f 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -220,30 +219,27 @@ class HomeActivityViewModel @AssistedInject constructor( // Try to initialize cross signing in background if possible Timber.d("Initialize cross signing...") try { - awaitCallback { - session.cryptoService().crossSigningService().initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - // We missed server grace period or it's not setup, see if we remember locally password - if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && - errCode == null && - reAuthHelper.data != null) { - promise.resume( - UserPasswordAuth( - session = flowResponse.session, - user = session.myUserId, - password = reAuthHelper.data - ) - ) - } else { - promise.resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing")) - } + session.cryptoService().crossSigningService().initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + // We missed server grace period or it's not setup, see if we remember locally password + if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && + errCode == null && + reAuthHelper.data != null) { + promise.resume( + UserPasswordAuth( + session = flowResponse.session, + user = session.myUserId, + password = reAuthHelper.data + ) + ) + } else { + promise.resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing")) } - }, - callback = it - ) - Timber.d("Initialize cross signing SUCCESS") - } + } + }, + ) + Timber.d("Initialize cross signing SUCCESS") } catch (failure: Throwable) { Timber.e(failure, "Failed to initialize cross signing") } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 59b39d17ef..bd0ba075a5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -27,6 +27,7 @@ import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.crypto.VerificationState import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session @@ -139,52 +140,54 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses } private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration { - return if ( - event.root.sendState == SendState.SYNCED && - roomSummary?.isEncrypted.orFalse() && - // is user verified - session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) { - val ts = roomSummary?.encryptionEventTs ?: 0 - val eventTs = event.root.originServerTs ?: 0 - if (event.isEncrypted()) { - // Do not decorate failed to decrypt, or redaction (we lost sender device info) - if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { - E2EDecoration.NONE - } else { - val sendingDevice = event.root.content - .toModel() - ?.deviceId - ?.let { deviceId -> - session.cryptoService().getDeviceInfo(event.root.senderId ?: "", deviceId) - } - when { - sendingDevice == null -> { - // For now do not decorate this with warning - // maybe it's a deleted session - E2EDecoration.NONE - } - sendingDevice.trustLevel == null -> { - E2EDecoration.WARN_SENT_BY_UNKNOWN - } - sendingDevice.trustLevel?.isVerified().orFalse() -> { - E2EDecoration.NONE - } - else -> { - E2EDecoration.WARN_SENT_BY_UNVERIFIED - } - } - } + if (event.root.sendState != SendState.SYNCED) + return E2EDecoration.NONE + if (!roomSummary?.isEncrypted.orFalse()) + return E2EDecoration.NONE + val isUserVerified = runBlocking { + session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted().orFalse() + } + if (!isUserVerified) { + return E2EDecoration.NONE + } + val ts = roomSummary?.encryptionEventTs ?: 0 + val eventTs = event.root.originServerTs ?: 0 + return if (event.isEncrypted()) { + // Do not decorate failed to decrypt, or redaction (we lost sender device info) + if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { + E2EDecoration.NONE } else { - if (event.root.isStateEvent()) { - // Do not warn for state event, they are always in clear - E2EDecoration.NONE - } else { - // If event is in clear after the room enabled encryption we should warn - if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE + val sendingDevice = event.root.content + .toModel() + ?.deviceId + ?.let { deviceId -> + session.cryptoService().getDeviceInfo(event.root.senderId ?: "", deviceId) + } + when { + sendingDevice == null -> { + // For now do not decorate this with warning + // maybe it's a deleted session + E2EDecoration.NONE + } + sendingDevice.trustLevel == null -> { + E2EDecoration.WARN_SENT_BY_UNKNOWN + } + sendingDevice.trustLevel?.isVerified().orFalse() -> { + E2EDecoration.NONE + } + else -> { + E2EDecoration.WARN_SENT_BY_UNVERIFIED + } } } } else { - E2EDecoration.NONE + if (event.root.isStateEvent()) { + // Do not warn for state event, they are always in clear + E2EDecoration.NONE + } else { + // If event is in clear after the room enabled encryption we should warn + if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE + } } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 2e5ac0cb01..2c1e373075 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -411,13 +411,15 @@ class DefaultNavigator @Inject constructor( override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) { // if cross signing is enabled and trusted or not set up at all we should propose full 4S sessionHolder.getSafeActiveSession()?.let { session -> - if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null || - session.cryptoService().crossSigningService().canCrossSign()) { - (context as? AppCompatActivity)?.let { - BootstrapBottomSheet.show(it.supportFragmentManager, SetupMode.NORMAL) + coroutineScope.launch { + if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null || + session.cryptoService().crossSigningService().canCrossSign()) { + (context as? AppCompatActivity)?.let { + BootstrapBottomSheet.show(it.supportFragmentManager, SetupMode.NORMAL) + } + } else { + context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) } - } else { - context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 0bbdd87f3e..af57f3de99 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -95,7 +95,6 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState room.flow().liveRoomMembers(roomMemberQueryParams) .flatMapLatest { membersSummary -> session.cryptoService().getLiveCryptoDeviceInfo(membersSummary.map { it.userId }) - .asFlow() .catch { Timber.e(it) } .map { deviceList -> // If any key change, emit the userIds list diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index ef87d908ea..95a7213b35 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -317,31 +317,32 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( // Todo this should be refactored and use same state as 4S section private fun refreshXSigningStatus() { - val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys() - val xSigningIsEnableInAccount = crossSigningKeys != null - val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() - val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() + lifecycleScope.launchWhenResumed { + val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys() + val xSigningIsEnableInAccount = crossSigningKeys != null + val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() + val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() - when { - xSigningKeyCanSign -> { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) - } - xSigningKeysAreTrusted -> { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) - } - xSigningIsEnableInAccount -> { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted) - } - else -> { - mCrossSigningStatePreference.setIcon(android.R.color.transparent) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled) + when { + xSigningKeyCanSign -> { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) + } + xSigningKeysAreTrusted -> { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) + } + xSigningIsEnableInAccount -> { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted) + } + else -> { + mCrossSigningStatePreference.setIcon(android.R.color.transparent) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled) + } } + mCrossSigningStatePreference.isVisible = true } - - mCrossSigningStatePreference.isVisible = true } private val saveMegolmStartForActivityResult = registerStartForActivityResult { diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 644b7f33dd..744a6eedd4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -29,6 +29,7 @@ import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor @@ -41,7 +42,6 @@ import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth -import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -55,25 +55,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( ) : VectorViewModel(initialState) { init { - combine( - session.flow().liveMyDevicesInfo(), - session.flow().liveCrossSigningInfo(session.myUserId) - ) { myDevicesInfo, mxCrossSigningInfo -> - myDevicesInfo to mxCrossSigningInfo - } - .execute { data -> - val crossSigningKeys = data.invoke()?.second?.getOrNull() - val xSigningIsEnableInAccount = crossSigningKeys != null - val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() - val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() - - copy( - crossSigningInfo = crossSigningKeys, - xSigningIsEnableInAccount = xSigningIsEnableInAccount, - xSigningKeysAreTrusted = xSigningKeysAreTrusted, - xSigningKeyCanSign = xSigningKeyCanSign - ) - } + observeCrossSigning() } var uiaContinuation: Continuation? = null @@ -90,29 +72,27 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) viewModelScope.launch(Dispatchers.IO) { try { - awaitCallback { - session.cryptoService().crossSigningService().initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, - errCode: String?, - promise: Continuation) { - Timber.d("## UIA : initializeCrossSigning UIA") - if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && - reAuthHelper.data != null && errCode == null) { - UserPasswordAuth( - session = null, - user = session.myUserId, - password = reAuthHelper.data - ).let { promise.resume(it) } - } else { - Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity") - _viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flowResponse, errCode)) - pendingAuth = DefaultBaseAuth(session = flowResponse.session) - uiaContinuation = promise - } + session.cryptoService().crossSigningService().initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, + errCode: String?, + promise: Continuation) { + Timber.d("## UIA : initializeCrossSigning UIA") + if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && + reAuthHelper.data != null && errCode == null) { + UserPasswordAuth( + session = null, + user = session.myUserId, + password = reAuthHelper.data + ).let { promise.resume(it) } + } else { + Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity") + _viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flowResponse, errCode)) + pendingAuth = DefaultBaseAuth(session = flowResponse.session) + uiaContinuation = promise } - }, it) - } + } + }) } catch (failure: Throwable) { handleInitializeXSigningError(failure) } finally { @@ -149,6 +129,28 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( }.exhaustive } + private fun observeCrossSigning() { + combine( + session.flow().liveMyDevicesInfo(), + session.flow().liveCrossSigningInfo(session.myUserId) + ) { myDevicesInfo, mxCrossSigningInfo -> + myDevicesInfo to mxCrossSigningInfo + }.onEach { data -> + val crossSigningKeys = data.second.getOrNull() + val xSigningIsEnableInAccount = crossSigningKeys != null + val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() + val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() + setState { + copy( + crossSigningInfo = crossSigningKeys, + xSigningIsEnableInAccount = xSigningIsEnableInAccount, + xSigningKeysAreTrusted = xSigningKeysAreTrusted, + xSigningKeyCanSign = xSigningKeyCanSign + ) + } + } + } + private fun handleInitializeXSigningError(failure: Throwable) { Timber.e(failure, "## CrossSigning - Failed to initialize cross signing") _viewEvents.post(CrossSigningSettingsViewEvents.Failure(Exception(stringProvider.getString(R.string.failed_to_initialize_cross_signing)))) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt index 3a944b5a71..937445e815 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt @@ -15,7 +15,6 @@ */ package im.vector.app.features.settings.devices -import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -26,6 +25,7 @@ import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo @@ -43,14 +43,7 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As by hiltMavericksViewModelFactory() init { - - setState { - copy( - hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized(), - accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(), - isRecoverySetup = session.sharedSecretStorageService.isRecoverySetup() - ) - } + initState() session.flow().liveCrossSigningInfo(session.myUserId) .execute { copy( @@ -78,10 +71,6 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As ) } - setState { - copy(deviceInfo = Loading()) - } - session.flow().liveMyDevicesInfo() .map { devices -> devices.firstOrNull { it.deviceId == initialState.deviceId } ?: DeviceInfo(deviceId = initialState.deviceId) @@ -91,6 +80,21 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As } } + private fun initState() { + viewModelScope.launch { + val hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized() + val accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified() + val isRecoverySetup = session.sharedSecretStorageService.isRecoverySetup() + setState { + copy( + hasAccountCrossSigning = hasAccountCrossSigning, + accountCrossSigningIsTrusted = accountCrossSigningIsTrusted, + isRecoverySetup = isRecoverySetup + ) + } + } + } + override fun handle(action: EmptyAction) { } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index f12060421b..10cda3ad44 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -106,15 +106,7 @@ class DevicesViewModel @AssistedInject constructor( private val refreshSource = PublishDataSource() init { - - setState { - copy( - hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized(), - accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(), - myDeviceId = session.sessionParams.deviceId ?: "" - ) - } - + initState() combine( session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo() @@ -176,6 +168,21 @@ class DevicesViewModel @AssistedInject constructor( queryRefreshDevicesList() } + private fun initState() { + viewModelScope.launch { + val hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized() + val accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified() + val myDeviceId = session.sessionParams.deviceId ?: "" + setState { + copy( + hasAccountCrossSigning = hasAccountCrossSigning, + accountCrossSigningIsTrusted = accountCrossSigningIsTrusted, + myDeviceId = myDeviceId + ) + } + } + } + override fun onCleared() { session.cryptoService().verificationService().removeListener(this) super.onCleared()