New security alert to review old sessions

This commit is contained in:
Valere 2020-04-29 11:14:57 +02:00
parent 67f07bd1bb
commit a806f70b35
24 changed files with 423 additions and 211 deletions

View file

@ -31,6 +31,7 @@ import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
@ -58,6 +59,13 @@ class RxSession(private val session: Session) {
} }
} }
fun liveMyDeviceInfo(): Observable<List<DeviceInfo>> {
return session.cryptoService().getLiveMyDeviceInfo().asObservable()
.startWithCallable {
session.cryptoService().getMyDeviceInfo()
}
}
fun liveSyncState(): Observable<SyncState> { fun liveSyncState(): Observable<SyncState> {
return session.getSyncStateLive().asObservable() return session.getSyncStateLive().asObservable()
} }

View file

@ -98,7 +98,9 @@ interface CryptoService {
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun getMyDeviceInfo() : List<DeviceInfo>
fun getLiveMyDeviceInfo() : LiveData<List<DeviceInfo>>
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)

View file

@ -251,15 +251,33 @@ internal class DefaultCryptoService @Inject constructor(
return myDeviceInfoHolder.get().myDevice return myDeviceInfoHolder.get().myDevice
} }
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) { override fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask getDevicesTask
.configureWith { .configureWith {
// this.executionThread = TaskThread.CRYPTO // this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = object : MatrixCallback<DevicesListResponse> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: DevicesListResponse) {
// Save in local DB
cryptoStore.saveMyDeviceInfos(data.devices ?: emptyList())
callback.onSuccess(data)
}
}
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getLiveMyDeviceInfo(): LiveData<List<DeviceInfo>> {
return cryptoStore.getLiveMyDeviceInfo()
}
override fun getMyDeviceInfo(): List<DeviceInfo> {
return cryptoStore.getMyDeviceInfo()
}
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) { override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
getDeviceInfoTask getDeviceInfoTask
.configureWith(GetDeviceInfoTask.Params(deviceId)) { .configureWith(GetDeviceInfoTask.Params(deviceId)) {
@ -318,6 +336,8 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
internalStart(isInitialSync) internalStart(isInitialSync)
} }
// Just update
fetchDevicesList(NoOpMatrixCallback())
} }
private suspend fun internalStart(isInitialSync: Boolean) { private suspend fun internalStart(isInitialSync: Boolean) {

View file

@ -29,7 +29,8 @@ data class CryptoDeviceInfo(
override val signatures: Map<String, Map<String, String>>? = null, override val signatures: Map<String, Map<String, String>>? = null,
val unsigned: JsonDict? = null, val unsigned: JsonDict? = null,
var trustLevel: DeviceTrustLevel? = null, var trustLevel: DeviceTrustLevel? = null,
var isBlocked: Boolean = false var isBlocked: Boolean = false,
val firsTimeSeenLocalTs: Long? = null
) : CryptoInfo { ) : CryptoInfo {
val isVerified: Boolean val isVerified: Boolean

View file

@ -61,20 +61,4 @@ internal object CryptoInfoMapper {
signatures = keyInfo.signatures signatures = keyInfo.signatures
) )
} }
fun RestDeviceInfo.toCryptoModel(): CryptoDeviceInfo {
return map(this)
}
fun CryptoDeviceInfo.toRest(): RestDeviceInfo {
return map(this)
}
// fun RestKeyInfo.toCryptoModel(): CryptoCrossSigningKey {
// return map(this)
// }
fun CryptoCrossSigningKey.toRest(): RestKeyInfo {
return map(this)
}
} }

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
@ -218,6 +219,9 @@ internal interface IMXCryptoStore {
// TODO temp // TODO temp
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>> fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
fun getMyDeviceInfo() : List<DeviceInfo>
fun getLiveMyDeviceInfo() : LiveData<List<DeviceInfo>>
fun saveMyDeviceInfos(info: List<DeviceInfo>)
/** /**
* Store the crypto algorithm for a room. * Store the crypto algorithm for a room.
* *

View file

@ -40,6 +40,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.model.toEntity import im.vector.matrix.android.internal.crypto.model.toEntity
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
@ -59,6 +60,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossiping
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
@ -287,10 +289,16 @@ internal class RealmCryptoStore @Inject constructor(
UserEntity.getOrCreate(realm, userId) UserEntity.getOrCreate(realm, userId)
.let { u -> .let { u ->
// Add the devices // Add the devices
val currentKnownDevices = u.devices.toList()
val new = devices.map { entry -> entry.value.toEntity() }
new.forEach { entity ->
// Maintain first time seen
val existing = currentKnownDevices.firstOrNull { it.deviceId == entity.deviceId && it.identityKey == entity.identityKey }
entity.firstTimeSeenLocalTs = existing?.firstTimeSeenLocalTs ?: System.currentTimeMillis()
realm.insertOrUpdate(entity)
}
// Ensure all other devices are deleted // Ensure all other devices are deleted
u.devices.deleteAllFromRealm() u.devices.deleteAllFromRealm()
val new = devices.map { entry -> entry.value.toEntity() }
new.forEach { realm.insertOrUpdate(it) }
u.devices.addAll(new) u.devices.addAll(new)
} }
} }
@ -482,6 +490,52 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun getMyDeviceInfo(): List<DeviceInfo> {
return monarchy.fetchAllCopiedSync {
it.where<MyDeviceLastSeenInfoEntity>()
}.map {
DeviceInfo(
deviceId = it.deviceId,
lastSeenIp = it.lastSeenIp,
lastSeenTs = it.lastSeenTs,
displayName = it.displayName
)
}
}
override fun getLiveMyDeviceInfo(): LiveData<List<DeviceInfo>> {
return monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm.where<MyDeviceLastSeenInfoEntity>()
},
{ entity ->
DeviceInfo(
deviceId = entity.deviceId,
lastSeenIp = entity.lastSeenIp,
lastSeenTs = entity.lastSeenTs,
displayName = entity.displayName
)
}
)
}
override fun saveMyDeviceInfos(info: List<DeviceInfo>) {
val entities = info.map {
MyDeviceLastSeenInfoEntity(
lastSeenTs = it.lastSeenTs,
lastSeenIp = it.lastSeenIp,
displayName = it.displayName,
deviceId = it.deviceId
)
}
monarchy.writeAsync { realm ->
realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm()
entities.forEach {
realm.insertOrUpdate(it)
}
}
}
override fun storeRoomAlgorithm(roomId: String, algorithm: String) { override fun storeRoomAlgorithm(roomId: String, algorithm: String) {
doRealmTransaction(realmConfiguration) { doRealmTransaction(realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.store.db
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types import com.squareup.moshi.Types
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper
@ -27,6 +28,8 @@ import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityF
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
@ -40,7 +43,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// Version 1L added Cross Signing info persistence // Version 1L added Cross Signing info persistence
companion object { companion object {
const val CRYPTO_STORE_SCHEMA_VERSION = 4L const val CRYPTO_STORE_SCHEMA_VERSION = 5L
} }
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -50,6 +53,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
if (oldVersion <= 1) migrateTo2(realm) if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -212,4 +216,23 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
} catch (failure: Throwable) { } catch (failure: Throwable) {
} }
} }
private fun migrateTo5(realm: DynamicRealm) {
realm.schema.create("MyDeviceLastSeenInfoEntity")
.addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java)
.addField(MyDeviceLastSeenInfoEntityFields.DISPLAY_NAME, String::class.java)
.addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_IP, String::class.java)
.addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java)
.setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true)
val now = System.currentTimeMillis()
realm.schema.get("DeviceInfoEntity")
?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java)
?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true)
?.transform { deviceInfoEntity ->
tryThis {
deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now)
}
}
}
} }

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEnt
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
@ -48,6 +49,7 @@ import io.realm.annotations.RealmModule
TrustLevelEntity::class, TrustLevelEntity::class,
GossipingEventEntity::class, GossipingEventEntity::class,
IncomingGossipingRequestEntity::class, IncomingGossipingRequestEntity::class,
OutgoingGossipingRequestEntity::class OutgoingGossipingRequestEntity::class,
MyDeviceLastSeenInfoEntity::class
]) ])
internal class RealmCryptoStoreModule internal class RealmCryptoStoreModule

View file

@ -104,7 +104,8 @@ object CryptoMapper {
Timber.e(failure) Timber.e(failure)
null null
} }
} },
firsTimeSeenLocalTs = deviceInfoEntity.firstTimeSeenLocalTs
) )
} }
} }

View file

@ -34,7 +34,12 @@ internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "",
var keysMapJson: String? = null, var keysMapJson: String? = null,
var signatureMapJson: String? = null, var signatureMapJson: String? = null,
var unsignedMapJson: String? = null, var unsignedMapJson: String? = null,
var trustLevelEntity: TrustLevelEntity? = null var trustLevelEntity: TrustLevelEntity? = null,
/**
* We use that to make distinction between old devices (there before mine)
* and new ones. Used for example to detect new unverified login
*/
var firstTimeSeenLocalTs: Long? = null
) : RealmObject() { ) : RealmObject() {
// // Deserialize data // // Deserialize data

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.store.db.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class MyDeviceLastSeenInfoEntity(
/**The device id*/
@PrimaryKey var deviceId: String? = null,
/** The device display name*/
var displayName: String? = null,
/** The last time this device has been seen. */
var lastSeenTs: Long? = null,
/** The last ip address*/
var lastSeenIp: String? = null
) : RealmObject() {
companion object
}

View file

@ -27,14 +27,13 @@ import im.vector.matrix.android.api.session.crypto.verification.SasVerificationT
import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.popup.DefaultVectorAlert import im.vector.riotx.features.popup.DefaultVectorAlert
import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.PopupAlertManager
@ -75,7 +74,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
session = null session = null
} }
override fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean { override fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean {
// By default riotX will not prompt if the SDK has decided that the request should not be fulfilled // By default riotX will not prompt if the SDK has decided that the request should not be fulfilled
Timber.v("## onSecretShareRequest() : Ignoring $request") Timber.v("## onSecretShareRequest() : Ignoring $request")
request.ignore?.run() request.ignore?.run()
@ -124,19 +123,11 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
// can we get more info on this device? // can we get more info on this device?
session?.cryptoService()?.getDevicesList(object : MatrixCallback<DevicesListResponse> { session?.cryptoService()?.getMyDeviceInfo()?.firstOrNull { it.deviceId == deviceId }?.let {
override fun onSuccess(data: DevicesListResponse) { postAlert(context, userId, deviceId, true, deviceInfo, it)
data.devices?.find { it.deviceId == deviceId }?.let { } ?: kotlin.run {
postAlert(context, userId, deviceId, true, deviceInfo, it) postAlert(context, userId, deviceId, true, deviceInfo)
} ?: run { }
postAlert(context, userId, deviceId, true, deviceInfo)
}
}
override fun onFailure(failure: Throwable) {
postAlert(context, userId, deviceId, true, deviceInfo)
}
})
} else { } else {
postAlert(context, userId, deviceId, false, deviceInfo) postAlert(context, userId, deviceId, false, deviceInfo)
} }

View file

@ -423,7 +423,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
// Just ignore for now // Just ignore for now
Timber.v("## Failed to restore backup after SSSS recovery") Timber.e(failure,"## Failed to restore backup after SSSS recovery")
} }
} }
} }

View file

@ -30,6 +30,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
@ -42,6 +43,7 @@ import im.vector.riotx.features.home.room.list.RoomListParams
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.popup.VerificationVectorAlert
import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
import im.vector.riotx.features.workers.signout.SignOutViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel
import kotlinx.android.synthetic.main.fragment_home_detail.* import kotlinx.android.synthetic.main.fragment_home_detail.*
import timber.log.Timber import timber.log.Timber
@ -86,43 +88,82 @@ class HomeDetailFragment @Inject constructor(
switchDisplayMode(displayMode) switchDisplayMode(displayMode)
} }
unknownDeviceDetectorSharedViewModel.subscribe { unknownDeviceDetectorSharedViewModel.subscribe { state ->
it.unknownSessions.invoke()?.let { unknownDevices -> state.unknownSessions.invoke()?.let { unknownDevices ->
Timber.v("## Detector - ${unknownDevices.size} Unknown sessions") if (state.canCrossSign && unknownDevices.isNotEmpty()) {
unknownDevices.forEachIndexed { index, deviceInfo -> Timber.v("## Detector - ${unknownDevices.size} Unknown sessions")
Timber.v("## Detector - #$index deviceId:${deviceInfo.second.deviceId} lastSeenTs:${deviceInfo.second.lastSeenTs}") unknownDevices.forEachIndexed { index, detectionInfo ->
} Timber.v("## Detector - #$index deviceId:${detectionInfo.deviceInfo.deviceId} lastSeenTs:${detectionInfo.deviceInfo.lastSeenTs} is New: ${detectionInfo.isNew}")
val uid = "Newest_Device" }
alertManager.cancelAlert(uid) val uid = "review_login"
if (it.canCrossSign && unknownDevices.isNotEmpty()) { alertManager.cancelAlert(uid)
val newest = unknownDevices.first().second val olderUnverified = unknownDevices.filter { !it.isNew }
val user = unknownDevices.first().first val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo
alertManager.postVectorAlert( if (newest != null) {
VerificationVectorAlert( promptForNewUnknownDevices(uid, state, newest)
uid = uid, } else if (olderUnverified.isNotEmpty()) {
title = getString(R.string.new_session), // In this case we prompt to go to settings to review logins
description = getString(R.string.new_session_review_with_info, newest.displayName ?: "", newest.deviceId ?: ""), promptToReviewChanges(uid, state, olderUnverified.map { it.deviceInfo })
iconId = R.drawable.ic_shield_warning }
).apply {
matrixItem = user
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)
?.navigator
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId ?: "")
)
}
}
)
} }
} }
} }
} }
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
val user = state.myMatrixItem
alertManager.postVectorAlert(
VerificationVectorAlert(
uid = uid,
title = getString(R.string.new_session),
description = getString(R.string.new_session_review_with_info, user?.displayName ?: "", newest.deviceId ?: ""),
iconId = R.drawable.ic_shield_warning
).apply {
matrixItem = user
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)
?.navigator
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
)
}
}
)
}
private fun promptToReviewChanges(uid: String, state: UnknownDevicesState, oldUnverified: List<DeviceInfo>) {
val user = state.myMatrixItem
alertManager.postVectorAlert(
VerificationVectorAlert(
uid = uid,
title = getString(R.string.review_logins),
description = getString(R.string.verify_other_sessions),
iconId = R.drawable.ic_shield_warning
).apply {
matrixItem = user
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
// mark as ignored to avoid showing it again
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
)
it.navigator.openSettings(it, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
}
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
)
}
}
)
}
private fun onGroupChange(groupSummary: GroupSummary?) { private fun onGroupChange(groupSummary: GroupSummary?) {
groupSummary?.let { groupSummary?.let {
// Use GlideApp with activity context to avoid the glideRequests to be paused // Use GlideApp with activity context to avoid the glideRequests to be paused

View file

@ -22,27 +22,35 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.matrix.rx.singleBuilder
import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
data class UnknownDevicesState( data class UnknownDevicesState(
val unknownSessions: Async<List<Pair<MatrixItem?, DeviceInfo>>> = Uninitialized, val myMatrixItem: MatrixItem.UserItem? = null,
val unknownSessions: Async<List<DeviceDetectionInfo>> = Uninitialized,
val canCrossSign: Boolean = false val canCrossSign: Boolean = false
) : MvRxState ) : MvRxState
data class DeviceDetectionInfo(
val deviceInfo: DeviceInfo,
val isNew: Boolean
)
class UnknownDeviceDetectorSharedViewModel( class UnknownDeviceDetectorSharedViewModel(
session: Session, session: Session,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
@ -50,72 +58,97 @@ class UnknownDeviceDetectorSharedViewModel(
: VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) { : VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
sealed class Action : VectorViewModelAction { sealed class Action : VectorViewModelAction {
data class IgnoreDevice(val deviceId: String) : Action() data class IgnoreDevice(val deviceIds: List<String>) : Action()
} }
val ignoredDeviceList = ArrayList<String>() private val ignoredDeviceList = ArrayList<String>()
init { init {
val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId).firstOrNull {
it.deviceId == session.sessionParams.credentials.deviceId
}?.firsTimeSeenLocalTs ?: System.currentTimeMillis()
Timber.v("## Detector - Current Session first time seen $currentSessionTs")
ignoredDeviceList.addAll( ignoredDeviceList.addAll(
vectorPreferences.getUnknownDeviceDismissedList().also { vectorPreferences.getUnknownDeviceDismissedList().also {
Timber.v("## Detector - Remembered ignored list $it") Timber.v("## Detector - Remembered ignored list $it")
} }
) )
session.rx().liveUserCryptoDevices(session.myUserId)
.debounce(600, TimeUnit.MILLISECONDS) Observable.combineLatest<List<CryptoDeviceInfo>, List<DeviceInfo>, List<DeviceDetectionInfo>>(
session.rx().liveUserCryptoDevices(session.myUserId),
session.rx().liveMyDeviceInfo(),
BiFunction { cryptoList, infoList ->
infoList
.filter { info ->
// filter verified session, by checking the crypto device info
cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse()
}
// filter out ignored devices
.filter { !ignoredDeviceList.contains(it.deviceId) }
.sortedByDescending { it.lastSeenTs }
.map { deviceInfo ->
val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firsTimeSeenLocalTs ?: 0
DeviceDetectionInfo(
deviceInfo,
deviceKnownSince > currentSessionTs + 60_000 // short window to avoid false positive
)
// .also {
// Timber.v("## Detector - first seen Difference with ${deviceInfo.deviceId} is ${deviceKnownSince - currentSessionTs}")
// }
}
}
)
.distinct() .distinct()
.switchMap { deviceList ->
Timber.v("## Detector - ============================")
Timber.v("## Detector - Crypto device update ${deviceList.map { "${it.deviceId} : ${it.isVerified}" }}")
singleBuilder<DevicesListResponse> {
session.cryptoService().getDevicesList(it)
NoOpCancellable
}.map { resp ->
// Timber.v("## Detector - Device Infos ${resp.devices?.map { "${it.deviceId} : lastSeen:${it.lastSeenTs}" }}")
resp.devices?.filter { info ->
deviceList.firstOrNull { info.deviceId == it.deviceId }?.let {
!it.isVerified
} ?: false
}
?.sortedByDescending { it.lastSeenTs }
?.map {
session.getUser(it.user_id ?: "")?.toMatrixItem() to it
}
?.filter { !ignoredDeviceList.contains(it.second.deviceId) }
?: emptyList()
}
.toObservable()
}
.execute { async -> .execute { async ->
copy(unknownSessions = async) copy(
myMatrixItem = session.getUser(session.myUserId)?.toMatrixItem(),
unknownSessions = async
)
} }
session.rx().liveUserCryptoDevices(session.myUserId)
.distinct()
.throttleLast(5_000, TimeUnit.MILLISECONDS)
.subscribe {
// If we have a new crypto device change, we might want to trigger refresh of device info
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
}.disposeOnClear()
session.rx().liveCrossSigningInfo(session.myUserId) session.rx().liveCrossSigningInfo(session.myUserId)
.execute { .execute {
copy(canCrossSign = session.cryptoService().crossSigningService().canCrossSign()) copy(canCrossSign = session.cryptoService().crossSigningService().canCrossSign())
} }
// trigger a refresh of lastSeen / last Ip
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
} }
override fun handle(action: Action) { override fun handle(action: Action) {
when (action) { when (action) {
is Action.IgnoreDevice -> { is Action.IgnoreDevice -> {
ignoredDeviceList.addAll(action.deviceIds)
// local echo // local echo
withState { state -> withState { state ->
state.unknownSessions.invoke()?.let { state.unknownSessions.invoke()?.let { detectedSessions ->
val updated = it.filter { it.second.deviceId != action.deviceId } val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) }
setState { setState {
copy(unknownSessions = Success(updated)) copy(unknownSessions = Success(updated))
} }
} }
} }
ignoredDeviceList.add(action.deviceId)
vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList)
} }
} }
} }
override fun onCleared() {
vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList)
super.onCleared()
}
companion object : MvRxViewModelFactory<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> { companion object : MvRxViewModelFactory<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? { override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state) return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state)

View file

@ -26,6 +26,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import kotlinx.android.synthetic.main.activity_vector_settings.* import kotlinx.android.synthetic.main.activity_vector_settings.*
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -58,11 +59,16 @@ class VectorSettingsActivity : VectorBaseActivity(),
if (isFirstCreation()) { if (isFirstCreation()) {
// display the fragment // display the fragment
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG) replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG) replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
else -> EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS ->
replaceFragment(R.id.vector_settings_page,
VectorSettingsDevicesFragment::class.java,
null,
FRAGMENT_TAG)
else ->
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
} }
} }
@ -130,6 +136,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
const val EXTRA_DIRECT_ACCESS_ROOT = 0 const val EXTRA_DIRECT_ACCESS_ROOT = 0
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1 const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2 const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS = 3
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
} }

View file

@ -412,7 +412,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
refreshCryptographyPreference(it) refreshCryptographyPreference(it)
} }
// TODO Move to a ViewModel... // TODO Move to a ViewModel...
session.cryptoService().getDevicesList(object : MatrixCallback<DevicesListResponse> { session.cryptoService().fetchDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) { override fun onSuccess(data: DevicesListResponse) {
if (isAdded) { if (isAdded) {
refreshCryptographyPreference(data.devices ?: emptyList()) refreshCryptographyPreference(data.devices ?: emptyList())

View file

@ -16,17 +16,14 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@ -40,7 +37,7 @@ data class DeviceVerificationInfoBottomSheetViewState(
val deviceInfo: Async<DeviceInfo> = Uninitialized, val deviceInfo: Async<DeviceInfo> = Uninitialized,
val hasAccountCrossSigning: Boolean = false, val hasAccountCrossSigning: Boolean = false,
val accountCrossSigningIsTrusted: Boolean = false, val accountCrossSigningIsTrusted: Boolean = false,
val isMine : Boolean = false val isMine: Boolean = false
) : MvRxState ) : MvRxState
class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState, class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState,
@ -79,22 +76,18 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As
isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId
) )
} }
setState { setState {
copy(deviceInfo = Loading()) copy(deviceInfo = Loading())
} }
session.cryptoService().getDeviceInfo(deviceId, object : MatrixCallback<DeviceInfo> {
override fun onSuccess(data: DeviceInfo) {
setState {
copy(deviceInfo = Success(data))
}
}
override fun onFailure(failure: Throwable) { session.rx().liveMyDeviceInfo()
setState { .map { devices ->
copy(deviceInfo = Fail(failure)) devices.firstOrNull { it.deviceId == deviceId } ?: DeviceInfo(deviceId = deviceId)
}
.execute {
copy(deviceInfo = it)
} }
}
})
} }
companion object : MvRxViewModelFactory<DeviceVerificationInfoBottomSheetViewModel, DeviceVerificationInfoBottomSheetViewState> { companion object : MvRxViewModelFactory<DeviceVerificationInfoBottomSheetViewModel, DeviceVerificationInfoBottomSheetViewState> {

View file

@ -20,7 +20,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
sealed class DevicesAction : VectorViewModelAction { sealed class DevicesAction : VectorViewModelAction {
object Retry : DevicesAction() object Refresh : DevicesAction()
data class Delete(val deviceId: String) : DevicesAction() data class Delete(val deviceId: String) : DevicesAction()
data class Password(val password: String) : DevicesAction() data class Password(val password: String) : DevicesAction()
data class Rename(val deviceId: String, val newName: String) : DevicesAction() data class Rename(val deviceId: String, val newName: String) : DevicesAction()

View file

@ -83,7 +83,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
legacyMode: Boolean, legacyMode: Boolean,
currentSessionCrossTrusted: Boolean) { currentSessionCrossTrusted: Boolean) {
devices devices
.firstOrNull() { .firstOrNull {
it.deviceId == myDeviceId it.deviceId == myDeviceId
}?.let { deviceInfo -> }?.let { deviceInfo ->

View file

@ -29,6 +29,7 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
@ -39,14 +40,13 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTxSt
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
data class DevicesViewState( data class DevicesViewState(
val myDeviceId: String = "", val myDeviceId: String = "",
@ -60,9 +60,8 @@ data class DevicesViewState(
class DevicesViewModel @AssistedInject constructor( class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState, @Assisted initialState: DevicesViewState,
private val session: Session, private val session: Session
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider) ) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
: VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -82,12 +81,15 @@ class DevicesViewModel @AssistedInject constructor(
private var _currentDeviceId: String? = null private var _currentDeviceId: String? = null
private var _currentSession: String? = null private var _currentSession: String? = null
private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
init { init {
setState { setState {
copy( copy(
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null, hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified() accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(),
myDeviceId = session.sessionParams.credentials.deviceId ?: ""
) )
} }
session.rx().liveCrossSigningInfo(session.myUserId) session.rx().liveCrossSigningInfo(session.myUserId)
@ -97,16 +99,29 @@ class DevicesViewModel @AssistedInject constructor(
accountCrossSigningIsTrusted = it.invoke()?.get()?.isTrusted() == true accountCrossSigningIsTrusted = it.invoke()?.get()?.isTrusted() == true
) )
} }
refreshDevicesList()
session.cryptoService().verificationService().addListener(this) session.cryptoService().verificationService().addListener(this)
session.rx().liveMyDeviceInfo()
.execute {
copy(
devices = it
)
}
session.rx().liveUserCryptoDevices(session.myUserId) session.rx().liveUserCryptoDevices(session.myUserId)
.execute { .execute {
copy( copy(
cryptoDevices = it cryptoDevices = it
) )
} }
refreshPublisher.throttleFirst(4_000, TimeUnit.MILLISECONDS)
.subscribe {
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
session.cryptoService().downloadKeys(listOf(session.myUserId), true, NoOpMatrixCallback())
}
.disposeOnClear()
// then force download
queryRefreshDevicesList()
} }
override fun onCleared() { override fun onCleared() {
@ -116,7 +131,7 @@ class DevicesViewModel @AssistedInject constructor(
override fun transactionUpdated(tx: VerificationTransaction) { override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.state == VerificationTxState.Verified) { if (tx.state == VerificationTxState.Verified) {
refreshDevicesList() queryRefreshDevicesList()
} }
} }
@ -125,75 +140,68 @@ class DevicesViewModel @AssistedInject constructor(
* The devices list is the list of the devices where the user is logged in. * The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers. * It can be any mobile devices, and any browsers.
*/ */
private fun refreshDevicesList() { private fun queryRefreshDevicesList() {
if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) { refreshPublisher.onNext(Unit)
// display something asap
val localKnown = session.cryptoService().getUserDevices(session.myUserId).map {
DeviceInfo(
user_id = session.myUserId,
deviceId = it.deviceId,
displayName = it.displayName()
)
}
setState { // if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) {
copy( // // display something asap
// Keep known list if we have it, and let refresh go in backgroung // val localKnown = session.cryptoService().getUserDevices(session.myUserId).map {
devices = this.devices.takeIf { it is Success } ?: Success(localKnown) // DeviceInfo(
) // user_id = session.myUserId,
} // deviceId = it.deviceId,
// displayName = it.displayName()
session.cryptoService().getDevicesList(object : MatrixCallback<DevicesListResponse> { // )
override fun onSuccess(data: DevicesListResponse) { // }
setState { //
copy( // setState {
myDeviceId = session.sessionParams.credentials.deviceId ?: "", // copy(
devices = Success(data.devices.orEmpty()) // // Keep known list if we have it, and let refresh go in backgroung
) // devices = this.devices.takeIf { it is Success } ?: Success(localKnown)
} // )
} // }
//
override fun onFailure(failure: Throwable) { // session.cryptoService().fetchDevicesList(object : MatrixCallback<DevicesListResponse> {
setState { // override fun onSuccess(data: DevicesListResponse) {
copy( // setState {
devices = Fail(failure) // copy(
) // myDeviceId = session.sessionParams.credentials.deviceId ?: "",
} // devices = Success(data.devices.orEmpty())
} // )
}) // }
// }
// Put cached state //
setState { // override fun onFailure(failure: Throwable) {
copy( // setState {
myDeviceId = session.sessionParams.credentials.deviceId ?: "", // copy(
cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId)) // devices = Fail(failure)
) // )
} // }
// }
// then force download // })
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { //
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) { // // Put cached state
setState { // setState {
copy( // copy(
cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId)) // myDeviceId = session.sessionParams.credentials.deviceId ?: "",
) // cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId))
} // )
} // }
}) //
} else { //
// Should not happen // } else {
} // // Should not happen
// }
} }
override fun handle(action: DevicesAction) { override fun handle(action: DevicesAction) {
return when (action) { return when (action) {
is DevicesAction.Retry -> refreshDevicesList() is DevicesAction.Refresh -> queryRefreshDevicesList()
is DevicesAction.Delete -> handleDelete(action) is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action) is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action) is DevicesAction.Rename -> handleRename(action)
is DevicesAction.PromptRename -> handlePromptRename(action) is DevicesAction.PromptRename -> handlePromptRename(action)
is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action) is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action)
is DevicesAction.CompleteSecurity -> handleCompleteSecurity() is DevicesAction.CompleteSecurity -> handleCompleteSecurity()
is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action) is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action)
is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action) is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action)
} }
@ -253,7 +261,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -324,7 +332,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
}) })
} }
@ -353,7 +361,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {

View file

@ -102,8 +102,8 @@ class VectorSettingsDevicesFragment @Inject constructor(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage) (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage)
viewModel.handle(DevicesAction.Refresh)
} }
override fun onDeviceClicked(deviceInfo: DeviceInfo) { override fun onDeviceClicked(deviceInfo: DeviceInfo) {
@ -122,7 +122,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
// } // }
override fun retry() { override fun retry() {
viewModel.handle(DevicesAction.Retry) viewModel.handle(DevicesAction.Refresh)
} }
/** /**

View file

@ -6,7 +6,8 @@
<!-- Sections has been created to limit merge conflicts. --> <!-- Sections has been created to limit merge conflicts. -->
<!-- BEGIN Strings added by Valere --> <!-- BEGIN Strings added by Valere -->
<string name="review_logins">Review where youre logged in</string>
<string name="verify_other_sessions">Verify your other sessions</string>
<!-- END Strings added by Valere --> <!-- END Strings added by Valere -->