diff --git a/CHANGES.md b/CHANGES.md index 6d756f2b3f..39ea3f1477 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,23 @@ +Changes in Element v1.6.2 (2023-06-02) +====================================== + +Features ✨ +---------- + - **Element Android is now using the Crypto Rust SDK**. Migration of user's data should be done at first launch after application upgrade. ([#8390](https://github.com/vector-im/element-android/issues/8390)) + - Marks WebP files as Animated and allows them to play ([#8120](https://github.com/vector-im/element-android/issues/8120)) + - Updates to protocol used for Sign in with QR code ([#8299](https://github.com/vector-im/element-android/issues/8299)) + - Updated rust crypto SDK to version 0.3.9 ([#8488](https://github.com/vector-im/element-android/issues/8488)) + +Bugfixes 🐛 +---------- + - Fix: Allow users to sign out even if the sign out request fails. ([#4855](https://github.com/vector-im/element-android/issues/4855)) + - fix: Make some crypto calls suspendable to avoid reported ANR ([#8482](https://github.com/vector-im/element-android/issues/8482)) + +Other changes +------------- + - Refactoring: Extract a new interface for common access to crypto store between kotlin and rust crypto ([#8470](https://github.com/vector-im/element-android/issues/8470)) + + Changes in Element v1.6.1 (2023-05-25) ====================================== diff --git a/dependencies.gradle b/dependencies.gradle index ca5495b04d..f4d32c4894 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -47,7 +47,7 @@ ext.libs = [ 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" ], androidx : [ - 'activity' : "androidx.activity:activity-ktx:1.7.1", + 'activity' : "androidx.activity:activity-ktx:1.7.2", 'appCompat' : "androidx.appcompat:appcompat:1.6.1", 'biometric' : "androidx.biometric:biometric:1.1.0", 'core' : "androidx.core:core-ktx:1.10.1", diff --git a/fastlane/metadata/android/en-US/changelogs/40106020.txt b/fastlane/metadata/android/en-US/changelogs/40106020.txt new file mode 100644 index 0000000000..badf979955 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40106020.txt @@ -0,0 +1,2 @@ +Main changes in this version: Element Android is now using the Crypto Rust SDK. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 8ad9ad6eb4..ee7d2fea12 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -328,6 +328,9 @@ Back up You’ll lose access to your encrypted messages unless you back up your keys before signing out. + Cannot reach the homeserver. If you sign out anyway, this device will not be erased from your device list, you may want to remove it using another client. + Sign out anyway + Loading… diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 230677e8e2..420b24ab5a 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -63,7 +63,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.6.1\"" + buildConfigField "String", "SDK_VERSION", "\"1.6.2\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" @@ -216,8 +216,8 @@ dependencies { implementation libs.google.phonenumber - rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.7") - // rustCryptoApi project(":library:rustCrypto") + rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.9") +// rustCryptoApi project(":library:rustCrypto") testImplementation libs.tests.junit // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt similarity index 100% rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt rename to matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt b/matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt similarity index 100% rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt rename to matrix-sdk-android/src/androidTestKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoStoreTest.kt diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt index 6ae12aaf3d..719c45a113 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt @@ -59,6 +59,7 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionD import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration @@ -254,6 +255,9 @@ internal abstract class CryptoModule { @Binds abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore + @Binds + abstract fun bindCommonCryptoStore(store: RealmCryptoStore): IMXCommonCryptoStore + @Binds abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 1dd02543cb..b25c04aa9b 100755 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -256,7 +256,7 @@ internal class DefaultCryptoService @Inject constructor( return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version } - override fun getMyCryptoDevice(): CryptoDeviceInfo { + override suspend fun getMyCryptoDevice(): CryptoDeviceInfo { return myDeviceInfoHolder.get().myDevice } @@ -506,10 +506,7 @@ internal class DefaultCryptoService @Inject constructor( null } else { withContext(coroutineDispatchers.io) { - cryptoStore.deviceWithIdentityKey(senderKey).takeIf { - // check that the claimed user id matches - it?.userId == userId - } + cryptoStore.deviceWithIdentityKey(userId, senderKey) } } } @@ -539,7 +536,7 @@ internal class DefaultCryptoService @Inject constructor( // .executeBy(taskExecutor) // } - override fun getCryptoDeviceInfo(userId: String): List { + override suspend fun getCryptoDeviceInfo(userId: String): List { return cryptoStore.getUserDeviceList(userId).orEmpty() } // diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt rename to matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt rename to matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt similarity index 81% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt rename to matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index a13c15bd1a..fc882e5c1d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -28,20 +28,16 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.CryptoRoomInfo -import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo 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.TrailType import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper -import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.olm.OlmAccount import org.matrix.olm.OlmOutboundGroupSession @@ -49,7 +45,7 @@ import org.matrix.olm.OlmOutboundGroupSession /** * The crypto data store. */ -internal interface IMXCryptoStore { +internal interface IMXCryptoStore : IMXCommonCryptoStore { /** * @return the device id @@ -78,21 +74,6 @@ internal interface IMXCryptoStore { */ fun getInboundGroupSessions(roomId: String): List - /** - * @return true to unilaterally blacklist all unverified devices. - */ - fun getGlobalBlacklistUnverifiedDevices(): Boolean - - /** - * Set the global override for whether the client should ever send encrypted - * messages to unverified devices. - * If false, it can still be overridden per-room. - * If true, it overrides the per-room settings. - * - * @param block true to unilaterally blacklist all - */ - fun setGlobalBlacklistUnverifiedDevices(block: Boolean) - /** * Enable or disable key gossiping. * Default is true. @@ -123,28 +104,6 @@ internal interface IMXCryptoStore { */ fun getRoomsListBlacklistUnverifiedDevices(): List - /** - * A live status regarding sharing keys for unverified devices in this room. - * - * @return Live status - */ - fun getLiveBlockUnverifiedDevices(roomId: String): LiveData - - /** - * Tell if unverified devices should be blacklisted when sending keys. - * - * @return true if should not send keys to unverified devices - */ - fun getBlockUnverifiedDevices(roomId: String): Boolean - - /** - * Define if encryption keys should be sent to unverified devices in this room. - * - * @param roomId the roomId - * @param block if true will not send keys to unverified devices - */ - fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) - /** * Get the current keys backup version. */ @@ -186,16 +145,6 @@ internal interface IMXCryptoStore { */ fun deleteStore() - /** - * open any existing crypto store. - */ - fun open() - - /** - * Close the store. - */ - fun close() - /** * Store the device id. * @@ -262,14 +211,6 @@ internal interface IMXCryptoStore { fun getLiveDeviceWithId(deviceId: String): LiveData> - fun getMyDevicesInfo(): List - - fun getLiveMyDevicesInfo(): LiveData> - - fun getLiveMyDevicesInfo(deviceId: String): LiveData> - - fun saveMyDevicesInfo(info: List) - /** * Store the crypto algorithm for a room. * @@ -278,47 +219,8 @@ internal interface IMXCryptoStore { */ fun storeRoomAlgorithm(roomId: String, algorithm: String?) - /** - * Provides the algorithm used in a dedicated room. - * - * @param roomId the room id - * @return the algorithm, null is the room is not encrypted - */ - fun getRoomAlgorithm(roomId: String): String? - - fun getRoomCryptoInfo(roomId: String): CryptoRoomInfo? - fun setAlgorithmInfo(roomId: String, encryption: EncryptionEventContent?) - - /** - * This is a bit different than isRoomEncrypted. - * A room is encrypted when there is a m.room.encryption state event in the room (malformed/invalid or not). - * But the crypto layer has additional guaranty to ensure that encryption would never been reverted. - * It's defensive coding out of precaution (if ever state is reset). - */ - fun roomWasOnceEncrypted(roomId: String): Boolean - - fun shouldEncryptForInvitedMembers(roomId: String): Boolean - - /** - * Sets a boolean flag that will determine whether or not this device should encrypt Events for - * invited members. - * - * @param roomId the room id - * @param shouldEncryptForInvitedMembers The boolean flag - */ - fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) - fun shouldShareHistory(roomId: String): Boolean - /** - * Sets a boolean flag that will determine whether or not room history (existing inbound sessions) - * will be shared to new user invites. - * - * @param roomId the room id - * @param shouldShareHistory The boolean flag - */ - fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) - /** * Store a session between the logged-in user and another device. * @@ -361,15 +263,6 @@ internal interface IMXCryptoStore { */ fun storeInboundGroupSessions(sessions: List) - /** - * Retrieve an inbound group session. - * - * @param sessionId the session identifier. - * @param senderKey the base64-encoded curve25519 key of the sender. - * @return an inbound group session. - */ - fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? - /** * Retrieve an inbound group session, filtering shared history. * @@ -552,7 +445,6 @@ internal interface IMXCryptoStore { // fun getCrossSigningPrivateKeysFlow(): Flow> fun getGlobalCryptoConfig(): GlobalCryptoConfig - fun getLiveGlobalCryptoConfig(): LiveData fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? @@ -597,14 +489,8 @@ internal interface IMXCryptoStore { fun setDeviceKeysUploaded(uploaded: Boolean) fun areDeviceKeysUploaded(): Boolean - fun tidyUpDataBase() fun getOutgoingRoomKeyRequests(inStates: Set): List - /** - * Store a bunch of data collected during a sync response treatment. @See [CryptoStoreAggregator]. - */ - fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) - /** * Store a bunch of data related to the users. @See [UserDataToStore]. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt similarity index 99% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt rename to matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 2d82f67b9e..e3595f6618 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -280,6 +280,19 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun deviceWithIdentityKey(userId: String, identityKey: String): CryptoDeviceInfo? { + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .equalTo(DeviceInfoEntityFields.USER_ID, userId) + .contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey) + .findAll() + .mapNotNull { CryptoMapper.mapToModel(it) } + .firstOrNull { + it.identityKey() == identityKey + } + } + } + override fun storeUserDevices(userId: String, devices: Map?) { doRealmTransaction("storeUserDevices", realmConfiguration) { realm -> storeUserDevices(realm, userId, devices) 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 043d0a29f5..31d11f6730 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 @@ -73,7 +73,7 @@ interface CryptoService { suspend fun getUserDevices(userId: String): List - fun getMyCryptoDevice(): CryptoDeviceInfo + suspend fun getMyCryptoDevice(): CryptoDeviceInfo fun getGlobalBlacklistUnverifiedDevices(): Boolean @@ -130,7 +130,7 @@ interface CryptoService { suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? - fun getCryptoDeviceInfo(userId: String): List + suspend fun getCryptoDeviceInfo(userId: String): List // fun getCryptoDeviceInfoFlow(userId: String): Flow> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt index d64b2e6e92..fade51600a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt @@ -37,6 +37,7 @@ interface SignOutService { /** * Sign out, and release the session, clear all the session data, including crypto data. * @param signOutFromHomeserver true if the sign out request has to be done + * @param ignoreServerRequestError true to ignore server error if any */ - suspend fun signOut(signOutFromHomeserver: Boolean) + suspend fun signOut(signOutFromHomeserver: Boolean, ignoreServerRequestError: Boolean = false) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt index 5ec0dedadf..af8ab71a87 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt @@ -30,6 +30,7 @@ object MimeTypes { const val BadJpg = "image/jpg" const val Jpeg = "image/jpeg" const val Gif = "image/gif" + const val Webp = "image/webp" const val Ogg = "audio/ogg" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index d1dd0238ba..bbef75a21d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -301,6 +301,9 @@ internal class DefaultAuthenticationService @Inject constructor( val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibilty == true } val flows = if (oidcCompatibilityFlow != null) listOf(oidcCompatibilityFlow) else loginFlowResponse.flows + val supportsGetLoginTokenFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.token" && it.getLoginToken == true } != null + + @Suppress("DEPRECATION") return LoginFlowResult( supportedLoginTypes = flows.orEmpty().mapNotNull { it.type }, ssoIdentityProviders = flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, @@ -309,7 +312,7 @@ internal class DefaultAuthenticationService @Inject constructor( isOutdatedHomeserver = !versions.isSupportedBySdk(), hasOidcCompatibilityFlow = oidcCompatibilityFlow != null, isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(), - isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(), + isLoginWithQrSupported = supportsGetLoginTokenFlow || versions.doesServerSupportQrCodeLogin(), ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index 971407388c..ea749a56b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -51,5 +51,13 @@ internal data class LoginFlow( * See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824) */ @Json(name = "org.matrix.msc3824.delegated_oidc_compatibility") - val delegatedOidcCompatibilty: Boolean? = null + val delegatedOidcCompatibilty: Boolean? = null, + + /** + * Whether a login flow of type m.login.token could accept a token issued using /login/get_token. + * + * See https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv1loginget_token + */ + @Json(name = "get_login_token") + val getLoginToken: Boolean? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 4d8e90cf35..3fe5541b68 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -54,6 +54,7 @@ private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token" private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind" private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440" private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" +@Deprecated("The availability of stable get_login_token is now exposed as a capability and part of login flow") private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" @@ -94,7 +95,9 @@ internal fun Versions.doesServerSupportThreadUnreadNotifications(): Boolean { return getMaxVersion() >= HomeServerVersion.v1_4_0 || (msc3771 && msc3773) } +@Deprecated("The availability of stable get_login_token is now exposed as a capability and part of login flow") internal fun Versions.doesServerSupportQrCodeLogin(): Boolean { + @Suppress("DEPRECATION") return unstableFeatures?.get(FEATURE_QR_CODE_LOGIN) ?: false } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ShouldEncryptForInvitedMembersUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ShouldEncryptForInvitedMembersUseCase.kt index 29a550aad3..7c7c8ce901 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ShouldEncryptForInvitedMembersUseCase.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/ShouldEncryptForInvitedMembersUseCase.kt @@ -17,11 +17,11 @@ package org.matrix.android.sdk.internal.crypto import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import javax.inject.Inject internal class ShouldEncryptForInvitedMembersUseCase @Inject constructor(private val cryptoConfig: MXCryptoConfig, - private val cryptoStore: IMXCryptoStore) { + private val cryptoStore: IMXCommonCryptoStore) { operator fun invoke(roomId: String): Boolean { return cryptoConfig.enableEncryptionForInvitedMembers && cryptoStore.shouldEncryptForInvitedMembers(roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt new file mode 100644 index 0000000000..68b002c087 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCommonCryptoStore.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2023 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.store + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.CryptoRoomInfo +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator + +/** + * As a temporary measure rust and kotlin flavor are still using realm to store some crypto + * related information. In the near future rust flavor will complitly stop using realm, as soon + * as the missing bits are store in rust side (like room encryption settings, ..) + * This interface defines what's now used by both flavors. + * The actual implementation are moved in each flavors + */ +interface IMXCommonCryptoStore { + + /** + * Provides the algorithm used in a dedicated room. + * + * @param roomId the room id + * @return the algorithm, null is the room is not encrypted + */ + fun getRoomAlgorithm(roomId: String): String? + + fun getRoomCryptoInfo(roomId: String): CryptoRoomInfo? + + fun setAlgorithmInfo(roomId: String, encryption: EncryptionEventContent?) + + fun roomWasOnceEncrypted(roomId: String): Boolean + + fun saveMyDevicesInfo(info: List) + + // questionable that it's stored in crypto store + fun getMyDevicesInfo(): List + + // questionable that it's stored in crypto store + fun getLiveMyDevicesInfo(): LiveData> + + // questionable that it's stored in crypto store + fun getLiveMyDevicesInfo(deviceId: String): LiveData> + + /** + * open any existing crypto store. + */ + fun open() + fun tidyUpDataBase() + + /** + * Close the store. + */ + fun close() + + /* + * Store a bunch of data collected during a sync response treatment. @See [CryptoStoreAggregator]. + */ + fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) + + fun shouldEncryptForInvitedMembers(roomId: String): Boolean + + /** + * Sets a boolean flag that will determine whether or not room history (existing inbound sessions) + * will be shared to new user invites. + * + * @param roomId the room id + * @param shouldShareHistory The boolean flag + */ + fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) + + /** + * Sets a boolean flag that will determine whether or not this device should encrypt Events for + * invited members. + * + * @param roomId the room id + * @param shouldEncryptForInvitedMembers The boolean flag + */ + fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) + + /** + * Define if encryption keys should be sent to unverified devices in this room. + * + * @param roomId the roomId + * @param block if true will not send keys to unverified devices + */ + fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. + * If false, it can still be overridden per-room. + * If true, it overrides the per-room settings. + * + * @param block true to unilaterally blacklist all + */ + fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + + fun getLiveGlobalCryptoConfig(): LiveData + + /** + * @return true to unilaterally blacklist all unverified devices. + */ + fun getGlobalBlacklistUnverifiedDevices(): Boolean + + /** + * A live status regarding sharing keys for unverified devices in this room. + * + * @return Live status + */ + fun getLiveBlockUnverifiedDevices(roomId: String): LiveData + + /** + * Tell if unverified devices should be blacklisted when sending keys. + * + * @return true if should not send keys to unverified devices + */ + fun getBlockUnverifiedDevices(roomId: String): Boolean + + /** + * Retrieve a device by its identity key. + * + * @param userId the device owner + * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) + * @return the device or null if not found + */ + fun deviceWithIdentityKey(userId: String, identityKey: String): CryptoDeviceInfo? + + /** + * Retrieve an inbound group session. + * Used in rust for lazy migration + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return an inbound group session. + */ + fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt index 95ff44807c..7c60eab08f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt @@ -71,7 +71,14 @@ internal data class Capabilities( * True if the user can use m.thread relation, false otherwise. */ @Json(name = "m.thread") - val threads: BooleanCapability? = null + val threads: BooleanCapability? = null, + + /** + * Capability to indicate if the server supports login token issuance for signing in another device. + * True if the user can use /login/get_token, false otherwise. + */ + @Json(name = "m.get_login_token") + val getLoginToken: BooleanCapability? = null ) @JsonClass(generateAdapter = true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index ec12695ecd..a368325793 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -151,8 +151,6 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( getVersionResult.doesServerSupportThreads() homeServerCapabilitiesEntity.canUseThreadReadReceiptsAndNotifications = getVersionResult.doesServerSupportThreadUnreadNotifications() - homeServerCapabilitiesEntity.canLoginWithQrCode = - getVersionResult.doesServerSupportQrCodeLogin() homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices = getVersionResult.doesServerSupportRemoteToggleOfPushNotifications() homeServerCapabilitiesEntity.canRedactEventWithRelations = @@ -169,10 +167,25 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl } + + homeServerCapabilitiesEntity.canLoginWithQrCode = canLoginWithQrCode(getCapabilitiesResult, getVersionResult) + homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time } } + private fun canLoginWithQrCode(getCapabilitiesResult: GetCapabilitiesResult?, getVersionResult: Versions?): Boolean { + // in r0 of MSC3882 an unstable feature was exposed. In stable it is done via /capabilities and /login + + // in stable 1.7 a capability is exposed for the authenticated user + if (getCapabilitiesResult?.capabilities?.getLoginToken != null) { + return getCapabilitiesResult.capabilities.getLoginToken.enabled == true + } + + @Suppress("DEPRECATION") + return getVersionResult?.doesServerSupportQrCodeLogin() == true + } + companion object { // 8 hours like on Element Web private const val MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS = 8 * 60 * 60 * 1000 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt index 5a66e7e62d..fbf1dc532c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt @@ -26,11 +26,10 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import timber.log.Timber +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import javax.inject.Inject -internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) { +internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCommonCryptoStore) { sealed class EditValidity { object Valid : EditValidity() @@ -53,7 +52,6 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto * If the original event was encrypted, the replacement should be too. */ fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity { - Timber.v("###REPLACE valide event $originalEvent replaced $replaceEvent") // we might not know the original event at that time. In this case we can't perform the validation // Edits should be revalidated when the original event is received if (originalEvent == null) { @@ -80,25 +78,21 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto val replaceDecrypted = replaceEvent.toValidDecryptedEvent() ?: return EditValidity.Unknown // UTD can't decide - val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId - val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId + if (originalEvent.senderId != replaceEvent.senderId) { + return EditValidity.Invalid("original event and replacement event must have the same sender") + } + + val originalSendingDevice = originalEvent.senderId?.let { cryptoStore.deviceWithIdentityKey(it, originalDecrypted.cryptoSenderKey) } + val editSendingDevice = originalEvent.senderId?.let { cryptoStore.deviceWithIdentityKey(it, replaceDecrypted.cryptoSenderKey) } if (originalDecrypted.getRelationContent()?.type == RelationType.REPLACE) { return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ") } - if (originalCryptoSenderId == null || editCryptoSenderId == null) { + if (originalSendingDevice == null || editSendingDevice == null) { // mm what can we do? we don't know if it's cryptographically from same user? - // let valid and UI should display send by deleted device warning? - val bestEffortOriginal = originalCryptoSenderId ?: originalEvent.senderId - val bestEffortEdit = editCryptoSenderId ?: replaceEvent.senderId - if (bestEffortOriginal != bestEffortEdit) { - return EditValidity.Invalid("original event and replacement event must have the same sender") - } - } else { - if (originalCryptoSenderId != editCryptoSenderId) { - return EditValidity.Invalid("Crypto: original event and replacement event must have the same sender") - } + // maybe it's a deleted device or a not yet downloaded one? + return EditValidity.Unknown } if (originalDecrypted.type != replaceDecrypted.type) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index 36ec5e8dac..73b7ae05fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -22,6 +22,8 @@ import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.util.Optional @@ -43,7 +45,8 @@ internal class DefaultReadService @AssistedInject constructor( private val setReadMarkersTask: SetReadMarkersTask, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, @UserId private val userId: String, - private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource + private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, + private val matrixCoroutineDispatchers: MatrixCoroutineDispatchers, ) : ReadService { @AssistedFactory @@ -66,7 +69,7 @@ internal class DefaultReadService @AssistedInject constructor( setReadMarkersTask.execute(taskParams) } - override suspend fun setReadReceipt(eventId: String, threadId: String) { + override suspend fun setReadReceipt(eventId: String, threadId: String) = withContext(matrixCoroutineDispatchers.io) { val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) { threadId } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index d29e7d8f36..7176e36e0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -49,7 +49,7 @@ import org.matrix.android.sdk.api.util.CancelableBag import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.TextContent -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.content.UploadContentWorker @@ -69,7 +69,7 @@ internal class DefaultSendService @AssistedInject constructor( private val workManagerProvider: WorkManagerProvider, @SessionId private val sessionId: String, private val localEchoEventFactory: LocalEchoEventFactory, - private val cryptoStore: IMXCryptoStore, + private val cryptoStore: IMXCommonCryptoStore, private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository, private val eventSenderProcessor: EventSenderProcessor, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt index 929fe7b9a6..9ce29c3c08 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt @@ -28,7 +28,7 @@ import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.task.CoroutineSequencer import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer @@ -54,7 +54,7 @@ private const val MAX_RETRY_COUNT = 3 */ @SessionScope internal class EventSenderProcessorCoroutine @Inject constructor( - private val cryptoStore: IMXCryptoStore, + private val cryptoStore: IMXCommonCryptoStore, private val sessionParams: SessionParams, private val queuedTaskFactory: QueuedTaskFactory, private val taskExecutor: TaskExecutor, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt index 1bb86ecb4b..2c34f1e2d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt @@ -35,7 +35,12 @@ internal class DefaultSignOutService @Inject constructor( sessionParamsStore.updateCredentials(credentials) } - override suspend fun signOut(signOutFromHomeserver: Boolean) { - return signOutTask.execute(SignOutTask.Params(signOutFromHomeserver)) + override suspend fun signOut(signOutFromHomeserver: Boolean, ignoreServerRequestError: Boolean) { + return signOutTask.execute( + SignOutTask.Params( + signOutFromHomeserver = signOutFromHomeserver, + ignoreServerRequestError = ignoreServerRequestError + ) + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt index e5213c4696..f8ec23b24d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt @@ -30,7 +30,8 @@ import javax.inject.Inject internal interface SignOutTask : Task { data class Params( - val signOutFromHomeserver: Boolean + val signOutFromHomeserver: Boolean, + val ignoreServerRequestError: Boolean, ) } @@ -59,7 +60,9 @@ internal class DefaultSignOutTask @Inject constructor( // Ignore Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755") } else { - throw throwable + if (!params.ignoreServerRequestError) { + throw throwable + } } } } diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt index 876109a2a3..cdc5973fa1 100644 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt @@ -58,8 +58,8 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionD import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore +import org.matrix.android.sdk.internal.crypto.store.RustCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask @@ -246,7 +246,7 @@ internal abstract class CryptoModule { abstract fun bindVerificationService(service: RustVerificationService): VerificationService @Binds - abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore + abstract fun bindCryptoStore(store: RustCryptoStore): IMXCommonCryptoStore @Binds abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index 2ff4d2d119..3686ab445d 100644 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -54,6 +54,7 @@ import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.coroutines.builder.safeInvokeOnClose import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.DefaultKeysAlgorithmAndData import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysAlgorithmAndData +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.verification.SasVerification import org.matrix.android.sdk.internal.crypto.verification.VerificationRequest @@ -312,10 +313,10 @@ internal class OlmMachine @Inject constructor( /** * Used for lazy migration of inboundGroupSession from EA to ER */ - suspend fun importRoomKey(inbound: InboundGroupSessionHolder): Result { + suspend fun importRoomKey(inbound: MXInboundMegolmSessionWrapper): Result { Timber.v("Migration:: Tentative lazy migration") return withContext(coroutineDispatchers.io) { - val export = inbound.wrapper.exportKeys() + val export = inbound.exportKeys() ?: return@withContext Result.failure(Exception("Failed to export key")) val result = importDecryptedKeys(listOf(export), null).also { Timber.v("Migration:: Tentative lazy migration result: ${it.totalNumberOfKeys}") diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt index cce457f6a7..891e1fe3c0 100644 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/PrepareToEncryptUseCase.kt @@ -28,7 +28,7 @@ import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService import org.matrix.android.sdk.internal.crypto.network.RequestSender -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.rustcomponents.sdk.crypto.EncryptionSettings @@ -47,7 +47,7 @@ private val loggerTag = LoggerTag("PrepareToEncryptUseCase", LoggerTag.CRYPTO) internal class PrepareToEncryptUseCase @Inject constructor( private val olmMachine: OlmMachine, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoStore: IMXCryptoStore, + private val cryptoStore: IMXCommonCryptoStore, private val getRoomUserIds: GetRoomUserIdsUseCase, private val requestSender: RequestSender, private val loadRoomMembersTask: LoadRoomMembersTask, diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt index 6d96c2c836..57f81ef592 100755 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.crypto import android.content.Context -import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.map import androidx.paging.PagedList @@ -26,7 +25,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixCoroutineDispatchers @@ -76,7 +74,7 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService import org.matrix.android.sdk.internal.crypto.model.SessionInfo import org.matrix.android.sdk.internal.crypto.network.OutgoingRequestsProcessor import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask @@ -111,7 +109,7 @@ internal class RustCryptoService @Inject constructor( @UserId private val myUserId: String, @DeviceId private val deviceId: String, // the crypto store - private val cryptoStore: IMXCryptoStore, + private val cryptoStore: IMXCommonCryptoStore, // Set of parameters used to configure/customize the end-to-end crypto. private val mxCryptoConfig: MXCryptoConfig, // Actions @@ -185,12 +183,13 @@ internal class RustCryptoService @Inject constructor( override fun getCryptoVersion(context: Context, longFormat: Boolean): String { val version = org.matrix.rustcomponents.sdk.crypto.version() + val gitHash = org.matrix.rustcomponents.sdk.crypto.versionInfo().gitSha val vodozemac = org.matrix.rustcomponents.sdk.crypto.vodozemacVersion() - return if (longFormat) "Rust SDK $version, Vodozemac $vodozemac" else version + return if (longFormat) "Rust SDK $version ($gitHash), Vodozemac $vodozemac" else version } - override fun getMyCryptoDevice(): CryptoDeviceInfo { - return runBlocking { olmMachine.ownDevice() } + override suspend fun getMyCryptoDevice(): CryptoDeviceInfo = withContext(coroutineDispatchers.io) { + olmMachine.ownDevice() } override suspend fun fetchDevicesList(): List { @@ -342,11 +341,11 @@ internal class RustCryptoService @Inject constructor( */ override suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { if (userId.isEmpty() || deviceId.isNullOrEmpty()) return null - return olmMachine.getCryptoDeviceInfo(userId, deviceId) + return withContext(coroutineDispatchers.io) { olmMachine.getCryptoDeviceInfo(userId, deviceId) } } - override fun getCryptoDeviceInfo(userId: String): List { - return runBlocking { + override suspend fun getCryptoDeviceInfo(userId: String): List { + return withContext(coroutineDispatchers.io) { olmMachine.getCryptoDeviceInfo(userId) } } @@ -903,13 +902,6 @@ internal class RustCryptoService @Inject constructor( // TODO("Not yet implemented") } - /* ========================================================================================== - * For test only - * ========================================================================================== */ - - @VisibleForTesting - val cryptoStoreForTesting = cryptoStore - companion object { const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour } diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt index df041bbf19..b5212ee45a 100644 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/network/RequestSender.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult @@ -36,7 +37,6 @@ 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.uia.UiaResult import org.matrix.android.sdk.internal.auth.registration.handleUIA -import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore import org.matrix.android.sdk.internal.crypto.OlmMachine import org.matrix.android.sdk.internal.crypto.PerSessionBackupQueryRateLimiter import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult @@ -59,6 +59,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask @@ -102,7 +103,7 @@ internal class RequestSender @Inject constructor( private val moshi: Moshi, cryptoCoroutineScope: CoroutineScope, private val rateLimiter: PerSessionBackupQueryRateLimiter, - private val inboundGroupSessionStore: InboundGroupSessionStore, + private val cryptoStore: IMXCommonCryptoStore, private val localEchoRepository: LocalEchoRepository, private val olmMachine: Lazy, ) { @@ -266,7 +267,9 @@ internal class RequestSender @Inject constructor( val senderKey = requestBody?.get("sender_key") as? String if (roomId != null && sessionId != null) { // try to perform a lazy migration from legacy store - val legacy = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey.orEmpty()) + val legacy = tryOrNull("Failed to access legacy crypto store") { + cryptoStore.getInboundGroupSession(sessionId, senderKey.orEmpty()) + } if (legacy == null || olmMachine.get().importRoomKey(legacy).isFailure) { rateLimiter.tryFromBackupIfPossible(sessionId, roomId) } diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt new file mode 100644 index 0000000000..b242a3ed34 --- /dev/null +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/store/RustCryptoStore.kt @@ -0,0 +1,387 @@ +/* + * Copyright 2023 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.store + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.where +import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.CryptoRoomInfo +import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator +import org.matrix.android.sdk.internal.crypto.store.db.doRealmTransaction +import org.matrix.android.sdk.internal.crypto.store.db.doRealmTransactionAsync +import org.matrix.android.sdk.internal.crypto.store.db.doWithRealm +import org.matrix.android.sdk.internal.crypto.store.db.mapper.CryptoRoomInfoMapper +import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.createPrimaryKey +import org.matrix.android.sdk.internal.crypto.store.db.query.getById +import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO) + +/** + * In the transition phase, the rust SDK is still using parts to the realm crypto store, + * this should be removed after full migration + */ +@SessionScope +internal class RustCryptoStore @Inject constructor( + @CryptoDatabase private val realmConfiguration: RealmConfiguration, + private val clock: Clock, + @UserId private val userId: String, + @DeviceId private val deviceId: String, + private val myDeviceLastSeenInfoEntityMapper: MyDeviceLastSeenInfoEntityMapper, + private val olmMachine: dagger.Lazy, + private val matrixCoroutineDispatchers: MatrixCoroutineDispatchers, +) : IMXCommonCryptoStore { + + // still needed on rust due to the global crypto settings + init { + // Ensure CryptoMetadataEntity is inserted in DB + doRealmTransaction("init", realmConfiguration) { realm -> + var currentMetadata = realm.where().findFirst() + + var deleteAll = false + + if (currentMetadata != null) { + // Check credentials + // The device id may not have been provided in credentials. + // Check it only if provided, else trust the stored one. + if (currentMetadata.userId != userId || deviceId != currentMetadata.deviceId) { + Timber.w("## open() : Credentials do not match, close this store and delete data") + deleteAll = true + currentMetadata = null + } + } + + if (currentMetadata == null) { + if (deleteAll) { + realm.deleteAll() + } + + // Metadata not found, or database cleaned, create it + realm.createObject(CryptoMetadataEntity::class.java, userId).apply { + deviceId = this@RustCryptoStore.deviceId + } + } + } + } + + /** + * Retrieve a device by its identity key. + * + * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) + * @return the device or null if not found + */ + override fun deviceWithIdentityKey(userId: String, identityKey: String): CryptoDeviceInfo? { + // XXX make this suspendable? + val knownDevices = runBlocking(matrixCoroutineDispatchers.io) { + olmMachine.get().getUserDevices(userId) + } + return knownDevices + .map { it.toCryptoDeviceInfo() } + .firstOrNull { + it.identityKey() == identityKey + } + } + + /** + * Needed for lazy migration of sessions from the legacy store + */ + override fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.toModel() + } + } + + // ================================================ + // Things that should be migrated to another store than realm + // ================================================ + + private val monarchyWriteAsyncExecutor = Executors.newSingleThreadExecutor() + + private val monarchy = Monarchy.Builder() + .setRealmConfiguration(realmConfiguration) + .setWriteAsyncExecutor(monarchyWriteAsyncExecutor) + .build() + + override fun open() { + // nop + } + + override fun tidyUpDataBase() { + // These entities are not used in rust actually, but as they are not yet cleaned up, this will do it with time + val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 + doRealmTransaction("tidyUpDataBase", realmConfiguration) { realm -> + + // Clean the old ones? + realm.where() + .lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs) + .findAll() + .also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") } + .deleteAllFromRealm() + + // Only keep one month history + + val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L + realm.where() + .lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs) + .findAll() + .also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") } + .deleteAllFromRealm() + + // Can we do something for WithHeldSessionEntity? + } + } + + override fun close() { + val tasks = monarchyWriteAsyncExecutor.shutdownNow() + Timber.w("Closing RealmCryptoStore, ${tasks.size} async task(s) cancelled") + tryOrNull("Interrupted") { + // Wait 1 minute max + monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES) + } + } + + override fun getRoomAlgorithm(roomId: String): String? { + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.algorithm + } + } + + override fun getRoomCryptoInfo(roomId: String): CryptoRoomInfo? { + return doWithRealm(realmConfiguration) { realm -> + CryptoRoomEntity.getById(realm, roomId)?.let { + CryptoRoomInfoMapper.map(it) + } + } + } + + /** + * This is a bit different than isRoomEncrypted. + * A room is encrypted when there is a m.room.encryption state event in the room (malformed/invalid or not). + * But the crypto layer has additional guaranty to ensure that encryption would never been reverted. + * It's defensive coding out of precaution (if ever state is reset). + */ + override fun roomWasOnceEncrypted(roomId: String): Boolean { + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.wasEncryptedOnce ?: false + } + } + + override fun setAlgorithmInfo(roomId: String, encryption: EncryptionEventContent?) { + doRealmTransaction("setAlgorithmInfo", realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).let { entity -> + entity.algorithm = encryption?.algorithm + // store anyway the new algorithm, but mark the room + // as having been encrypted once whatever, this can never + // go back to false + if (encryption?.algorithm == MXCRYPTO_ALGORITHM_MEGOLM) { + entity.wasEncryptedOnce = true + entity.rotationPeriodMs = encryption.rotationPeriodMs + entity.rotationPeriodMsgs = encryption.rotationPeriodMsgs + } + } + } + } + + override fun saveMyDevicesInfo(info: List) { + val entities = info.map { myDeviceLastSeenInfoEntityMapper.map(it) } + doRealmTransactionAsync(realmConfiguration) { realm -> + realm.where().findAll().deleteAllFromRealm() + entities.forEach { + realm.insertOrUpdate(it) + } + } + } + + override fun getMyDevicesInfo(): List { + return monarchy.fetchAllCopiedSync { + it.where() + }.map { + DeviceInfo( + deviceId = it.deviceId, + lastSeenIp = it.lastSeenIp, + lastSeenTs = it.lastSeenTs, + displayName = it.displayName + ) + } + } + + override fun getLiveMyDevicesInfo(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + }, + { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) } + ) + } + + override fun getLiveMyDevicesInfo(deviceId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + .equalTo(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, deviceId) + }, + { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) } + ) + + return Transformations.map(liveData) { + it.firstOrNull().toOptional() + } + } + + override fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) { + if (cryptoStoreAggregator.isEmpty()) { + return + } + doRealmTransaction("storeData - CryptoStoreAggregator", realmConfiguration) { realm -> + // setShouldShareHistory + cryptoStoreAggregator.setShouldShareHistoryData.forEach { + Timber.tag(loggerTag.value) + .v("setShouldShareHistory for room ${it.key} is ${it.value}") + CryptoRoomEntity.getOrCreate(realm, it.key).shouldShareHistory = it.value + } + // setShouldEncryptForInvitedMembers + cryptoStoreAggregator.setShouldEncryptForInvitedMembersData.forEach { + CryptoRoomEntity.getOrCreate(realm, it.key).shouldEncryptForInvitedMembers = it.value + } + } + } + + override fun shouldEncryptForInvitedMembers(roomId: String): Boolean { + return doWithRealm(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers + } + ?: false + } + + override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) { + Timber.tag(loggerTag.value) + .v("setShouldShareHistory for room $roomId is $shouldShareHistory") + doRealmTransaction("setShouldShareHistory", realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory + } + } + + override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { + doRealmTransaction("setShouldEncryptForInvitedMembers", realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers + } + } + + override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) { + doRealmTransaction("blockUnverifiedDevicesInRoom", realmConfiguration) { realm -> + CryptoRoomEntity.getById(realm, roomId) + ?.blacklistUnverifiedDevices = block + } + } + + override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { + doRealmTransaction("setGlobalBlacklistUnverifiedDevices", realmConfiguration) { + it.where().findFirst()?.globalBlacklistUnverifiedDevices = block + } + } + + override fun getLiveGlobalCryptoConfig(): LiveData { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where() + }, + { + GlobalCryptoConfig( + globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices, + globalEnableKeyGossiping = it.globalEnableKeyGossiping, + enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite + ) + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: GlobalCryptoConfig(false, false, false) + } + } + + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.globalBlacklistUnverifiedDevices + } ?: false + } + + override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + }, + { + it.blacklistUnverifiedDevices + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: false + } + } + + override fun getBlockUnverifiedDevices(roomId: String): Boolean { + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + .findFirst() + ?.blacklistUnverifiedDevices ?: false + } + } +} diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt index 8cb6618a18..35965d6f2e 100644 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_ import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE import org.matrix.android.sdk.internal.crypto.model.rest.toValue import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.rustcomponents.sdk.crypto.VerificationRequestState import timber.log.Timber import javax.inject.Inject @@ -165,7 +166,7 @@ internal class RustVerificationService @Inject constructor( // If this is a SAS verification originating from a `m.key.verification.request` // event, we auto-accept here considering that we either initiated the request or // accepted the request. If it's a QR code verification, just dispatch an update. - if (request.isReady() && transaction is SasVerification) { + if (request.innerState() is VerificationRequestState.Ready && transaction is SasVerification) { // accept() will dispatch an update, no need to do it twice. Timber.d("## Verification: Auto accepting SAS verification with $sender") transaction.accept() @@ -308,7 +309,7 @@ internal class RustVerificationService @Inject constructor( return if (request != null) { request.acceptWithMethods(methods) request.startQrCode() - request.isReady() + request.innerState() is VerificationRequestState.Ready } else { false } diff --git a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt index 8d1b3392aa..641bf66c12 100644 --- a/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt +++ b/matrix-sdk-android/src/rustCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationRequest.kt @@ -136,9 +136,9 @@ internal class VerificationRequest @AssistedInject constructor( * concrete verification flow, i.e. we can show/scan a QR code or start emoji * verification. */ - internal fun isReady(): Boolean { - return innerVerificationRequest.isReady() - } +// internal fun isReady(): Boolean { +// return innerVerificationRequest.isReady() +// } /** Did we advertise that we're able to scan QR codes */ internal fun canScanQrCodes(): Boolean { diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt index 0ae712bff1..113dc4ce83 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt @@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult 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.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore class EventEditValidatorTest { @@ -62,7 +62,7 @@ class EventEditValidatorTest { @Test fun `edit should be valid`() { - val mockCryptoStore = mockk() + val mockCryptoStore = mockk() val validator = EventEditValidator(mockCryptoStore) validator @@ -71,7 +71,7 @@ class EventEditValidatorTest { @Test fun `original event and replacement event must have the same sender`() { - val mockCryptoStore = mockk() + val mockCryptoStore = mockk() val validator = EventEditValidator(mockCryptoStore) validator @@ -83,7 +83,7 @@ class EventEditValidatorTest { @Test fun `original event and replacement event must have the same room_id`() { - val mockCryptoStore = mockk() + val mockCryptoStore = mockk() val validator = EventEditValidator(mockCryptoStore) validator @@ -101,7 +101,7 @@ class EventEditValidatorTest { @Test fun `replacement and original events must not have a state_key property`() { - val mockCryptoStore = mockk() + val mockCryptoStore = mockk() val validator = EventEditValidator(mockCryptoStore) validator @@ -119,8 +119,8 @@ class EventEditValidatorTest { @Test fun `replacement event must have an new_content property`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } @@ -157,8 +157,8 @@ class EventEditValidatorTest { @Test fun `The original event must not itself have a rel_type of m_replace`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } @@ -207,8 +207,8 @@ class EventEditValidatorTest { @Test fun `valid e2ee edit`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } @@ -224,8 +224,8 @@ class EventEditValidatorTest { @Test fun `If the original event was encrypted, the replacement should be too`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } @@ -241,12 +241,12 @@ class EventEditValidatorTest { @Test fun `encrypted, original event and replacement event must have the same sender`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } - every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns + every { deviceWithIdentityKey("@bob:example.com", "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns mockk { every { userId } returns "@bob:example.com" } @@ -256,7 +256,9 @@ class EventEditValidatorTest { validator .validateEdit( encryptedEvent, - encryptedEditEvent.copy().apply { + encryptedEditEvent.copy( + senderId = "@bob:example.com" + ).apply { mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" ) @@ -269,12 +271,12 @@ class EventEditValidatorTest { @Test fun `encrypted, sent fom a deleted device, original event and replacement event must have the same sender`() { - val mockCryptoStore = mockk { - every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns + val mockCryptoStore = mockk { + every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns mockk { every { userId } returns "@alice:example.com" } - every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns + every { deviceWithIdentityKey(any(), "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns null } val validator = EventEditValidator(mockCryptoStore) @@ -288,7 +290,7 @@ class EventEditValidatorTest { ) } - ) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class + ) shouldBeInstanceOf EventEditValidator.EditValidity.Unknown::class validator .validateEdit( diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 28ccc03815..9a2506391e 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 6 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 1 +ext.versionPatch = 2 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' diff --git a/vector-app/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt b/vector-app/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt index cafc2d65e6..313073da4d 100644 --- a/vector-app/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt +++ b/vector-app/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestTokenRegistration.kt @@ -17,6 +17,7 @@ package im.vector.app.gplay.features.settings.troubleshoot import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.work.WorkInfo import androidx.work.WorkManager import im.vector.app.R @@ -25,6 +26,8 @@ import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.pushers.PushersManager import im.vector.app.core.resources.StringProvider import im.vector.app.features.settings.troubleshoot.TroubleshootTest +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.pushers.PusherState import javax.inject.Inject @@ -60,16 +63,18 @@ class TestTokenRegistration @Inject constructor( ) quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_token_registration_quick_fix) { override fun doFix() { - val workId = pushersManager.enqueueRegisterPusherWithFcmKey(fcmToken) - WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo -> - if (workInfo != null) { - if (workInfo.state == WorkInfo.State.SUCCEEDED) { - manager?.retry(testParameters) - } else if (workInfo.state == WorkInfo.State.FAILED) { - manager?.retry(testParameters) + context.lifecycleScope.launch(Dispatchers.IO) { + val workId = pushersManager.enqueueRegisterPusherWithFcmKey(fcmToken) + WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo -> + if (workInfo != null) { + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + manager?.retry(testParameters) + } else if (workInfo.state == WorkInfo.State.FAILED) { + manager?.retry(testParameters) + } } - } - }) + }) + } } } diff --git a/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt index 53e65f88b4..676cd0258f 100755 --- a/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt +++ b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt @@ -26,8 +26,11 @@ import dagger.hilt.android.qualifiers.ApplicationContext import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DefaultPreferences +import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.pushers.PushersManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -38,7 +41,12 @@ import javax.inject.Inject class GoogleFcmHelper @Inject constructor( @ApplicationContext private val context: Context, @DefaultPreferences private val sharedPrefs: SharedPreferences, + appScope: CoroutineScope, + private val coroutineDispatchers: CoroutineDispatchers ) : FcmHelper { + + private val scope = CoroutineScope(appScope.coroutineContext + coroutineDispatchers.io) + companion object { private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" } @@ -64,7 +72,9 @@ class GoogleFcmHelper @Inject constructor( .addOnSuccessListener { token -> storeFcmToken(token) if (registerPusher) { - pushersManager.enqueueRegisterPusherWithFcmKey(token) + scope.launch { + pushersManager.enqueueRegisterPusherWithFcmKey(token) + } } } .addOnFailureListener { e -> diff --git a/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt b/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt index 7fd55bd165..6ab9b90a84 100644 --- a/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt @@ -27,6 +27,10 @@ import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.pushers.VectorPushHandler import im.vector.app.features.settings.VectorPreferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.logger.LoggerTag import timber.log.Timber import javax.inject.Inject @@ -43,6 +47,12 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var vectorPushHandler: VectorPushHandler @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + private val scope = CoroutineScope(SupervisorJob()) + + override fun onDestroy() { + scope.cancel() + super.onDestroy() + } override fun onNewToken(token: String) { Timber.tag(loggerTag.value).d("New Firebase token") fcmHelper.storeFcmToken(token) @@ -51,7 +61,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { activeSessionHolder.hasActiveSession() && unifiedPushHelper.isEmbeddedDistributor() ) { - pushersManager.enqueueRegisterPusher(token, getString(R.string.pusher_http_url)) + scope.launch { + pushersManager.enqueueRegisterPusher(token, getString(R.string.pusher_http_url)) + } } } diff --git a/vector/build.gradle b/vector/build.gradle index 05a23bf0df..fb9c63ad2a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -310,7 +310,7 @@ dependencies { // Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868 // Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0) //noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26. - implementation "org.checkerframework:checker:3.34.0" + implementation "org.checkerframework:checker:3.35.0" androidTestImplementation libs.androidx.testCore androidTestImplementation libs.androidx.testRunner diff --git a/vector/src/main/java/im/vector/app/core/device/GetDeviceInfoUseCase.kt b/vector/src/main/java/im/vector/app/core/device/GetDeviceInfoUseCase.kt index 4a66988b46..a97fa32a61 100644 --- a/vector/src/main/java/im/vector/app/core/device/GetDeviceInfoUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/device/GetDeviceInfoUseCase.kt @@ -22,14 +22,14 @@ import javax.inject.Inject interface GetDeviceInfoUseCase { - fun execute(): CryptoDeviceInfo + suspend fun execute(): CryptoDeviceInfo } class DefaultGetDeviceInfoUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder ) : GetDeviceInfoUseCase { - override fun execute(): CryptoDeviceInfo { + override suspend fun execute(): CryptoDeviceInfo { return activeSessionHolder.getActiveSession().cryptoService().getMyCryptoDevice() } } diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index fb78feaef1..402471ecef 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -49,11 +49,11 @@ class PushersManager @Inject constructor( ) } - fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID { + suspend fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID { return enqueueRegisterPusher(pushKey, stringProvider.getString(R.string.pusher_http_url)) } - fun enqueueRegisterPusher( + suspend fun enqueueRegisterPusher( pushKey: String, gateway: String ): UUID { @@ -62,7 +62,7 @@ class PushersManager @Inject constructor( return currentSession.pushersService().enqueueAddHttpPusher(pusher) } - private fun createHttpPusher( + private suspend fun createHttpPusher( pushKey: String, gateway: String ) = HttpPusher( diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorUnifiedPushMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorUnifiedPushMessagingReceiver.kt index 838cbd5935..f0e5cd2c2c 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorUnifiedPushMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorUnifiedPushMessagingReceiver.kt @@ -76,7 +76,9 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { coroutineScope.launch { unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { unifiedPushHelper.getPushGateway()?.let { - pushersManager.enqueueRegisterPusher(endpoint, it) + coroutineScope.launch { + pushersManager.enqueueRegisterPusher(endpoint, it) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index ff0e9721e4..48fe092524 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -60,6 +60,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.failure.GlobalError +import org.matrix.android.sdk.api.session.Session import timber.log.Timber import javax.inject.Inject @@ -262,18 +263,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity } } args.clearCredentials -> { - lifecycleScope.launch { - try { - session.signOutService().signOut(!args.isUserLoggedOut) - } catch (failure: Throwable) { - displayError(failure) - return@launch - } - Timber.w("SIGN_OUT: success, start app") - activeSessionHolder.clearActiveSession() - doLocalCleanup(clearPreferences = true, onboardingStore) - startNextActivityAndFinish() - } + signout(session, onboardingStore, ignoreServerError = false) } args.clearCache -> { lifecycleScope.launch { @@ -286,6 +276,26 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity } } + private fun signout( + session: Session, + onboardingStore: VectorSessionStore, + ignoreServerError: Boolean, + ) { + lifecycleScope.launch { + try { + session.signOutService().signOut(!args.isUserLoggedOut, ignoreServerError) + } catch (failure: Throwable) { + Timber.e(failure, "SIGN_OUT: error, propose to sign out anyway") + displaySignOutFailedDialog(session, onboardingStore) + return@launch + } + Timber.w("SIGN_OUT: success, start app") + activeSessionHolder.clearActiveSession() + doLocalCleanup(clearPreferences = true, onboardingStore) + startNextActivityAndFinish() + } + } + override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { // No op here Timber.w("Ignoring invalid token global error") @@ -313,12 +323,20 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity } } - private fun displayError(failure: Throwable) { + private fun displaySignOutFailedDialog( + session: Session, + onboardingStore: VectorSessionStore, + ) { if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { MaterialAlertDialogBuilder(this) .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(failure)) - .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() } + .setMessage(R.string.sign_out_failed_dialog_message) + .setPositiveButton(R.string.sign_out_anyway) { _, _ -> + signout(session, onboardingStore, ignoreServerError = true) + } + .setNeutralButton(R.string.global_retry) { _, _ -> + signout(session, onboardingStore, ignoreServerError = false) + } .setNegativeButton(R.string.action_cancel) { _, _ -> startNextActivityAndFinish(ignoreClearCredentials = true) } .setCancelable(false) .show() diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt index 4097cf957b..9871d21601 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt @@ -275,7 +275,7 @@ class SelfVerificationController @Inject constructor( id("notice_div") } // Option to verify with another device - if (state.hasAnyOtherSession) { + if (state.hasAnyOtherSession.invoke() == true) { bottomSheetVerificationActionItem { id("start") title(host.stringProvider.getString(R.string.verification_verify_with_another_device)) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt index e92616f7df..55d84a2446 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt @@ -83,7 +83,7 @@ data class SelfVerificationViewState( val transactionId: String? = null, val currentDeviceCanCrossSign: Boolean = false, val userWantsToCancel: Boolean = false, - val hasAnyOtherSession: Boolean = false, + val hasAnyOtherSession: Async = Uninitialized, val quadSContainsSecrets: Boolean = false, val isVerificationRequired: Boolean = false, val isThisSessionVerified: Boolean = false, @@ -146,21 +146,28 @@ class SelfVerificationViewModel @AssistedInject constructor( } } - val hasAnyOtherSession = session.cryptoService() - .getCryptoDeviceInfo(session.myUserId) - .any { - it.deviceId != session.sessionParams.deviceId - } + setState { copy(hasAnyOtherSession = Loading()) } + viewModelScope.launch { + val hasAnyOtherSession = session.cryptoService() + .getCryptoDeviceInfo(session.myUserId) + .any { + it.deviceId != session.sessionParams.deviceId + } + setState { + copy( + hasAnyOtherSession = Success(hasAnyOtherSession) + ) + } + } setState { copy( currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(), quadSContainsSecrets = session.sharedSecretStorageService().isRecoverySetup(), - hasAnyOtherSession = hasAnyOtherSession ) } - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { val isThisSessionVerified = session.cryptoService().crossSigningService().isCrossSigningVerified() setState { copy( diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 28016f109f..6ba5976eb8 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -92,11 +92,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( } init { - val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) - .firstOrNull { it.deviceId == session.sessionParams.deviceId } - ?.firstTimeSeenLocalTs - ?: clock.epochMillis() - Timber.v("## Detector - Current Session first time seen $currentSessionTs") combine( session.flow().liveUserCryptoDevices(session.myUserId), @@ -108,6 +103,12 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( deleteUnusedClientInformation(infoList) + val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) + .firstOrNull { it.deviceId == session.sessionParams.deviceId } + ?.firstTimeSeenLocalTs + ?: clock.epochMillis() + Timber.v("## Detector - Current Session first time seen $currentSessionTs") + infoList .asSequence() .filter { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 09b91cc4f1..5f8883fdfe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -528,6 +528,8 @@ class MessageItemFactory @Inject constructor( ) val playable = messageContent.mimeType == MimeTypes.Gif + // don't show play button because detecting animated webp isn't possible via mimetype + val playableIfAutoplay = playable || messageContent.mimeType == MimeTypes.Webp return MessageImageVideoItem_() .attributes(attributes) @@ -549,7 +551,7 @@ class MessageItemFactory @Inject constructor( } } }.apply { - if (playable && vectorPreferences.autoplayAnimatedImages()) { + if (playableIfAutoplay && vectorPreferences.autoplayAnimatedImages()) { mode(ImageContentRenderer.Mode.ANIMATED_THUMBNAIL) } } diff --git a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt index 0924532bc2..a0354f3d60 100644 --- a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt @@ -49,7 +49,7 @@ class DataAttachmentRoomProvider( return getItem(position).let { when (it) { is ImageContentRenderer.Data -> { - if (it.mimeType == MimeTypes.Gif) { + if (it.mimeType == MimeTypes.Gif || it.mimeType == MimeTypes.Webp) { AttachmentInfo.AnimatedImage( uid = it.eventId, url = it.url ?: "", diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index baad815df2..876812cea0 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -135,7 +135,7 @@ class ImageContentRenderer @Inject constructor( if (mode == Mode.ANIMATED_THUMBNAIL) it else it.dontAnimate() } - .transform(cornerTransformation) + .optionalTransform(cornerTransformation) .into(imageView) } @@ -167,7 +167,7 @@ class ImageContentRenderer @Inject constructor( } req - .fitCenter() + .optionalFitCenter() .into(target) } @@ -211,7 +211,7 @@ class ImageContentRenderer @Inject constructor( return false } }) - .fitCenter() + .optionalFitCenter() .into(imageView) } diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt index e18a13a3e6..f833a33b87 100644 --- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt @@ -71,7 +71,7 @@ class RoomEventsAttachmentProvider( allowNonMxcUrls = it.root.sendState.isSending() ) - if (content.mimeType == MimeTypes.Gif) { + if (content.mimeType == MimeTypes.Gif || content.mimeType == MimeTypes.Webp) { AttachmentInfo.AnimatedImage( uid = it.eventId, url = content.url ?: "", diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt index b355b55903..6489fe537d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -17,6 +17,7 @@ package im.vector.app.features.settings.troubleshoot import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import androidx.work.WorkInfo import androidx.work.WorkManager import im.vector.app.R @@ -72,13 +73,15 @@ class TestEndpointAsTokenRegistration @Inject constructor( } private fun unregisterThenRegister(testParameters: TestParameters, pushKey: String) { - activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch { + val scope = activeSessionHolder.getSafeActiveSession()?.coroutineScope ?: return + val io = activeSessionHolder.getActiveSession().coroutineDispatchers.io + scope.launch(io) { unregisterUnifiedPushUseCase.execute(pushersManager) registerUnifiedPush(distributor = "", testParameters, pushKey) } } - private fun registerUnifiedPush( + private suspend fun registerUnifiedPush( distributor: String, testParameters: TestParameters, pushKey: String, @@ -106,7 +109,9 @@ class TestEndpointAsTokenRegistration @Inject constructor( pushKey: String, ) { unifiedPushHelper.showSelectDistributorDialog(context) { selection -> - registerUnifiedPush(distributor = selection, testParameters, pushKey) + context.lifecycleScope.launch { + registerUnifiedPush(distributor = selection, testParameters, pushKey) + } } } } diff --git a/vector/src/test/java/im/vector/app/core/device/DefaultGetDeviceInfoUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/device/DefaultGetDeviceInfoUseCaseTest.kt index 0673fbadb5..01e5c88809 100644 --- a/vector/src/test/java/im/vector/app/core/device/DefaultGetDeviceInfoUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/device/DefaultGetDeviceInfoUseCaseTest.kt @@ -19,6 +19,7 @@ package im.vector.app.core.device import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeCryptoService import im.vector.app.test.fakes.FakeSession +import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test @@ -31,7 +32,7 @@ class DefaultGetDeviceInfoUseCaseTest { private val getDeviceInfoUseCase = DefaultGetDeviceInfoUseCase(activeSessionHolder.instance) @Test - fun `when execute, then get crypto device info`() { + fun `when execute, then get crypto device info`() = runTest { val result = getDeviceInfoUseCase.execute() result shouldBeEqualTo cryptoService.cryptoDeviceInfo diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt index 1e228353f5..4b6063fb93 100644 --- a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt +++ b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt @@ -29,6 +29,7 @@ import im.vector.app.test.fixtures.CryptoDeviceInfoFixture.aCryptoDeviceInfo import im.vector.app.test.fixtures.PusherFixture import im.vector.app.test.fixtures.SessionParamsFixture import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo @@ -56,7 +57,7 @@ class PushersManagerTest { ) @Test - fun `when enqueueRegisterPusher, then HttpPusher created and enqueued`() { + fun `when enqueueRegisterPusher, then HttpPusher created and enqueued`() = runTest { val pushKey = "abc" val gateway = "123" val pusherAppId = "app-id" diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt index e8bb15ac58..b5e503dd98 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt @@ -84,5 +84,5 @@ class FakeCryptoService( } } - override fun getMyCryptoDevice() = cryptoDeviceInfo + override suspend fun getMyCryptoDevice() = cryptoDeviceInfo } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeGetDeviceInfoUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeGetDeviceInfoUseCase.kt index c284263d28..34853d1776 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeGetDeviceInfoUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeGetDeviceInfoUseCase.kt @@ -17,13 +17,13 @@ package im.vector.app.test.fakes import im.vector.app.core.device.GetDeviceInfoUseCase -import io.mockk.every +import io.mockk.coEvery import io.mockk.mockk import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo class FakeGetDeviceInfoUseCase : GetDeviceInfoUseCase by mockk() { fun givenDeviceInfo(cryptoDeviceInfo: CryptoDeviceInfo) { - every { execute() } returns cryptoDeviceInfo + coEvery { execute() } returns cryptoDeviceInfo } }