Merge pull request #1296 from vector-im/feature/untrusted_session_shields

Update manage sessions screen
This commit is contained in:
Valere 2020-04-28 19:17:00 +02:00 committed by GitHub
commit c02cfb2f4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 650 additions and 147 deletions

View file

@ -28,6 +28,7 @@ Improvements 🙌:
- Restart broken Olm sessions ([MSC1719](https://github.com/matrix-org/matrix-doc/pull/1719))
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
- Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
- Manage Session Settings / Cross Signing update (#1295)
Bugfix 🐛:
- Fix summary notification staying after "mark as read"

View file

@ -0,0 +1,52 @@
/*
* 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.riotx.core.dialogs
import android.app.Activity
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R
object ManuallyVerifyDialog {
fun show(activity: Activity, cryptoDeviceInfo: CryptoDeviceInfo, onVerified: (() -> Unit)) {
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_device_verify, null)
val builder = AlertDialog.Builder(activity)
.setTitle(R.string.cross_signing_verify_by_text)
.setView(dialogLayout)
.setPositiveButton(R.string.encryption_information_verify) { _, _ ->
onVerified()
}
.setNegativeButton(R.string.cancel, null)
dialogLayout.findViewById<TextView>(R.id.encrypted_device_info_device_name)?.let {
it.text = cryptoDeviceInfo.displayName()
}
dialogLayout.findViewById<TextView>(R.id.encrypted_device_info_device_id)?.let {
it.text = cryptoDeviceInfo.deviceId
}
dialogLayout.findViewById<TextView>(R.id.encrypted_device_info_device_key)?.let {
it.text = cryptoDeviceInfo.getFingerprintHumanReadable()
}
builder.show()
}
}

View file

@ -0,0 +1,65 @@
/*
* 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.riotx.core.ui.list
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.button.MaterialButton
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.themes.ThemeUtils
/**
* A generic button list item.
*/
@EpoxyModelClass(layout = R.layout.item_generic_button)
abstract class GenericButtonItem : VectorEpoxyModel<GenericButtonItem.Holder>() {
@EpoxyAttribute
var text: String? = null
@EpoxyAttribute
var itemClickAction: View.OnClickListener? = null
@EpoxyAttribute
@ColorInt
var textColor: Int? = null
@EpoxyAttribute
@DrawableRes
var iconRes: Int? = null
override fun bind(holder: Holder) {
holder.button.text = text
val textColor = textColor ?: ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_primary)
holder.button.setTextColor(textColor)
if (iconRes != null) {
holder.button.setIconResource(iconRes!!)
} else {
holder.button.icon = null
}
itemClickAction?.let { holder.view.setOnClickListener(it) }
}
class Holder : VectorEpoxyHolder() {
val button by bind<MaterialButton>(R.id.itemGenericItemButton)
}
}

View file

@ -165,7 +165,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
} else {
otherUserShield.setImageResource(R.drawable.ic_shield_warning)
}
otherUserNameText.text = getString(R.string.complete_security)
otherUserNameText.text = getString(
if (state.selfVerificationMode) R.string.crosssigning_verify_this_session else R.string.crosssigning_verify_session
)
otherUserShield.isVisible = true
} else {
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
@ -241,7 +243,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
when (state.qrTransactionState) {
is VerificationTxState.QrScannedByOther -> {
is VerificationTxState.QrScannedByOther -> {
showFragment(VerificationQrScannedByOtherFragment::class, Bundle())
return@withState
}
@ -252,19 +254,19 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
})
return@withState
}
is VerificationTxState.Verified -> {
is VerificationTxState.Verified -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
})
return@withState
}
is VerificationTxState.Cancelled -> {
is VerificationTxState.Cancelled -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, state.qrTransactionState.cancelCode.value, state.isMe))
})
return@withState
}
else -> Unit
else -> Unit
}
// At this point there is no SAS transaction for this request

View file

@ -185,8 +185,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
// We need to ask
promptSecurityEvent(
session,
R.string.complete_security,
R.string.crosssigning_verify_this_session
R.string.crosssigning_verify_this_session,
R.string.confirm_your_identity
) {
it.navigator.waitSessionVerification(it)
}

View file

@ -101,7 +101,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.verification_verify_device_manually))
title(stringProvider.getString(R.string.cross_signing_verify_by_emoji))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))

View file

@ -59,7 +59,7 @@ class CrossSigningEpoxyController @Inject constructor(
if (!data.isUploadingKeys) {
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.complete_security))
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
@ -76,7 +76,7 @@ class CrossSigningEpoxyController @Inject constructor(
}
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.complete_security))
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))

View file

@ -20,15 +20,17 @@ import android.graphics.Typeface
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DimensionConverter
import me.gujun.android.span.span
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
@ -53,22 +55,31 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
var detailedMode = false
@EpoxyAttribute
var trusted : Boolean? = null
var trusted: DeviceTrustLevel? = null
@EpoxyAttribute
var legacyMode: Boolean = false
@EpoxyAttribute
var trustedSession: Boolean = false
@EpoxyAttribute
var colorProvider: ColorProvider? = null
@EpoxyAttribute
var dimensionConverter: DimensionConverter? = null
override fun bind(holder: Holder) {
holder.root.setOnClickListener { itemClickAction?.invoke() }
if (trusted != null) {
holder.trustIcon.setImageDrawable(
ContextCompat.getDrawable(
holder.view.context,
if (trusted!!) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning
)
)
holder.trustIcon.isInvisible = false
} else {
holder.trustIcon.isInvisible = true
}
val shield = TrustUtils.shieldForTrust(
currentDevice,
trustedSession,
legacyMode,
trusted
)
holder.trustIcon.setImageResource(shield)
val detailedModeLabels = listOf(
holder.displayNameLabelText,
@ -103,7 +114,28 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
}
} else {
holder.summaryLabelText.text = deviceInfo.displayName ?: deviceInfo.deviceId ?: ""
holder.summaryLabelText.text =
span {
+(deviceInfo.displayName ?: deviceInfo.deviceId ?: "")
apply {
// Add additional info if current session is not trusted
if (!trustedSession) {
+"\n"
span {
text = "${deviceInfo.deviceId}"
apply {
colorProvider?.getColorFromAttribute(R.attr.riotx_text_secondary)?.let {
textColor = it
}
dimensionConverter?.spToPx(12)?.let {
textSize = it
}
}
}
}
}
}
holder.summaryLabelText.isVisible = true
detailedModeLabels.map {
it.isVisible = false

View file

@ -37,7 +37,10 @@ import im.vector.riotx.core.platform.VectorViewModel
data class DeviceVerificationInfoBottomSheetViewState(
val cryptoDeviceInfo: Async<CryptoDeviceInfo?> = Uninitialized,
val deviceInfo: Async<DeviceInfo> = Uninitialized
val deviceInfo: Async<DeviceInfo> = Uninitialized,
val hasAccountCrossSigning: Boolean = false,
val accountCrossSigningIsTrusted: Boolean = false,
val isMine : Boolean = false
) : MvRxState
class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState,
@ -51,13 +54,29 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As
}
init {
setState {
copy(
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified()
)
}
session.rx().liveCrossSigningInfo(session.myUserId)
.execute {
copy(
hasAccountCrossSigning = it.invoke()?.get() != null,
accountCrossSigningIsTrusted = it.invoke()?.get()?.isTrusted() == true
)
}
session.rx().liveUserCryptoDevices(session.myUserId)
.map { list ->
list.firstOrNull { it.deviceId == deviceId }
}
.execute {
copy(
cryptoDeviceInfo = it
cryptoDeviceInfo = it,
isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId
)
}
setState {

View file

@ -16,7 +16,9 @@
package im.vector.riotx.features.settings.devices
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.epoxy.loadingItem
@ -26,6 +28,7 @@ import im.vector.riotx.core.ui.list.GenericItem
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import timber.log.Timber
import javax.inject.Inject
class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
@ -37,111 +40,251 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) {
val cryptoDeviceInfo = data?.cryptoDeviceInfo?.invoke()
if (cryptoDeviceInfo != null) {
if (cryptoDeviceInfo.isVerified) {
when {
cryptoDeviceInfo != null -> {
// It's a E2E capable device
handleE2ECapableDevice(data, cryptoDeviceInfo)
}
data?.deviceInfo?.invoke() != null -> {
// It's a non E2E capable device
handleNonE2EDevice(data)
}
else -> {
loadingItem {
id("loading")
}
}
}
}
private fun handleE2ECapableDevice(data: DeviceVerificationInfoBottomSheetViewState, cryptoDeviceInfo: CryptoDeviceInfo) {
val shield = TrustUtils.shieldForTrust(
currentDevice = data.isMine,
trustMSK = data.accountCrossSigningIsTrusted,
legacyMode = !data.hasAccountCrossSigning,
deviceTrustLevel = cryptoDeviceInfo.trustLevel
)
if (data.hasAccountCrossSigning) {
// Cross Signing is enabled
handleE2EWithCrossSigning(data.isMine, data.accountCrossSigningIsTrusted, cryptoDeviceInfo, shield)
} else {
handleE2EInLegacy(data.isMine, cryptoDeviceInfo, shield)
}
// COMMON ACTIONS (Rename / signout)
addGenericDeviceManageActions(data, cryptoDeviceInfo.deviceId)
}
private fun handleE2EWithCrossSigning(isMine: Boolean, currentSessionIsTrusted: Boolean, cryptoDeviceInfo: CryptoDeviceInfo, shield: Int) {
Timber.v("handleE2EWithCrossSigning $isMine, $cryptoDeviceInfo, $shield")
if (isMine) {
if (currentSessionIsTrusted) {
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(R.drawable.ic_shield_trusted)
titleIconResourceId(shield)
title(stringProvider.getString(R.string.encryption_information_verified))
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
}
} else {
// You need tomcomplete security
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
titleIconResourceId(R.drawable.ic_shield_warning)
style(GenericItem.STYLE.BIG_TEXT)
title(stringProvider.getString(R.string.encryption_information_not_verified))
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
}
}
genericItem {
id("info${cryptoDeviceInfo.deviceId}")
title(cryptoDeviceInfo.displayName() ?: "")
description("(${cryptoDeviceInfo.deviceId})")
}
if (!cryptoDeviceInfo.isVerified) {
dividerItem {
id("d1")
}
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.verification_verify_device))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener {
callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId))
}
}
}
if (cryptoDeviceInfo.deviceId != session.sessionParams.credentials.deviceId) {
// Add the delete option
dividerItem {
id("d2")
}
bottomSheetVerificationActionItem {
id("delete")
title(stringProvider.getString(R.string.settings_active_sessions_signout_device))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
callback?.onAction(DevicesAction.Delete(cryptoDeviceInfo.deviceId))
}
}
}
dividerItem {
id("d3")
}
bottomSheetVerificationActionItem {
id("rename")
title(stringProvider.getString(R.string.rename))
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener {
callback?.onAction(DevicesAction.PromptRename(cryptoDeviceInfo.deviceId))
}
}
} else if (data?.deviceInfo?.invoke() != null) {
val info = data.deviceInfo.invoke()
genericItem {
id("info${info?.deviceId}")
title(info?.displayName ?: "")
description("(${info?.deviceId})")
}
genericFooterItem {
id("infoCrypto${info?.deviceId}")
text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info))
}
if (info?.deviceId != session.sessionParams.credentials.deviceId) {
// Add the delete option
dividerItem {
id("d2")
}
bottomSheetVerificationActionItem {
id("delete")
title(stringProvider.getString(R.string.settings_active_sessions_signout_device))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
callback?.onAction(DevicesAction.Delete(info?.deviceId ?: ""))
}
titleIconResourceId(shield)
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
description(stringProvider.getString(R.string.confirm_your_identity))
}
}
} else {
loadingItem {
id("loading")
if (!currentSessionIsTrusted) {
// we don't know if this session is trusted...
// for now we show nothing?
} else {
// we rely on cross signing status
val trust = cryptoDeviceInfo.trustLevel?.isCrossSigningVerified() == true
if (trust) {
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(shield)
title(stringProvider.getString(R.string.encryption_information_verified))
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
}
} else {
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
titleIconResourceId(shield)
style(GenericItem.STYLE.BIG_TEXT)
title(stringProvider.getString(R.string.encryption_information_not_verified))
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
}
}
}
}
// DEVICE INFO SECTION
genericItem {
id("info${cryptoDeviceInfo.deviceId}")
title(cryptoDeviceInfo.displayName() ?: "")
description("(${cryptoDeviceInfo.deviceId})")
}
if (isMine && !currentSessionIsTrusted) {
// Add complete security
dividerItem {
id("completeSecurityDiv")
}
bottomSheetVerificationActionItem {
id("completeSecurity")
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener {
callback?.onAction(DevicesAction.CompleteSecurity)
}
}
} else if (!isMine) {
if (currentSessionIsTrusted) {
// we can propose to verify it
val isVerified = cryptoDeviceInfo.trustLevel?.crossSigningVerified.orFalse()
if (!isVerified) {
addVerifyActions(cryptoDeviceInfo)
}
}
}
}
private fun handleE2EInLegacy(isMine: Boolean, cryptoDeviceInfo: CryptoDeviceInfo, shield: Int) {
// ==== Legacy
// TRUST INFO SECTION
if (cryptoDeviceInfo.trustLevel?.isLocallyVerified() == true) {
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(shield)
title(stringProvider.getString(R.string.encryption_information_verified))
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
}
} else {
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
titleIconResourceId(shield)
style(GenericItem.STYLE.BIG_TEXT)
title(stringProvider.getString(R.string.encryption_information_not_verified))
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
}
}
// DEVICE INFO SECTION
genericItem {
id("info${cryptoDeviceInfo.deviceId}")
title(cryptoDeviceInfo.displayName() ?: "")
description("(${cryptoDeviceInfo.deviceId})")
}
// ACTIONS
if (!isMine) {
// if it's not the current device you can trigger a verification
dividerItem {
id("d1")
}
bottomSheetVerificationActionItem {
id("verify${cryptoDeviceInfo.deviceId}")
title(stringProvider.getString(R.string.verification_verify_device))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener {
callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId))
}
}
}
}
private fun addVerifyActions(cryptoDeviceInfo: CryptoDeviceInfo) {
dividerItem {
id("verifyDiv")
}
bottomSheetVerificationActionItem {
id("verify_text")
title(stringProvider.getString(R.string.cross_signing_verify_by_text))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener {
callback?.onAction(DevicesAction.VerifyMyDeviceManually(cryptoDeviceInfo.deviceId))
}
}
dividerItem {
id("verifyDiv2")
}
bottomSheetVerificationActionItem {
id("verify_emoji")
title(stringProvider.getString(R.string.cross_signing_verify_by_emoji))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener {
callback?.onAction(DevicesAction.VerifyMyDevice(cryptoDeviceInfo.deviceId))
}
}
}
private fun addGenericDeviceManageActions(data: DeviceVerificationInfoBottomSheetViewState, deviceId: String) {
// Offer delete session if not me
if (!data.isMine) {
// Add the delete option
dividerItem {
id("manageD1")
}
bottomSheetVerificationActionItem {
id("delete")
title(stringProvider.getString(R.string.settings_active_sessions_signout_device))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
callback?.onAction(DevicesAction.Delete(deviceId))
}
}
}
// Always offer rename
dividerItem {
id("manageD2")
}
bottomSheetVerificationActionItem {
id("rename")
title(stringProvider.getString(R.string.rename))
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener {
callback?.onAction(DevicesAction.PromptRename(deviceId))
}
}
}
private fun handleNonE2EDevice(data: DeviceVerificationInfoBottomSheetViewState) {
val info = data.deviceInfo.invoke() ?: return
genericItem {
id("info${info.deviceId}")
title(info.displayName ?: "")
description("(${info.deviceId})")
}
genericFooterItem {
id("infoCrypto${info.deviceId}")
text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info))
}
info.deviceId?.let { addGenericDeviceManageActions(data, it) }
}
interface Callback {

View file

@ -16,6 +16,7 @@
package im.vector.riotx.features.settings.devices
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class DevicesAction : VectorViewModelAction {
@ -26,4 +27,7 @@ sealed class DevicesAction : VectorViewModelAction {
data class PromptRename(val deviceId: String) : DevicesAction()
data class VerifyMyDevice(val deviceId: String) : DevicesAction()
data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction()
object CompleteSecurity : DevicesAction()
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
}

View file

@ -22,19 +22,24 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.extensions.sortByLastSeen
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.rest.DeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericItemHeader
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Inject
class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val vectorPreferences: VectorPreferences) : EpoxyController() {
var callback: Callback? = null
@ -68,30 +73,51 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
listener { callback?.retry() }
}
is Success ->
buildDevicesList(devices(), state.cryptoDevices(), state.myDeviceId)
buildDevicesList(devices(), state.cryptoDevices(), state.myDeviceId, !state.hasAccountCrossSigning, state.accountCrossSigningIsTrusted)
}
}
private fun buildDevicesList(devices: List<DeviceInfo>, cryptoDevices: List<CryptoDeviceInfo>?, myDeviceId: String) {
// Current device
genericItemHeader {
id("current")
text(stringProvider.getString(R.string.devices_current_device))
}
private fun buildDevicesList(devices: List<DeviceInfo>,
cryptoDevices: List<CryptoDeviceInfo>?,
myDeviceId: String,
legacyMode: Boolean,
currentSessionCrossTrusted: Boolean) {
devices
.filter {
.firstOrNull() {
it.deviceId == myDeviceId
}
.forEachIndexed { idx, deviceInfo ->
}?.let { deviceInfo ->
// Current device
genericItemHeader {
id("current")
text(stringProvider.getString(R.string.devices_current_device))
}
deviceItem {
id("myDevice$idx")
id("myDevice${deviceInfo.deviceId}")
legacyMode(legacyMode)
trustedSession(currentSessionCrossTrusted)
dimensionConverter(dimensionConverter)
colorProvider(colorProvider)
detailedMode(vectorPreferences.developerMode())
deviceInfo(deviceInfo)
currentDevice(true)
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
trusted(true)
trusted(DeviceTrustLevel(currentSessionCrossTrusted, true))
}
// // If cross signing enabled and this session not trusted, add short cut to complete security
// NEED DESIGN
// if (!legacyMode && !currentSessionCrossTrusted) {
// genericButtonItem {
// id("complete_security")
// iconRes(R.drawable.ic_shield_warning)
// text(stringProvider.getString(R.string.complete_security))
// itemClickAction(DebouncedClickListener(View.OnClickListener { _ ->
// callback?.completeSecurity()
// }))
// }
// }
}
// Other devices
@ -111,11 +137,15 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
val isCurrentDevice = deviceInfo.deviceId == myDeviceId
deviceItem {
id("device$idx")
legacyMode(legacyMode)
trustedSession(currentSessionCrossTrusted)
dimensionConverter(dimensionConverter)
colorProvider(colorProvider)
detailedMode(vectorPreferences.developerMode())
deviceInfo(deviceInfo)
currentDevice(isCurrentDevice)
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
trusted(cryptoDevices?.firstOrNull { it.deviceId == deviceInfo.deviceId }?.isVerified)
trusted(cryptoDevices?.firstOrNull { it.deviceId == deviceInfo.deviceId }?.trustLevel)
}
}
}

View file

@ -17,6 +17,8 @@
package im.vector.riotx.features.settings.devices
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.rest.DeviceInfo
import im.vector.riotx.core.platform.VectorViewEvents
@ -35,4 +37,10 @@ sealed class DevicesViewEvents : VectorViewEvents {
val userId: String,
val transactionId: String?
) : DevicesViewEvents()
data class SelfVerification(
val session: Session
) : DevicesViewEvents()
data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvents()
}

View file

@ -16,6 +16,7 @@
package im.vector.riotx.features.settings.devices
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
@ -28,26 +29,33 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
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.VerificationTxState
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.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.DevicesListResponse
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
import kotlinx.coroutines.launch
data class DevicesViewState(
val myDeviceId: String = "",
val devices: Async<List<DeviceInfo>> = Uninitialized,
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized,
// TODO Replace by isLoading boolean
val request: Async<Unit> = Uninitialized
val request: Async<Unit> = Uninitialized,
val hasAccountCrossSigning: Boolean = false,
val accountCrossSigningIsTrusted: Boolean = false
) : MvRxState
class DevicesViewModel @AssistedInject constructor(
@ -75,6 +83,21 @@ class DevicesViewModel @AssistedInject constructor(
private var _currentSession: String? = null
init {
setState {
copy(
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified()
)
}
session.rx().liveCrossSigningInfo(session.myUserId)
.execute {
copy(
hasAccountCrossSigning = it.invoke()?.get() != null,
accountCrossSigningIsTrusted = it.invoke()?.get()?.isTrusted() == true
)
}
refreshDevicesList()
session.cryptoService().verificationService().addListener(this)
@ -164,25 +187,56 @@ class DevicesViewModel @AssistedInject constructor(
override fun handle(action: DevicesAction) {
return when (action) {
is DevicesAction.Retry -> refreshDevicesList()
is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action)
is DevicesAction.PromptRename -> handlePromptRename(action)
is DevicesAction.VerifyMyDevice -> handleVerify(action)
is DevicesAction.Retry -> refreshDevicesList()
is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action)
is DevicesAction.PromptRename -> handlePromptRename(action)
is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action)
is DevicesAction.CompleteSecurity -> handleCompleteSecurity()
is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action)
is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action)
}
}
private fun handleVerify(action: DevicesAction.VerifyMyDevice) {
private fun handleInteractiveVerification(action: DevicesAction.VerifyMyDevice) {
val txID = session.cryptoService()
.verificationService()
.requestKeyVerification(supportedVerificationMethodsProvider.provide(), session.myUserId, listOf(action.deviceId))
.beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId, null)
_viewEvents.post(DevicesViewEvents.ShowVerifyDevice(
session.myUserId,
txID.transactionId
txID
))
}
private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state ->
state.cryptoDevices.invoke()
?.firstOrNull { it.deviceId == action.deviceId }
?.let {
_viewEvents.post(DevicesViewEvents.ShowManuallyVerify(it))
}
}
private fun handleVerifyManually(action: DevicesAction.MarkAsManuallyVerified) = withState { state ->
viewModelScope.launch {
if (state.hasAccountCrossSigning) {
awaitCallback<Unit> {
tryThis { session.cryptoService().crossSigningService().trustDevice(action.cryptoDeviceInfo.deviceId, it) }
}
} else {
// legacy
session.cryptoService().setDeviceVerification(
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true),
action.cryptoDeviceInfo.userId,
action.cryptoDeviceInfo.deviceId)
}
}
}
private fun handleCompleteSecurity() {
_viewEvents.post(DevicesViewEvents.SelfVerification(session))
}
private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId }
if (info != null) {

View file

@ -0,0 +1,56 @@
/*
* 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.riotx.features.settings.devices
import androidx.annotation.DrawableRes
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.riotx.R
object TrustUtils {
@DrawableRes
fun shieldForTrust(currentDevice: Boolean, trustMSK: Boolean, legacyMode: Boolean, deviceTrustLevel: DeviceTrustLevel?): Int {
return when {
currentDevice -> {
if (legacyMode) {
// In legacy, current session is always trusted
R.drawable.ic_shield_trusted
} else {
// If current session doesn't trust MSK, show red shield for current device
R.drawable.ic_shield_trusted.takeIf { trustMSK } ?: R.drawable.ic_shield_warning
}
}
else -> {
if (legacyMode) {
// use local trust
R.drawable.ic_shield_trusted.takeIf { deviceTrustLevel?.locallyVerified == true } ?: R.drawable.ic_shield_warning
} else {
if (trustMSK) {
// use cross sign trust, put locally trusted in black
R.drawable.ic_shield_trusted.takeIf { deviceTrustLevel?.crossSigningVerified == true }
?: R.drawable.ic_shield_black.takeIf { deviceTrustLevel?.locallyVerified == true }
?: R.drawable.ic_shield_warning
} else {
// The current session is untrusted, so displays others in black
// as we can't know the cross-signing state
R.drawable.ic_shield_black
}
}
}
}
}
}

View file

@ -27,6 +27,7 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ManuallyVerifyDialog
import im.vector.riotx.core.dialogs.PromptPasswordDialog
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
@ -73,6 +74,15 @@ class VectorSettingsDevicesFragment @Inject constructor(
transactionId = it.transactionId
).show(childFragmentManager, "REQPOP")
}
is DevicesViewEvents.SelfVerification -> {
VerificationBottomSheet.forSelfVerification(it.session)
.show(childFragmentManager, "REQPOP")
}
is DevicesViewEvents.ShowManuallyVerify -> {
ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) {
viewModel.handle(DevicesAction.MarkAsManuallyVerified(it.cryptoDeviceInfo))
}
}
}.exhaustive
}
}

View file

@ -33,7 +33,7 @@
<LinearLayout
android:id="@+id/alerter_texts"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -68,6 +68,7 @@
android:textAppearance="@style/AlertTextAppearance.Text"
android:visibility="gone"
tools:text="Text"
android:maxLines="3"
tools:visibility="visible" />
</LinearLayout>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/itemGenericItemButton"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:textAllCaps="false"
tools:icon="@drawable/ic_shield_warning"
app:iconGravity="textStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Action Name" />
</LinearLayout>

View file

@ -1024,7 +1024,7 @@
<string name="encryption_never_send_to_unverified_devices_summary">Never send encrypted messages to unverified sessions from this session.</string>
<string name="encryption_import_room_keys_success">%1$d/%2$d key(s) imported with success.</string>
<string name="encryption_information_not_verified">NOT Verified</string>
<string name="encryption_information_not_verified">Not Verified</string>
<string name="encryption_information_verified">Verified</string>
<string name="encryption_information_blocked">Blacklisted</string>
@ -1038,8 +1038,8 @@
<string name="encryption_information_unblock">Unblacklist</string>
<string name="encryption_information_verify_device">Verify session</string>
<string name="encryption_information_verify_device_warning">To verify that this session can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this session matches the key below:</string>
<string name="encryption_information_verify_device_warning2">If it matches, press the verify button below. If it doesnt, then someone else is intercepting this session and you should probably blacklist it. In the future this verification process will be more sophisticated.</string>
<string name="encryption_information_verify_device_warning">Confirm by comparing the following with the User Settings in your other session:</string>
<string name="encryption_information_verify_device_warning2">"If they don't match, the security of your communication may be compromised."</string>
<string name="encryption_information_verify_key_match">I verify that the keys match</string>
<string name="e2e_enabling_on_app_update">Riot now supports end-to-end encryption but you need to log in again to enable it.\n\nYou can do it now or later from the application settings.</string>
@ -2110,7 +2110,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="settings_active_sessions_list">Active Sessions</string>
<string name="settings_active_sessions_show_all">Show All Sessions</string>
<string name="settings_active_sessions_manage">Manage Sessions</string>
<string name="settings_active_sessions_signout_device">Sign out this session</string>
<string name="settings_active_sessions_signout_device">Sign out of this session</string>
<string name="settings_failed_to_get_crypto_device_info">No cryptographic information available</string>
@ -2122,7 +2122,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<item quantity="other">%d active sessions</item>
</plurals>
<string name="crosssigning_verify_this_session">Verify this session</string>
<string name="crosssigning_verify_this_session">Verify this login</string>
<string name="crosssigning_other_user_not_trust">Other users may not trust it</string>
<string name="complete_security">Complete Security</string>

View file

@ -19,6 +19,12 @@
<string name="enter_secret_storage_input_key">Select your Recovery Key, or input it manually by typing it or pasting from your clipboard</string>
<string name="keys_backup_recovery_key_error_decrypt">Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key.</string>
<string name="failed_to_access_secure_storage">Failed to access secure storage</string>
<string name="cross_signing_verify_by_text">Manually Verify by Text</string>
<string name="crosssigning_verify_session">Verify login</string>
<string name="cross_signing_verify_by_emoji">Interactively Verify by Emoji</string>
<string name="confirm_your_identity">Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.</string>
<string name="mark_as_verified">Mark as Trusted</string>
<!-- END Strings added by Valere -->