Merge branch 'release/1.6.2' into main

This commit is contained in:
Valere 2023-06-02 18:02:45 +02:00
commit 9f797e49e9
59 changed files with 849 additions and 277 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)
======================================

View file

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

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="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 -->
<string name="loading">Loading…</string>

View file

@ -63,7 +63,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.6.1\""
buildConfigField "String", "SDK_VERSION", "\"1.6.2\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@ -216,8 +216,8 @@ dependencies {
implementation libs.google.phonenumber
rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.7")
// rustCryptoApi project(":library:rustCrypto")
rustCryptoImplementation("org.matrix.rustcomponents:crypto-android:0.3.9")
// rustCryptoApi project(":library:rustCrypto")
testImplementation libs.tests.junit
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281

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

View file

@ -256,7 +256,7 @@ internal class DefaultCryptoService @Inject constructor(
return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version
}
override fun getMyCryptoDevice(): CryptoDeviceInfo {
override suspend fun getMyCryptoDevice(): CryptoDeviceInfo {
return myDeviceInfoHolder.get().myDevice
}
@ -506,10 +506,7 @@ internal class DefaultCryptoService @Inject constructor(
null
} else {
withContext(coroutineDispatchers.io) {
cryptoStore.deviceWithIdentityKey(senderKey).takeIf {
// check that the claimed user id matches
it?.userId == userId
}
cryptoStore.deviceWithIdentityKey(userId, senderKey)
}
}
}
@ -539,7 +536,7 @@ internal class DefaultCryptoService @Inject constructor(
// .executeBy(taskExecutor)
// }
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
override suspend fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
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.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.CryptoRoomInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.model.TrailType
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmOutboundGroupSession
@ -49,7 +45,7 @@ import org.matrix.olm.OlmOutboundGroupSession
/**
* The crypto data store.
*/
internal interface IMXCryptoStore {
internal interface IMXCryptoStore : IMXCommonCryptoStore {
/**
* @return the device id
@ -78,21 +74,6 @@ internal interface IMXCryptoStore {
*/
fun getInboundGroupSessions(roomId: String): List<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.
* Default is true.
@ -123,28 +104,6 @@ internal interface IMXCryptoStore {
*/
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.
*/
@ -186,16 +145,6 @@ internal interface IMXCryptoStore {
*/
fun deleteStore()
/**
* open any existing crypto store.
*/
fun open()
/**
* Close the store.
*/
fun close()
/**
* Store the device id.
*
@ -262,14 +211,6 @@ internal interface IMXCryptoStore {
fun getLiveDeviceWithId(deviceId: String): LiveData<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.
*
@ -278,47 +219,8 @@ internal interface IMXCryptoStore {
*/
fun storeRoomAlgorithm(roomId: String, algorithm: String?)
/**
* Provides the algorithm used in a dedicated room.
*
* @param roomId the room id
* @return the algorithm, null is the room is not encrypted
*/
fun getRoomAlgorithm(roomId: String): String?
fun getRoomCryptoInfo(roomId: String): CryptoRoomInfo?
fun setAlgorithmInfo(roomId: String, encryption: EncryptionEventContent?)
/**
* This is a bit different than isRoomEncrypted.
* A room is encrypted when there is a m.room.encryption state event in the room (malformed/invalid or not).
* But the crypto layer has additional guaranty to ensure that encryption would never been reverted.
* It's defensive coding out of precaution (if ever state is reset).
*/
fun roomWasOnceEncrypted(roomId: String): Boolean
fun shouldEncryptForInvitedMembers(roomId: String): Boolean
/**
* Sets a boolean flag that will determine whether or not this device should encrypt Events for
* invited members.
*
* @param roomId the room id
* @param shouldEncryptForInvitedMembers The boolean flag
*/
fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
fun shouldShareHistory(roomId: String): Boolean
/**
* Sets a boolean flag that will determine whether or not room history (existing inbound sessions)
* will be shared to new user invites.
*
* @param roomId the room id
* @param shouldShareHistory The boolean flag
*/
fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean)
/**
* Store a session between the logged-in user and another device.
*
@ -361,15 +263,6 @@ internal interface IMXCryptoStore {
*/
fun storeInboundGroupSessions(sessions: List<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.
*
@ -552,7 +445,6 @@ internal interface IMXCryptoStore {
// fun getCrossSigningPrivateKeysFlow(): Flow<Optional<PrivateKeysInfo>>
fun getGlobalCryptoConfig(): GlobalCryptoConfig
fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig>
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo?
@ -597,14 +489,8 @@ internal interface IMXCryptoStore {
fun setDeviceKeysUploaded(uploaded: Boolean)
fun areDeviceKeysUploaded(): Boolean
fun tidyUpDataBase()
fun getOutgoingRoomKeyRequests(inStates: Set<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].
*/

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>?) {
doRealmTransaction("storeUserDevices", realmConfiguration) { realm ->
storeUserDevices(realm, userId, devices)

View file

@ -73,7 +73,7 @@ interface CryptoService {
suspend fun getUserDevices(userId: String): List<CryptoDeviceInfo>
fun getMyCryptoDevice(): CryptoDeviceInfo
suspend fun getMyCryptoDevice(): CryptoDeviceInfo
fun getGlobalBlacklistUnverifiedDevices(): Boolean
@ -130,7 +130,7 @@ interface CryptoService {
suspend fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo>
suspend fun getCryptoDeviceInfo(userId: String): 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.
* @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

@ -30,6 +30,7 @@ object MimeTypes {
const val BadJpg = "image/jpg"
const val Jpeg = "image/jpeg"
const val Gif = "image/gif"
const val Webp = "image/webp"
const val Ogg = "audio/ogg"

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

View file

@ -51,5 +51,13 @@ internal data class LoginFlow(
* See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824)
*/
@Json(name = "org.matrix.msc3824.delegated_oidc_compatibility")
val delegatedOidcCompatibilty: Boolean? = null
val delegatedOidcCompatibilty: Boolean? = null,
/**
* Whether a login flow of type m.login.token could accept a token issued using /login/get_token.
*
* See https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv1loginget_token
*/
@Json(name = "get_login_token")
val getLoginToken: Boolean? = null
)

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_THREADS_MSC3440 = "org.matrix.msc3440"
private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"
@Deprecated("The availability of stable get_login_token is now exposed as a capability and part of login flow")
private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882"
private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771"
private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773"
@ -94,7 +95,9 @@ internal fun Versions.doesServerSupportThreadUnreadNotifications(): Boolean {
return getMaxVersion() >= HomeServerVersion.v1_4_0 || (msc3771 && msc3773)
}
@Deprecated("The availability of stable get_login_token is now exposed as a capability and part of login flow")
internal fun Versions.doesServerSupportQrCodeLogin(): Boolean {
@Suppress("DEPRECATION")
return unstableFeatures?.get(FEATURE_QR_CODE_LOGIN) ?: false
}

View file

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

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.
*/
@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)

View file

@ -151,8 +151,6 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
getVersionResult.doesServerSupportThreads()
homeServerCapabilitiesEntity.canUseThreadReadReceiptsAndNotifications =
getVersionResult.doesServerSupportThreadUnreadNotifications()
homeServerCapabilitiesEntity.canLoginWithQrCode =
getVersionResult.doesServerSupportQrCodeLogin()
homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices =
getVersionResult.doesServerSupportRemoteToggleOfPushNotifications()
homeServerCapabilitiesEntity.canRedactEventWithRelations =
@ -169,10 +167,25 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
}
homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl
}
homeServerCapabilitiesEntity.canLoginWithQrCode = canLoginWithQrCode(getCapabilitiesResult, getVersionResult)
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time
}
}
private fun canLoginWithQrCode(getCapabilitiesResult: GetCapabilitiesResult?, getVersionResult: Versions?): Boolean {
// in r0 of MSC3882 an unstable feature was exposed. In stable it is done via /capabilities and /login
// in stable 1.7 a capability is exposed for the authenticated user
if (getCapabilitiesResult?.capabilities?.getLoginToken != null) {
return getCapabilitiesResult.capabilities.getLoginToken.enabled == true
}
@Suppress("DEPRECATION")
return getVersionResult?.doesServerSupportQrCodeLogin() == true
}
companion object {
// 8 hours like on Element Web
private const val MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS = 8 * 60 * 60 * 1000

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.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber
import org.matrix.android.sdk.internal.crypto.store.IMXCommonCryptoStore
import javax.inject.Inject
internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) {
internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCommonCryptoStore) {
sealed class EditValidity {
object Valid : EditValidity()
@ -53,7 +52,6 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
* If the original event was encrypted, the replacement should be too.
*/
fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity {
Timber.v("###REPLACE valide event $originalEvent replaced $replaceEvent")
// we might not know the original event at that time. In this case we can't perform the validation
// Edits should be revalidated when the original event is received
if (originalEvent == null) {
@ -80,25 +78,21 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto
val replaceDecrypted = replaceEvent.toValidDecryptedEvent()
?: return EditValidity.Unknown // UTD can't decide
val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId
val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId
if (originalEvent.senderId != replaceEvent.senderId) {
return EditValidity.Invalid("original event and replacement event must have the same sender")
}
val originalSendingDevice = originalEvent.senderId?.let { cryptoStore.deviceWithIdentityKey(it, originalDecrypted.cryptoSenderKey) }
val editSendingDevice = originalEvent.senderId?.let { cryptoStore.deviceWithIdentityKey(it, replaceDecrypted.cryptoSenderKey) }
if (originalDecrypted.getRelationContent()?.type == RelationType.REPLACE) {
return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ")
}
if (originalCryptoSenderId == null || editCryptoSenderId == null) {
if (originalSendingDevice == null || editSendingDevice == null) {
// mm what can we do? we don't know if it's cryptographically from same user?
// let valid and UI should display send by deleted device warning?
val bestEffortOriginal = originalCryptoSenderId ?: originalEvent.senderId
val bestEffortEdit = editCryptoSenderId ?: replaceEvent.senderId
if (bestEffortOriginal != bestEffortEdit) {
return EditValidity.Invalid("original event and replacement event must have the same sender")
}
} else {
if (originalCryptoSenderId != editCryptoSenderId) {
return EditValidity.Invalid("Crypto: original event and replacement event must have the same sender")
}
// maybe it's a deleted device or a not yet downloaded one?
return EditValidity.Unknown
}
if (originalDecrypted.type != replaceDecrypted.type) {

View file

@ -22,6 +22,8 @@ import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.api.util.Optional
@ -43,7 +45,8 @@ internal class DefaultReadService @AssistedInject constructor(
private val setReadMarkersTask: SetReadMarkersTask,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
@UserId private val userId: String,
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource
private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
private val matrixCoroutineDispatchers: MatrixCoroutineDispatchers,
) : ReadService {
@AssistedFactory
@ -66,7 +69,7 @@ internal class DefaultReadService @AssistedInject constructor(
setReadMarkersTask.execute(taskParams)
}
override suspend fun setReadReceipt(eventId: String, threadId: String) {
override suspend fun setReadReceipt(eventId: String, threadId: String) = withContext(matrixCoroutineDispatchers.io) {
val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) {
threadId
} else {

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

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

View file

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

View file

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

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

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

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

View file

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

View file

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

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.toValue
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.rustcomponents.sdk.crypto.VerificationRequestState
import timber.log.Timber
import javax.inject.Inject
@ -165,7 +166,7 @@ internal class RustVerificationService @Inject constructor(
// If this is a SAS verification originating from a `m.key.verification.request`
// event, we auto-accept here considering that we either initiated the request or
// accepted the request. If it's a QR code verification, just dispatch an update.
if (request.isReady() && transaction is SasVerification) {
if (request.innerState() is VerificationRequestState.Ready && transaction is SasVerification) {
// accept() will dispatch an update, no need to do it twice.
Timber.d("## Verification: Auto accepting SAS verification with $sender")
transaction.accept()
@ -308,7 +309,7 @@ internal class RustVerificationService @Inject constructor(
return if (request != null) {
request.acceptWithMethods(methods)
request.startQrCode()
request.isReady()
request.innerState() is VerificationRequestState.Ready
} else {
false
}

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
* verification.
*/
internal fun isReady(): Boolean {
return innerVerificationRequest.isReady()
}
// internal fun isReady(): Boolean {
// return innerVerificationRequest.isReady()
// }
/** Did we advertise that we're able to scan QR codes */
internal fun canScanQrCodes(): Boolean {

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

View file

@ -37,7 +37,7 @@ ext.versionMinor = 6
// Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
ext.versionPatch = 1
ext.versionPatch = 2
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'

View file

@ -17,6 +17,7 @@ package im.vector.app.gplay.features.settings.troubleshoot
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.work.WorkInfo
import androidx.work.WorkManager
import im.vector.app.R
@ -25,6 +26,8 @@ import im.vector.app.core.pushers.FcmHelper
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.settings.troubleshoot.TroubleshootTest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.pushers.PusherState
import javax.inject.Inject
@ -60,16 +63,18 @@ class TestTokenRegistration @Inject constructor(
)
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_token_registration_quick_fix) {
override fun doFix() {
val workId = pushersManager.enqueueRegisterPusherWithFcmKey(fcmToken)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo ->
if (workInfo != null) {
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
manager?.retry(testParameters)
} else if (workInfo.state == WorkInfo.State.FAILED) {
manager?.retry(testParameters)
context.lifecycleScope.launch(Dispatchers.IO) {
val workId = pushersManager.enqueueRegisterPusherWithFcmKey(fcmToken)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo ->
if (workInfo != null) {
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
manager?.retry(testParameters)
} else if (workInfo.state == WorkInfo.State.FAILED) {
manager?.retry(testParameters)
}
}
}
})
})
}
}
}

View file

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

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

View file

@ -310,7 +310,7 @@ dependencies {
// Fix issue with Jitsi. Inspired from https://github.com/android/android-test/issues/861#issuecomment-872067868
// Error was lots of `Duplicate class org.checkerframework.common.reflection.qual.MethodVal found in modules jetified-checker-3.1 (org.checkerframework:checker:3.1.1) and jetified-checker-qual-3.12.0 (org.checkerframework:checker-qual:3.12.0)
//noinspection GradleDependency Cannot use latest 3.15.0 since it required min API 26.
implementation "org.checkerframework:checker:3.34.0"
implementation "org.checkerframework:checker:3.35.0"
androidTestImplementation libs.androidx.testCore
androidTestImplementation libs.androidx.testRunner

View file

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

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

View file

@ -76,7 +76,9 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
coroutineScope.launch {
unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) {
unifiedPushHelper.getPushGateway()?.let {
pushersManager.enqueueRegisterPusher(endpoint, it)
coroutineScope.launch {
pushersManager.enqueueRegisterPusher(endpoint, it)
}
}
}
}

View file

@ -60,6 +60,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.session.Session
import timber.log.Timber
import javax.inject.Inject
@ -262,18 +263,7 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), UnlockedActivity
}
}
args.clearCredentials -> {
lifecycleScope.launch {
try {
session.signOutService().signOut(!args.isUserLoggedOut)
} catch (failure: Throwable) {
displayError(failure)
return@launch
}
Timber.w("SIGN_OUT: success, start app")
activeSessionHolder.clearActiveSession()
doLocalCleanup(clearPreferences = true, onboardingStore)
startNextActivityAndFinish()
}
signout(session, onboardingStore, ignoreServerError = false)
}
args.clearCache -> {
lifecycleScope.launch {
@ -286,6 +276,26 @@ class MainActivity : VectorBaseActivity<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) {
// No op here
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)) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure))
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
.setMessage(R.string.sign_out_failed_dialog_message)
.setPositiveButton(R.string.sign_out_anyway) { _, _ ->
signout(session, onboardingStore, ignoreServerError = true)
}
.setNeutralButton(R.string.global_retry) { _, _ ->
signout(session, onboardingStore, ignoreServerError = false)
}
.setNegativeButton(R.string.action_cancel) { _, _ -> startNextActivityAndFinish(ignoreClearCredentials = true) }
.setCancelable(false)
.show()

View file

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

View file

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

View file

@ -92,11 +92,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
}
init {
val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId)
.firstOrNull { it.deviceId == session.sessionParams.deviceId }
?.firstTimeSeenLocalTs
?: clock.epochMillis()
Timber.v("## Detector - Current Session first time seen $currentSessionTs")
combine(
session.flow().liveUserCryptoDevices(session.myUserId),
@ -108,6 +103,12 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
deleteUnusedClientInformation(infoList)
val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId)
.firstOrNull { it.deviceId == session.sessionParams.deviceId }
?.firstTimeSeenLocalTs
?: clock.epochMillis()
Timber.v("## Detector - Current Session first time seen $currentSessionTs")
infoList
.asSequence()
.filter {

View file

@ -528,6 +528,8 @@ class MessageItemFactory @Inject constructor(
)
val playable = messageContent.mimeType == MimeTypes.Gif
// don't show play button because detecting animated webp isn't possible via mimetype
val playableIfAutoplay = playable || messageContent.mimeType == MimeTypes.Webp
return MessageImageVideoItem_()
.attributes(attributes)
@ -549,7 +551,7 @@ class MessageItemFactory @Inject constructor(
}
}
}.apply {
if (playable && vectorPreferences.autoplayAnimatedImages()) {
if (playableIfAutoplay && vectorPreferences.autoplayAnimatedImages()) {
mode(ImageContentRenderer.Mode.ANIMATED_THUMBNAIL)
}
}

View file

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

View file

@ -135,7 +135,7 @@ class ImageContentRenderer @Inject constructor(
if (mode == Mode.ANIMATED_THUMBNAIL) it
else it.dontAnimate()
}
.transform(cornerTransformation)
.optionalTransform(cornerTransformation)
.into(imageView)
}
@ -167,7 +167,7 @@ class ImageContentRenderer @Inject constructor(
}
req
.fitCenter()
.optionalFitCenter()
.into(target)
}
@ -211,7 +211,7 @@ class ImageContentRenderer @Inject constructor(
return false
}
})
.fitCenter()
.optionalFitCenter()
.into(imageView)
}

View file

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

View file

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

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

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

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
import im.vector.app.core.device.GetDeviceInfoUseCase
import io.mockk.every
import io.mockk.coEvery
import io.mockk.mockk
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
class FakeGetDeviceInfoUseCase : GetDeviceInfoUseCase by mockk() {
fun givenDeviceInfo(cryptoDeviceInfo: CryptoDeviceInfo) {
every { execute() } returns cryptoDeviceInfo
coEvery { execute() } returns cryptoDeviceInfo
}
}