Merge tag 'v1.6.2' into sc

Change-Id: I7a9ebbae6df28044a8fa308fe34241dbd1d8b8ae

Conflicts:
	vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
	vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt
This commit is contained in:
SpiritCroc 2023-06-05 14:45:57 +02:00
commit f440628208
58 changed files with 849 additions and 278 deletions

View file

@ -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) Changes in Element v1.6.1 (2023-05-25)
====================================== ======================================

View file

@ -48,7 +48,7 @@ ext.libs = [
'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
], ],
androidx : [ androidx : [
'activity' : "androidx.activity:activity-ktx:1.7.1", 'activity' : "androidx.activity:activity-ktx:1.7.2",
'appCompat' : "androidx.appcompat:appcompat:1.6.1", 'appCompat' : "androidx.appcompat:appcompat:1.6.1",
'biometric' : "androidx.biometric:biometric:1.1.0", 'biometric' : "androidx.biometric:biometric:1.1.0",
'core' : "androidx.core:core-ktx:1.10.1", 'core' : "androidx.core:core-ktx:1.10.1",

View file

@ -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

View file

@ -328,6 +328,9 @@
<string name="backup">Back up</string> <string name="backup">Back up</string>
<string name="sign_out_bottom_sheet_will_lose_secure_messages">Youll lose access to your encrypted messages unless you back up your keys before signing out.</string> <string name="sign_out_bottom_sheet_will_lose_secure_messages">Youll lose access to your encrypted messages unless you back up your keys before signing out.</string>
<string name="sign_out_failed_dialog_message">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.</string>
<string name="sign_out_anyway">Sign out anyway</string>
<!-- splash screen accessibility --> <!-- splash screen accessibility -->
<string name="loading">Loading…</string> <string name="loading">Loading…</string>

View file

@ -63,7 +63,7 @@ android {
// that the app's state is completely cleared between tests. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' 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", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@ -216,7 +216,7 @@ dependencies {
implementation libs.google.phonenumber implementation libs.google.phonenumber
rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.7") rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.9")
// rustCryptoApi project(":library:rustCrypto") // rustCryptoApi project(":library:rustCrypto")
testImplementation libs.tests.junit testImplementation libs.tests.junit

View file

@ -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.StoreRoomSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask 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.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.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration
@ -254,6 +255,9 @@ internal abstract class CryptoModule {
@Binds @Binds
abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore
@Binds
abstract fun bindCommonCryptoStore(store: RealmCryptoStore): IMXCommonCryptoStore
@Binds @Binds
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask

View file

@ -256,7 +256,7 @@ internal class DefaultCryptoService @Inject constructor(
return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version
} }
override fun getMyCryptoDevice(): CryptoDeviceInfo { override suspend fun getMyCryptoDevice(): CryptoDeviceInfo {
return myDeviceInfoHolder.get().myDevice return myDeviceInfoHolder.get().myDevice
} }
@ -506,10 +506,7 @@ internal class DefaultCryptoService @Inject constructor(
null null
} else { } else {
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
cryptoStore.deviceWithIdentityKey(senderKey).takeIf { cryptoStore.deviceWithIdentityKey(userId, senderKey)
// check that the claimed user id matches
it?.userId == userId
}
} }
} }
} }
@ -539,7 +536,7 @@ internal class DefaultCryptoService @Inject constructor(
// .executeBy(taskExecutor) // .executeBy(taskExecutor)
// } // }
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> { override suspend fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDeviceList(userId).orEmpty() return cryptoStore.getUserDeviceList(userId).orEmpty()
} }
// //

View file

@ -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.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail 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.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.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody 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.crypto.model.TrailType
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent 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.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.util.Optional 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.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper 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.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.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmOutboundGroupSession import org.matrix.olm.OlmOutboundGroupSession
@ -49,7 +45,7 @@ import org.matrix.olm.OlmOutboundGroupSession
/** /**
* The crypto data store. * The crypto data store.
*/ */
internal interface IMXCryptoStore { internal interface IMXCryptoStore : IMXCommonCryptoStore {
/** /**
* @return the device id * @return the device id
@ -78,21 +74,6 @@ internal interface IMXCryptoStore {
*/ */
fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper> fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper>
/**
* @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. * Enable or disable key gossiping.
* Default is true. * Default is true.
@ -123,28 +104,6 @@ internal interface IMXCryptoStore {
*/ */
fun getRoomsListBlacklistUnverifiedDevices(): List<String> fun getRoomsListBlacklistUnverifiedDevices(): List<String>
/**
* A live status regarding sharing keys for unverified devices in this room.
*
* @return Live status
*/
fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean>
/**
* 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. * Get the current keys backup version.
*/ */
@ -186,16 +145,6 @@ internal interface IMXCryptoStore {
*/ */
fun deleteStore() fun deleteStore()
/**
* open any existing crypto store.
*/
fun open()
/**
* Close the store.
*/
fun close()
/** /**
* Store the device id. * Store the device id.
* *
@ -262,14 +211,6 @@ internal interface IMXCryptoStore {
fun getLiveDeviceWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>> fun getLiveDeviceWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>>
fun getMyDevicesInfo(): List<DeviceInfo>
fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>>
fun getLiveMyDevicesInfo(deviceId: String): LiveData<Optional<DeviceInfo>>
fun saveMyDevicesInfo(info: List<DeviceInfo>)
/** /**
* Store the crypto algorithm for a room. * Store the crypto algorithm for a room.
* *
@ -278,47 +219,8 @@ internal interface IMXCryptoStore {
*/ */
fun storeRoomAlgorithm(roomId: String, algorithm: String?) 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 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. * Store a session between the logged-in user and another device.
* *
@ -361,15 +263,6 @@ internal interface IMXCryptoStore {
*/ */
fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>) fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>)
/**
* 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. * Retrieve an inbound group session, filtering shared history.
* *
@ -552,7 +445,6 @@ internal interface IMXCryptoStore {
// fun getCrossSigningPrivateKeysFlow(): Flow<Optional<PrivateKeysInfo>> // fun getCrossSigningPrivateKeysFlow(): Flow<Optional<PrivateKeysInfo>>
fun getGlobalCryptoConfig(): GlobalCryptoConfig fun getGlobalCryptoConfig(): GlobalCryptoConfig
fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig>
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo?
@ -597,14 +489,8 @@ internal interface IMXCryptoStore {
fun setDeviceKeysUploaded(uploaded: Boolean) fun setDeviceKeysUploaded(uploaded: Boolean)
fun areDeviceKeysUploaded(): Boolean fun areDeviceKeysUploaded(): Boolean
fun tidyUpDataBase()
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest> fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
/**
* 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]. * Store a bunch of data related to the users. @See [UserDataToStore].
*/ */

View file

@ -280,6 +280,19 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun deviceWithIdentityKey(userId: String, identityKey: String): CryptoDeviceInfo? {
return doWithRealm(realmConfiguration) { realm ->
realm.where<DeviceInfoEntity>()
.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<String, CryptoDeviceInfo>?) { override fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) {
doRealmTransaction("storeUserDevices", realmConfiguration) { realm -> doRealmTransaction("storeUserDevices", realmConfiguration) { realm ->
storeUserDevices(realm, userId, devices) storeUserDevices(realm, userId, devices)

View file

@ -73,7 +73,7 @@ interface CryptoService {
suspend fun getUserDevices(userId: String): List<CryptoDeviceInfo> suspend fun getUserDevices(userId: String): List<CryptoDeviceInfo>
fun getMyCryptoDevice(): CryptoDeviceInfo suspend fun getMyCryptoDevice(): CryptoDeviceInfo
fun getGlobalBlacklistUnverifiedDevices(): Boolean fun getGlobalBlacklistUnverifiedDevices(): Boolean
@ -130,7 +130,7 @@ interface CryptoService {
suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> suspend fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo>
// fun getCryptoDeviceInfoFlow(userId: String): Flow<List<CryptoDeviceInfo>> // fun getCryptoDeviceInfoFlow(userId: String): Flow<List<CryptoDeviceInfo>>

View file

@ -37,6 +37,7 @@ interface SignOutService {
/** /**
* Sign out, and release the session, clear all the session data, including crypto data. * 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 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)
} }

View file

@ -301,6 +301,9 @@ internal class DefaultAuthenticationService @Inject constructor(
val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibilty == true } 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 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( return LoginFlowResult(
supportedLoginTypes = flows.orEmpty().mapNotNull { it.type }, supportedLoginTypes = flows.orEmpty().mapNotNull { it.type },
ssoIdentityProviders = flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, ssoIdentityProviders = flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
@ -309,7 +312,7 @@ internal class DefaultAuthenticationService @Inject constructor(
isOutdatedHomeserver = !versions.isSupportedBySdk(), isOutdatedHomeserver = !versions.isSupportedBySdk(),
hasOidcCompatibilityFlow = oidcCompatibilityFlow != null, hasOidcCompatibilityFlow = oidcCompatibilityFlow != null,
isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(), isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(),
isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(), isLoginWithQrSupported = supportsGetLoginTokenFlow || versions.doesServerSupportQrCodeLogin(),
) )
} }

View file

@ -51,5 +51,13 @@ internal data class LoginFlow(
* See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824) * See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824)
*/ */
@Json(name = "org.matrix.msc3824.delegated_oidc_compatibility") @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
) )

View file

@ -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_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440" private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" 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_QR_CODE_LOGIN = "org.matrix.msc3882"
private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771" private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771"
private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773" 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) 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 { internal fun Versions.doesServerSupportQrCodeLogin(): Boolean {
@Suppress("DEPRECATION")
return unstableFeatures?.get(FEATURE_QR_CODE_LOGIN) ?: false return unstableFeatures?.get(FEATURE_QR_CODE_LOGIN) ?: false
} }

View file

@ -17,11 +17,11 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.crypto.MXCryptoConfig 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 import javax.inject.Inject
internal class ShouldEncryptForInvitedMembersUseCase @Inject constructor(private val cryptoConfig: MXCryptoConfig, internal class ShouldEncryptForInvitedMembersUseCase @Inject constructor(private val cryptoConfig: MXCryptoConfig,
private val cryptoStore: IMXCryptoStore) { private val cryptoStore: IMXCommonCryptoStore) {
operator fun invoke(roomId: String): Boolean { operator fun invoke(roomId: String): Boolean {
return cryptoConfig.enableEncryptionForInvitedMembers && cryptoStore.shouldEncryptForInvitedMembers(roomId) return cryptoConfig.enableEncryptionForInvitedMembers && cryptoStore.shouldEncryptForInvitedMembers(roomId)

View file

@ -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<DeviceInfo>)
// questionable that it's stored in crypto store
fun getMyDevicesInfo(): List<DeviceInfo>
// questionable that it's stored in crypto store
fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>>
// questionable that it's stored in crypto store
fun getLiveMyDevicesInfo(deviceId: String): LiveData<Optional<DeviceInfo>>
/**
* 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<GlobalCryptoConfig>
/**
* @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<Boolean>
/**
* 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?
}

View file

@ -71,7 +71,14 @@ internal data class Capabilities(
* True if the user can use m.thread relation, false otherwise. * True if the user can use m.thread relation, false otherwise.
*/ */
@Json(name = "m.thread") @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) @JsonClass(generateAdapter = true)

View file

@ -151,8 +151,6 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
getVersionResult.doesServerSupportThreads() getVersionResult.doesServerSupportThreads()
homeServerCapabilitiesEntity.canUseThreadReadReceiptsAndNotifications = homeServerCapabilitiesEntity.canUseThreadReadReceiptsAndNotifications =
getVersionResult.doesServerSupportThreadUnreadNotifications() getVersionResult.doesServerSupportThreadUnreadNotifications()
homeServerCapabilitiesEntity.canLoginWithQrCode =
getVersionResult.doesServerSupportQrCodeLogin()
homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices = homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices =
getVersionResult.doesServerSupportRemoteToggleOfPushNotifications() getVersionResult.doesServerSupportRemoteToggleOfPushNotifications()
homeServerCapabilitiesEntity.canRedactEventWithRelations = homeServerCapabilitiesEntity.canRedactEventWithRelations =
@ -169,10 +167,25 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
} }
homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl
} }
homeServerCapabilitiesEntity.canLoginWithQrCode = canLoginWithQrCode(getCapabilitiesResult, getVersionResult)
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time 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 { companion object {
// 8 hours like on Element Web // 8 hours like on Element Web
private const val MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS = 8 * 60 * 60 * 1000 private const val MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS = 8 * 60 * 60 * 1000

View file

@ -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.events.model.toValidDecryptedEvent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent 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.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) { internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCommonCryptoStore) {
sealed class EditValidity { sealed class EditValidity {
object Valid : 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. * If the original event was encrypted, the replacement should be too.
*/ */
fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity { 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 // 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 // Edits should be revalidated when the original event is received
if (originalEvent == null) { if (originalEvent == null) {
@ -80,25 +78,21 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
val replaceDecrypted = replaceEvent.toValidDecryptedEvent() val replaceDecrypted = replaceEvent.toValidDecryptedEvent()
?: return EditValidity.Unknown // UTD can't decide ?: return EditValidity.Unknown // UTD can't decide
val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId if (originalEvent.senderId != replaceEvent.senderId) {
val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId 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) { if (originalDecrypted.getRelationContent()?.type == RelationType.REPLACE) {
return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.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? // 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? // maybe it's a deleted device or a not yet downloaded one?
val bestEffortOriginal = originalCryptoSenderId ?: originalEvent.senderId return EditValidity.Unknown
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")
}
} }
if (originalDecrypted.type != replaceDecrypted.type) { if (originalDecrypted.type != replaceDecrypted.type) {

View file

@ -22,6 +22,8 @@ import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import 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.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
@ -44,7 +46,8 @@ internal class DefaultReadService @AssistedInject constructor(
private val setMarkedUnreadTask: SetMarkedUnreadTask, private val setMarkedUnreadTask: SetMarkedUnreadTask,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
@UserId private val userId: String, @UserId private val userId: String,
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
private val matrixCoroutineDispatchers: MatrixCoroutineDispatchers,
) : ReadService { ) : ReadService {
@AssistedFactory @AssistedFactory
@ -69,7 +72,7 @@ internal class DefaultReadService @AssistedInject constructor(
setMarkedUnreadFlag(false) setMarkedUnreadFlag(false)
} }
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) { val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) {
threadId threadId
} else { } else {

View file

@ -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.JsonDict
import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.api.util.TextContent 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.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.content.UploadContentWorker
@ -69,7 +69,7 @@ internal class DefaultSendService @AssistedInject constructor(
private val workManagerProvider: WorkManagerProvider, private val workManagerProvider: WorkManagerProvider,
@SessionId private val sessionId: String, @SessionId private val sessionId: String,
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCommonCryptoStore,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository, private val localEchoRepository: LocalEchoRepository,
private val eventSenderProcessor: EventSenderProcessor, private val eventSenderProcessor: EventSenderProcessor,

View file

@ -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.Session
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.Cancelable 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.session.SessionScope
import org.matrix.android.sdk.internal.task.CoroutineSequencer import org.matrix.android.sdk.internal.task.CoroutineSequencer
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
@ -54,7 +54,7 @@ private const val MAX_RETRY_COUNT = 3
*/ */
@SessionScope @SessionScope
internal class EventSenderProcessorCoroutine @Inject constructor( internal class EventSenderProcessorCoroutine @Inject constructor(
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCommonCryptoStore,
private val sessionParams: SessionParams, private val sessionParams: SessionParams,
private val queuedTaskFactory: QueuedTaskFactory, private val queuedTaskFactory: QueuedTaskFactory,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,

View file

@ -35,7 +35,12 @@ internal class DefaultSignOutService @Inject constructor(
sessionParamsStore.updateCredentials(credentials) sessionParamsStore.updateCredentials(credentials)
} }
override suspend fun signOut(signOutFromHomeserver: Boolean) { override suspend fun signOut(signOutFromHomeserver: Boolean, ignoreServerRequestError: Boolean) {
return signOutTask.execute(SignOutTask.Params(signOutFromHomeserver)) return signOutTask.execute(
SignOutTask.Params(
signOutFromHomeserver = signOutFromHomeserver,
ignoreServerRequestError = ignoreServerRequestError
)
)
} }
} }

View file

@ -30,7 +30,8 @@ import javax.inject.Inject
internal interface SignOutTask : Task<SignOutTask.Params, Unit> { internal interface SignOutTask : Task<SignOutTask.Params, Unit> {
data class Params( data class Params(
val signOutFromHomeserver: Boolean val signOutFromHomeserver: Boolean,
val ignoreServerRequestError: Boolean,
) )
} }
@ -59,10 +60,12 @@ internal class DefaultSignOutTask @Inject constructor(
// Ignore // Ignore
Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755") Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755")
} else { } else {
if (!params.ignoreServerRequestError) {
throw throwable throw throwable
} }
} }
} }
}
// Logout from identity server if any // Logout from identity server if any
runCatching { identityDisconnectTask.execute(Unit) } runCatching { identityDisconnectTask.execute(Unit) }

View file

@ -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.StoreRoomSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask 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.keysbackup.tasks.UpdateKeysBackupVersionTask
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.RealmCryptoStore 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.RealmCryptoStoreMigration
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
@ -246,7 +246,7 @@ internal abstract class CryptoModule {
abstract fun bindVerificationService(service: RustVerificationService): VerificationService abstract fun bindVerificationService(service: RustVerificationService): VerificationService
@Binds @Binds
abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore abstract fun bindCryptoStore(store: RustCryptoStore): IMXCommonCryptoStore
@Binds @Binds
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask

View file

@ -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.coroutines.builder.safeInvokeOnClose
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.DefaultKeysAlgorithmAndData 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.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.network.RequestSender
import org.matrix.android.sdk.internal.crypto.verification.SasVerification import org.matrix.android.sdk.internal.crypto.verification.SasVerification
import org.matrix.android.sdk.internal.crypto.verification.VerificationRequest 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 * Used for lazy migration of inboundGroupSession from EA to ER
*/ */
suspend fun importRoomKey(inbound: InboundGroupSessionHolder): Result<Unit> { suspend fun importRoomKey(inbound: MXInboundMegolmSessionWrapper): Result<Unit> {
Timber.v("Migration:: Tentative lazy migration") Timber.v("Migration:: Tentative lazy migration")
return withContext(coroutineDispatchers.io) { return withContext(coroutineDispatchers.io) {
val export = inbound.wrapper.exportKeys() val export = inbound.exportKeys()
?: return@withContext Result.failure(Exception("Failed to export key")) ?: return@withContext Result.failure(Exception("Failed to export key"))
val result = importDecryptedKeys(listOf(export), null).also { val result = importDecryptedKeys(listOf(export), null).also {
Timber.v("Migration:: Tentative lazy migration result: ${it.totalNumberOfKeys}") Timber.v("Migration:: Tentative lazy migration result: ${it.totalNumberOfKeys}")

View file

@ -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.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService 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.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.SessionScope
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.rustcomponents.sdk.crypto.EncryptionSettings import org.matrix.rustcomponents.sdk.crypto.EncryptionSettings
@ -47,7 +47,7 @@ private val loggerTag = LoggerTag("PrepareToEncryptUseCase", LoggerTag.CRYPTO)
internal class PrepareToEncryptUseCase @Inject constructor( internal class PrepareToEncryptUseCase @Inject constructor(
private val olmMachine: OlmMachine, private val olmMachine: OlmMachine,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCommonCryptoStore,
private val getRoomUserIds: GetRoomUserIdsUseCase, private val getRoomUserIds: GetRoomUserIdsUseCase,
private val requestSender: RequestSender, private val requestSender: RequestSender,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,

View file

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import android.content.Context import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.paging.PagedList import androidx.paging.PagedList
@ -26,7 +25,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers 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.model.SessionInfo
import org.matrix.android.sdk.internal.crypto.network.OutgoingRequestsProcessor 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.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.store.db.CryptoStoreAggregator
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
@ -111,7 +109,7 @@ internal class RustCryptoService @Inject constructor(
@UserId private val myUserId: String, @UserId private val myUserId: String,
@DeviceId private val deviceId: String, @DeviceId private val deviceId: String,
// the crypto store // the crypto store
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCommonCryptoStore,
// Set of parameters used to configure/customize the end-to-end crypto. // Set of parameters used to configure/customize the end-to-end crypto.
private val mxCryptoConfig: MXCryptoConfig, private val mxCryptoConfig: MXCryptoConfig,
// Actions // Actions
@ -185,12 +183,13 @@ internal class RustCryptoService @Inject constructor(
override fun getCryptoVersion(context: Context, longFormat: Boolean): String { override fun getCryptoVersion(context: Context, longFormat: Boolean): String {
val version = org.matrix.rustcomponents.sdk.crypto.version() 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() 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 { override suspend fun getMyCryptoDevice(): CryptoDeviceInfo = withContext(coroutineDispatchers.io) {
return runBlocking { olmMachine.ownDevice() } olmMachine.ownDevice()
} }
override suspend fun fetchDevicesList(): List<DeviceInfo> { override suspend fun fetchDevicesList(): List<DeviceInfo> {
@ -342,11 +341,11 @@ internal class RustCryptoService @Inject constructor(
*/ */
override suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { override suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? {
if (userId.isEmpty() || deviceId.isNullOrEmpty()) return null 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<CryptoDeviceInfo> { override suspend fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return runBlocking { return withContext(coroutineDispatchers.io) {
olmMachine.getCryptoDeviceInfo(userId) olmMachine.getCryptoDeviceInfo(userId)
} }
} }
@ -903,13 +902,6 @@ internal class RustCryptoService @Inject constructor(
// TODO("Not yet implemented") // TODO("Not yet implemented")
} }
/* ==========================================================================================
* For test only
* ========================================================================================== */
@VisibleForTesting
val cryptoStoreForTesting = cryptoStore
companion object { companion object {
const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour
} }

View file

@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor 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.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult 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.events.model.EventType
import org.matrix.android.sdk.api.session.uia.UiaResult import org.matrix.android.sdk.api.session.uia.UiaResult
import org.matrix.android.sdk.internal.auth.registration.handleUIA 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.OlmMachine
import org.matrix.android.sdk.internal.crypto.PerSessionBackupQueryRateLimiter import org.matrix.android.sdk.internal.crypto.PerSessionBackupQueryRateLimiter
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult 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.KeysUploadResponse
import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo 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.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.ClaimOneTimeKeysForUsersDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
@ -102,7 +103,7 @@ internal class RequestSender @Inject constructor(
private val moshi: Moshi, private val moshi: Moshi,
cryptoCoroutineScope: CoroutineScope, cryptoCoroutineScope: CoroutineScope,
private val rateLimiter: PerSessionBackupQueryRateLimiter, private val rateLimiter: PerSessionBackupQueryRateLimiter,
private val inboundGroupSessionStore: InboundGroupSessionStore, private val cryptoStore: IMXCommonCryptoStore,
private val localEchoRepository: LocalEchoRepository, private val localEchoRepository: LocalEchoRepository,
private val olmMachine: Lazy<OlmMachine>, private val olmMachine: Lazy<OlmMachine>,
) { ) {
@ -266,7 +267,9 @@ internal class RequestSender @Inject constructor(
val senderKey = requestBody?.get("sender_key") as? String val senderKey = requestBody?.get("sender_key") as? String
if (roomId != null && sessionId != null) { if (roomId != null && sessionId != null) {
// try to perform a lazy migration from legacy store // 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) { if (legacy == null || olmMachine.get().importRoomKey(legacy).isFailure) {
rateLimiter.tryFromBackupIfPossible(sessionId, roomId) rateLimiter.tryFromBackupIfPossible(sessionId, roomId)
} }

View file

@ -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<OlmMachine>,
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<CryptoMetadataEntity>().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<OlmInboundGroupSessionEntity>()
.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<OutgoingKeyRequestEntity>()
.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<AuditTrailEntity>()
.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<DeviceInfo>) {
val entities = info.map { myDeviceLastSeenInfoEntityMapper.map(it) }
doRealmTransactionAsync(realmConfiguration) { realm ->
realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm()
entities.forEach {
realm.insertOrUpdate(it)
}
}
}
override fun getMyDevicesInfo(): List<DeviceInfo> {
return monarchy.fetchAllCopiedSync {
it.where<MyDeviceLastSeenInfoEntity>()
}.map {
DeviceInfo(
deviceId = it.deviceId,
lastSeenIp = it.lastSeenIp,
lastSeenTs = it.lastSeenTs,
displayName = it.displayName
)
}
}
override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> {
return monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm.where<MyDeviceLastSeenInfoEntity>()
},
{ entity -> myDeviceLastSeenInfoEntityMapper.map(entity) }
)
}
override fun getLiveMyDevicesInfo(deviceId: String): LiveData<Optional<DeviceInfo>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm.where<MyDeviceLastSeenInfoEntity>()
.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<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices = block
}
}
override fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm
.where<CryptoMetadataEntity>()
},
{
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<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices
} ?: false
}
override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm.where<CryptoRoomEntity>()
.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<CryptoRoomEntity>()
.equalTo(CryptoRoomEntityFields.ROOM_ID, roomId)
.findFirst()
?.blacklistUnverifiedDevices ?: false
}
}
}

View file

@ -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.VERIFICATION_METHOD_RECIPROCATE
import org.matrix.android.sdk.internal.crypto.model.rest.toValue import org.matrix.android.sdk.internal.crypto.model.rest.toValue
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.rustcomponents.sdk.crypto.VerificationRequestState
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject 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` // 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 // 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. // 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. // accept() will dispatch an update, no need to do it twice.
Timber.d("## Verification: Auto accepting SAS verification with $sender") Timber.d("## Verification: Auto accepting SAS verification with $sender")
transaction.accept() transaction.accept()
@ -308,7 +309,7 @@ internal class RustVerificationService @Inject constructor(
return if (request != null) { return if (request != null) {
request.acceptWithMethods(methods) request.acceptWithMethods(methods)
request.startQrCode() request.startQrCode()
request.isReady() request.innerState() is VerificationRequestState.Ready
} else { } else {
false false
} }

View file

@ -136,9 +136,9 @@ internal class VerificationRequest @AssistedInject constructor(
* concrete verification flow, i.e. we can show/scan a QR code or start emoji * concrete verification flow, i.e. we can show/scan a QR code or start emoji
* verification. * verification.
*/ */
internal fun isReady(): Boolean { // internal fun isReady(): Boolean {
return innerVerificationRequest.isReady() // return innerVerificationRequest.isReady()
} // }
/** Did we advertise that we're able to scan QR codes */ /** Did we advertise that we're able to scan QR codes */
internal fun canScanQrCodes(): Boolean { internal fun canScanQrCodes(): Boolean {

View file

@ -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.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore
class EventEditValidatorTest { class EventEditValidatorTest {
@ -62,7 +62,7 @@ class EventEditValidatorTest {
@Test @Test
fun `edit should be valid`() { fun `edit should be valid`() {
val mockCryptoStore = mockk<IMXCryptoStore>() val mockCryptoStore = mockk<IMXCommonCryptoStore>()
val validator = EventEditValidator(mockCryptoStore) val validator = EventEditValidator(mockCryptoStore)
validator validator
@ -71,7 +71,7 @@ class EventEditValidatorTest {
@Test @Test
fun `original event and replacement event must have the same sender`() { fun `original event and replacement event must have the same sender`() {
val mockCryptoStore = mockk<IMXCryptoStore>() val mockCryptoStore = mockk<IMXCommonCryptoStore>()
val validator = EventEditValidator(mockCryptoStore) val validator = EventEditValidator(mockCryptoStore)
validator validator
@ -83,7 +83,7 @@ class EventEditValidatorTest {
@Test @Test
fun `original event and replacement event must have the same room_id`() { fun `original event and replacement event must have the same room_id`() {
val mockCryptoStore = mockk<IMXCryptoStore>() val mockCryptoStore = mockk<IMXCommonCryptoStore>()
val validator = EventEditValidator(mockCryptoStore) val validator = EventEditValidator(mockCryptoStore)
validator validator
@ -101,7 +101,7 @@ class EventEditValidatorTest {
@Test @Test
fun `replacement and original events must not have a state_key property`() { fun `replacement and original events must not have a state_key property`() {
val mockCryptoStore = mockk<IMXCryptoStore>() val mockCryptoStore = mockk<IMXCommonCryptoStore>()
val validator = EventEditValidator(mockCryptoStore) val validator = EventEditValidator(mockCryptoStore)
validator validator
@ -119,8 +119,8 @@ class EventEditValidatorTest {
@Test @Test
fun `replacement event must have an new_content property`() { fun `replacement event must have an new_content property`() {
val mockCryptoStore = mockk<IMXCryptoStore> { val mockCryptoStore = mockk<IMXCommonCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk<CryptoDeviceInfo> { mockk<CryptoDeviceInfo> {
every { userId } returns "@alice:example.com" every { userId } returns "@alice:example.com"
} }
@ -157,8 +157,8 @@ class EventEditValidatorTest {
@Test @Test
fun `The original event must not itself have a rel_type of m_replace`() { fun `The original event must not itself have a rel_type of m_replace`() {
val mockCryptoStore = mockk<IMXCryptoStore> { val mockCryptoStore = mockk<IMXCommonCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk<CryptoDeviceInfo> { mockk<CryptoDeviceInfo> {
every { userId } returns "@alice:example.com" every { userId } returns "@alice:example.com"
} }
@ -207,8 +207,8 @@ class EventEditValidatorTest {
@Test @Test
fun `valid e2ee edit`() { fun `valid e2ee edit`() {
val mockCryptoStore = mockk<IMXCryptoStore> { val mockCryptoStore = mockk<IMXCommonCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk<CryptoDeviceInfo> { mockk<CryptoDeviceInfo> {
every { userId } returns "@alice:example.com" every { userId } returns "@alice:example.com"
} }
@ -224,8 +224,8 @@ class EventEditValidatorTest {
@Test @Test
fun `If the original event was encrypted, the replacement should be too`() { fun `If the original event was encrypted, the replacement should be too`() {
val mockCryptoStore = mockk<IMXCryptoStore> { val mockCryptoStore = mockk<IMXCommonCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk<CryptoDeviceInfo> { mockk<CryptoDeviceInfo> {
every { userId } returns "@alice:example.com" every { userId } returns "@alice:example.com"
} }
@ -241,12 +241,12 @@ class EventEditValidatorTest {
@Test @Test
fun `encrypted, original event and replacement event must have the same sender`() { fun `encrypted, original event and replacement event must have the same sender`() {
val mockCryptoStore = mockk<IMXCryptoStore> { val mockCryptoStore = mockk<IMXCommonCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk { mockk {
every { userId } returns "@alice:example.com" every { userId } returns "@alice:example.com"
} }
every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns every { deviceWithIdentityKey("@bob:example.com", "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns
mockk { mockk {
every { userId } returns "@bob:example.com" every { userId } returns "@bob:example.com"
} }
@ -256,7 +256,9 @@ class EventEditValidatorTest {
validator validator
.validateEdit( .validateEdit(
encryptedEvent, encryptedEvent,
encryptedEditEvent.copy().apply { encryptedEditEvent.copy(
senderId = "@bob:example.com"
).apply {
mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy( mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI" senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI"
) )
@ -269,12 +271,12 @@ class EventEditValidatorTest {
@Test @Test
fun `encrypted, sent fom a deleted device, original event and replacement event must have the same sender`() { fun `encrypted, sent fom a deleted device, original event and replacement event must have the same sender`() {
val mockCryptoStore = mockk<IMXCryptoStore> { val mockCryptoStore = mockk<IMXCommonCryptoStore> {
every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns every { deviceWithIdentityKey("@alice:example.com", "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
mockk { mockk {
every { userId } returns "@alice:example.com" every { userId } returns "@alice:example.com"
} }
every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns every { deviceWithIdentityKey(any(), "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns
null null
} }
val validator = EventEditValidator(mockCryptoStore) val validator = EventEditValidator(mockCryptoStore)
@ -288,7 +290,7 @@ class EventEditValidatorTest {
) )
} }
) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class ) shouldBeInstanceOf EventEditValidator.EditValidity.Unknown::class
validator validator
.validateEdit( .validateEdit(

View file

@ -37,7 +37,7 @@ ext.versionMinor = 6
// Note: even values are reserved for regular release, odd values for hotfix release. // 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 // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
ext.versionPatch = 1 ext.versionPatch = 2
ext.scVersion = 68 ext.scVersion = 68

View file

@ -17,6 +17,7 @@ package im.vector.app.gplay.features.settings.troubleshoot
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import im.vector.app.R 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.pushers.PushersManager
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.settings.troubleshoot.TroubleshootTest 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 org.matrix.android.sdk.api.session.pushers.PusherState
import javax.inject.Inject import javax.inject.Inject
@ -60,6 +63,7 @@ class TestTokenRegistration @Inject constructor(
) )
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_token_registration_quick_fix) { quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_token_registration_quick_fix) {
override fun doFix() { override fun doFix() {
context.lifecycleScope.launch(Dispatchers.IO) {
val workId = pushersManager.enqueueRegisterPusherWithFcmKey(fcmToken) val workId = pushersManager.enqueueRegisterPusherWithFcmKey(fcmToken)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo -> WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo ->
if (workInfo != null) { if (workInfo != null) {
@ -72,6 +76,7 @@ class TestTokenRegistration @Inject constructor(
}) })
} }
} }
}
status = TestStatus.FAILED status = TestStatus.FAILED
} else { } else {

View file

@ -26,8 +26,11 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.DefaultPreferences 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.FcmHelper
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -38,7 +41,12 @@ import javax.inject.Inject
class GoogleFcmHelper @Inject constructor( class GoogleFcmHelper @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
@DefaultPreferences private val sharedPrefs: SharedPreferences, @DefaultPreferences private val sharedPrefs: SharedPreferences,
appScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers
) : FcmHelper { ) : FcmHelper {
private val scope = CoroutineScope(appScope.coroutineContext + coroutineDispatchers.io)
companion object { companion object {
private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
} }
@ -64,9 +72,11 @@ class GoogleFcmHelper @Inject constructor(
.addOnSuccessListener { token -> .addOnSuccessListener { token ->
storeFcmToken(token) storeFcmToken(token)
if (registerPusher) { if (registerPusher) {
scope.launch {
pushersManager.enqueueRegisterPusherWithFcmKey(token) pushersManager.enqueueRegisterPusherWithFcmKey(token)
} }
} }
}
.addOnFailureListener { e -> .addOnFailureListener { e ->
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
} }

View file

@ -27,6 +27,10 @@ import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.pushers.VectorPushHandler import im.vector.app.core.pushers.VectorPushHandler
import im.vector.app.features.settings.VectorPreferences 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 org.matrix.android.sdk.api.logger.LoggerTag
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -43,6 +47,12 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
@Inject lateinit var vectorPushHandler: VectorPushHandler @Inject lateinit var vectorPushHandler: VectorPushHandler
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper @Inject lateinit var unifiedPushHelper: UnifiedPushHelper
private val scope = CoroutineScope(SupervisorJob())
override fun onDestroy() {
scope.cancel()
super.onDestroy()
}
override fun onNewToken(token: String) { override fun onNewToken(token: String) {
Timber.tag(loggerTag.value).d("New Firebase token") Timber.tag(loggerTag.value).d("New Firebase token")
fcmHelper.storeFcmToken(token) fcmHelper.storeFcmToken(token)
@ -51,9 +61,11 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
activeSessionHolder.hasActiveSession() && activeSessionHolder.hasActiveSession() &&
unifiedPushHelper.isEmbeddedDistributor() unifiedPushHelper.isEmbeddedDistributor()
) { ) {
scope.launch {
pushersManager.enqueueRegisterPusher(token, getString(R.string.pusher_http_url)) pushersManager.enqueueRegisterPusher(token, getString(R.string.pusher_http_url))
} }
} }
}
override fun onMessageReceived(message: RemoteMessage) { override fun onMessageReceived(message: RemoteMessage) {
Timber.tag(loggerTag.value).d("New Firebase message") Timber.tag(loggerTag.value).d("New Firebase message")

View file

@ -313,7 +313,7 @@ dependencies {
// Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868 // 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) // 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. //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.testCore
androidTestImplementation libs.androidx.testRunner androidTestImplementation libs.androidx.testRunner

View file

@ -22,14 +22,14 @@ import javax.inject.Inject
interface GetDeviceInfoUseCase { interface GetDeviceInfoUseCase {
fun execute(): CryptoDeviceInfo suspend fun execute(): CryptoDeviceInfo
} }
class DefaultGetDeviceInfoUseCase @Inject constructor( class DefaultGetDeviceInfoUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder private val activeSessionHolder: ActiveSessionHolder
) : GetDeviceInfoUseCase { ) : GetDeviceInfoUseCase {
override fun execute(): CryptoDeviceInfo { override suspend fun execute(): CryptoDeviceInfo {
return activeSessionHolder.getActiveSession().cryptoService().getMyCryptoDevice() return activeSessionHolder.getActiveSession().cryptoService().getMyCryptoDevice()
} }
} }

View file

@ -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)) return enqueueRegisterPusher(pushKey, stringProvider.getString(R.string.pusher_http_url))
} }
fun enqueueRegisterPusher( suspend fun enqueueRegisterPusher(
pushKey: String, pushKey: String,
gateway: String gateway: String
): UUID { ): UUID {
@ -62,7 +62,7 @@ class PushersManager @Inject constructor(
return currentSession.pushersService().enqueueAddHttpPusher(pusher) return currentSession.pushersService().enqueueAddHttpPusher(pusher)
} }
private fun createHttpPusher( private suspend fun createHttpPusher(
pushKey: String, pushKey: String,
gateway: String gateway: String
) = HttpPusher( ) = HttpPusher(

View file

@ -76,10 +76,12 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
coroutineScope.launch { coroutineScope.launch {
unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) {
unifiedPushHelper.getPushGateway()?.let { unifiedPushHelper.getPushGateway()?.let {
coroutineScope.launch {
pushersManager.enqueueRegisterPusher(endpoint, it) pushersManager.enqueueRegisterPusher(endpoint, it)
} }
} }
} }
}
} else { } else {
Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") Timber.tag(loggerTag.value).i("onNewEndpoint: skipped")
} }

View file

@ -60,6 +60,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.session.Session
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -262,18 +263,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
} }
} }
args.clearCredentials -> { args.clearCredentials -> {
lifecycleScope.launch { signout(session, onboardingStore, ignoreServerError = false)
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()
}
} }
args.clearCache -> { args.clearCache -> {
lifecycleScope.launch { lifecycleScope.launch {
@ -286,6 +276,26 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), 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) { override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
// No op here // No op here
Timber.w("Ignoring invalid token global error") Timber.w("Ignoring invalid token global error")
@ -313,12 +323,20 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
} }
} }
private fun displayError(failure: Throwable) { private fun displaySignOutFailedDialog(
session: Session,
onboardingStore: VectorSessionStore,
) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_title_error) .setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure)) .setMessage(R.string.sign_out_failed_dialog_message)
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() } .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) } .setNegativeButton(R.string.action_cancel) { _, _ -> startNextActivityAndFinish(ignoreClearCredentials = true) }
.setCancelable(false) .setCancelable(false)
.show() .show()

View file

@ -275,7 +275,7 @@ class SelfVerificationController @Inject constructor(
id("notice_div") id("notice_div")
} }
// Option to verify with another device // Option to verify with another device
if (state.hasAnyOtherSession) { if (state.hasAnyOtherSession.invoke() == true) {
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("start") id("start")
title(host.stringProvider.getString(R.string.verification_verify_with_another_device)) title(host.stringProvider.getString(R.string.verification_verify_with_another_device))

View file

@ -83,7 +83,7 @@ data class SelfVerificationViewState(
val transactionId: String? = null, val transactionId: String? = null,
val currentDeviceCanCrossSign: Boolean = false, val currentDeviceCanCrossSign: Boolean = false,
val userWantsToCancel: Boolean = false, val userWantsToCancel: Boolean = false,
val hasAnyOtherSession: Boolean = false, val hasAnyOtherSession: Async<Boolean> = Uninitialized,
val quadSContainsSecrets: Boolean = false, val quadSContainsSecrets: Boolean = false,
val isVerificationRequired: Boolean = false, val isVerificationRequired: Boolean = false,
val isThisSessionVerified: Boolean = false, val isThisSessionVerified: Boolean = false,
@ -146,21 +146,28 @@ class SelfVerificationViewModel @AssistedInject constructor(
} }
} }
setState { copy(hasAnyOtherSession = Loading()) }
viewModelScope.launch {
val hasAnyOtherSession = session.cryptoService() val hasAnyOtherSession = session.cryptoService()
.getCryptoDeviceInfo(session.myUserId) .getCryptoDeviceInfo(session.myUserId)
.any { .any {
it.deviceId != session.sessionParams.deviceId it.deviceId != session.sessionParams.deviceId
} }
setState {
copy(
hasAnyOtherSession = Success(hasAnyOtherSession)
)
}
}
setState { setState {
copy( copy(
currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(), currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(),
quadSContainsSecrets = session.sharedSecretStorageService().isRecoverySetup(), quadSContainsSecrets = session.sharedSecretStorageService().isRecoverySetup(),
hasAnyOtherSession = hasAnyOtherSession
) )
} }
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
val isThisSessionVerified = session.cryptoService().crossSigningService().isCrossSigningVerified() val isThisSessionVerified = session.cryptoService().crossSigningService().isCrossSigningVerified()
setState { setState {
copy( copy(

View file

@ -92,11 +92,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
} }
init { 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( combine(
session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveUserCryptoDevices(session.myUserId),
@ -108,6 +103,12 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
deleteUnusedClientInformation(infoList) 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 infoList
.asSequence() .asSequence()
.filter { .filter {

View file

@ -561,6 +561,8 @@ class MessageItemFactory @Inject constructor(
// Furthermore, its corner transformations are wrong when not using the animated case for rendering. // Furthermore, its corner transformations are wrong when not using the animated case for rendering.
val forcePlay = messageContent.mimeType == MimeTypes.Webp val forcePlay = messageContent.mimeType == MimeTypes.Webp
val playable = messageContent.mimeType == MimeTypes.Gif || forcePlay val playable = messageContent.mimeType == MimeTypes.Gif || forcePlay
// don't show play button because detecting animated webp isn't possible via mimetype
val playableIfAutoplay = playable || messageContent.mimeType == MimeTypes.Webp
return MessageImageVideoItem_() return MessageImageVideoItem_()
.attributes(attributes.takeUnless { forcePlay && !it.autoplayAnimatedImages } ?: attributes.copy(autoplayAnimatedImages = true)) .attributes(attributes.takeUnless { forcePlay && !it.autoplayAnimatedImages } ?: attributes.copy(autoplayAnimatedImages = true))
@ -587,7 +589,7 @@ class MessageItemFactory @Inject constructor(
} }
} }
}.apply { }.apply {
if (playable && vectorPreferences.autoplayAnimatedImages()) { if (playableIfAutoplay && vectorPreferences.autoplayAnimatedImages()) {
mode(ImageContentRenderer.Mode.ANIMATED_THUMBNAIL) mode(ImageContentRenderer.Mode.ANIMATED_THUMBNAIL)
} }
} }

View file

@ -49,7 +49,7 @@ class DataAttachmentRoomProvider(
return getItem(position).let { return getItem(position).let {
when (it) { when (it) {
is ImageContentRenderer.Data -> { is ImageContentRenderer.Data -> {
if (it.mimeType == MimeTypes.Gif) { if (it.mimeType == MimeTypes.Gif || it.mimeType == MimeTypes.Webp) {
AttachmentInfo.AnimatedImage( AttachmentInfo.AnimatedImage(
uid = it.eventId, uid = it.eventId,
url = it.url ?: "", url = it.url ?: "",

View file

@ -184,12 +184,12 @@ class ImageContentRenderer @Inject constructor(
request = if (animate && mode == Mode.ANIMATED_THUMBNAIL) { request = if (animate && mode == Mode.ANIMATED_THUMBNAIL) {
// Glide seems to already do some dp to px calculation for animated gifs? // Glide seems to already do some dp to px calculation for animated gifs?
val animatedCornerTransformation = RoundedCorners(cornerRoundnessDp) val animatedCornerTransformation = RoundedCorners(cornerRoundnessDp)
request.transform(animatedCornerTransformation) request.optionalTransform(animatedCornerTransformation)
.transform(WebpDrawable::class.java, WebpDrawableTransformation(animatedCornerTransformation)) .transform(WebpDrawable::class.java, WebpDrawableTransformation(animatedCornerTransformation))
//request.apply(RequestOptions.bitmapTransform(RoundedCorners(3))) //request.apply(RequestOptions.bitmapTransform(RoundedCorners(3)))
} else { } else {
request.dontAnimate() request.dontAnimate()
.transform(cornerTransformation) .optionalTransform(cornerTransformation)
} }
request request
.into(imageView) .into(imageView)
@ -223,7 +223,7 @@ class ImageContentRenderer @Inject constructor(
} }
req req
.fitCenter() .optionalFitCenter()
.into(target) .into(target)
} }
@ -267,7 +267,7 @@ class ImageContentRenderer @Inject constructor(
return false return false
} }
}) })
.fitCenter() .optionalFitCenter()
.into(imageView) .into(imageView)
} }

View file

@ -74,7 +74,7 @@ class RoomEventsAttachmentProvider(
allowNonMxcUrls = it.root.sendState.isSending() allowNonMxcUrls = it.root.sendState.isSending()
) )
if (content.mimeType == MimeTypes.Gif) { if (content.mimeType == MimeTypes.Gif || content.mimeType == MimeTypes.Webp) {
AttachmentInfo.AnimatedImage( AttachmentInfo.AnimatedImage(
uid = it.eventId, uid = it.eventId,
url = content.url ?: "", url = content.url ?: "",

View file

@ -17,6 +17,7 @@
package im.vector.app.features.settings.troubleshoot package im.vector.app.features.settings.troubleshoot
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import im.vector.app.R import im.vector.app.R
@ -72,13 +73,15 @@ class TestEndpointAsTokenRegistration @Inject constructor(
} }
private fun unregisterThenRegister(testParameters: TestParameters, pushKey: String) { 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) unregisterUnifiedPushUseCase.execute(pushersManager)
registerUnifiedPush(distributor = "", testParameters, pushKey) registerUnifiedPush(distributor = "", testParameters, pushKey)
} }
} }
private fun registerUnifiedPush( private suspend fun registerUnifiedPush(
distributor: String, distributor: String,
testParameters: TestParameters, testParameters: TestParameters,
pushKey: String, pushKey: String,
@ -106,7 +109,9 @@ class TestEndpointAsTokenRegistration @Inject constructor(
pushKey: String, pushKey: String,
) { ) {
unifiedPushHelper.showSelectDistributorDialog(context) { selection -> unifiedPushHelper.showSelectDistributorDialog(context) { selection ->
context.lifecycleScope.launch {
registerUnifiedPush(distributor = selection, testParameters, pushKey) registerUnifiedPush(distributor = selection, testParameters, pushKey)
} }
} }
} }
}

View file

@ -19,6 +19,7 @@ package im.vector.app.core.device
import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeCryptoService import im.vector.app.test.fakes.FakeCryptoService
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
@ -31,7 +32,7 @@ class DefaultGetDeviceInfoUseCaseTest {
private val getDeviceInfoUseCase = DefaultGetDeviceInfoUseCase(activeSessionHolder.instance) private val getDeviceInfoUseCase = DefaultGetDeviceInfoUseCase(activeSessionHolder.instance)
@Test @Test
fun `when execute, then get crypto device info`() { fun `when execute, then get crypto device info`() = runTest {
val result = getDeviceInfoUseCase.execute() val result = getDeviceInfoUseCase.execute()
result shouldBeEqualTo cryptoService.cryptoDeviceInfo result shouldBeEqualTo cryptoService.cryptoDeviceInfo

View file

@ -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.PusherFixture
import im.vector.app.test.fixtures.SessionParamsFixture import im.vector.app.test.fixtures.SessionParamsFixture
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo
@ -56,7 +57,7 @@ class PushersManagerTest {
) )
@Test @Test
fun `when enqueueRegisterPusher, then HttpPusher created and enqueued`() { fun `when enqueueRegisterPusher, then HttpPusher created and enqueued`() = runTest {
val pushKey = "abc" val pushKey = "abc"
val gateway = "123" val gateway = "123"
val pusherAppId = "app-id" val pusherAppId = "app-id"

View file

@ -84,5 +84,5 @@ class FakeCryptoService(
} }
} }
override fun getMyCryptoDevice() = cryptoDeviceInfo override suspend fun getMyCryptoDevice() = cryptoDeviceInfo
} }

View file

@ -17,13 +17,13 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import im.vector.app.core.device.GetDeviceInfoUseCase import im.vector.app.core.device.GetDeviceInfoUseCase
import io.mockk.every import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
class FakeGetDeviceInfoUseCase : GetDeviceInfoUseCase by mockk() { class FakeGetDeviceInfoUseCase : GetDeviceInfoUseCase by mockk() {
fun givenDeviceInfo(cryptoDeviceInfo: CryptoDeviceInfo) { fun givenDeviceInfo(cryptoDeviceInfo: CryptoDeviceInfo) {
every { execute() } returns cryptoDeviceInfo coEvery { execute() } returns cryptoDeviceInfo
} }
} }