diff --git a/CHANGES.md b/CHANGES.md index 1c35e70cd8..12274a3e4c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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" diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/ManuallyVerifyDialog.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/ManuallyVerifyDialog.kt new file mode 100644 index 0000000000..9863f7030f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/ManuallyVerifyDialog.kt @@ -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(R.id.encrypted_device_info_device_name)?.let { + it.text = cryptoDeviceInfo.displayName() + } + + dialogLayout.findViewById(R.id.encrypted_device_info_device_id)?.let { + it.text = cryptoDeviceInfo.deviceId + } + + dialogLayout.findViewById(R.id.encrypted_device_info_device_key)?.let { + it.text = cryptoDeviceInfo.getFingerprintHumanReadable() + } + + builder.show() + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericButtonItem.kt b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericButtonItem.kt new file mode 100644 index 0000000000..c0fdb010a5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericButtonItem.kt @@ -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() { + + @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(R.id.itemGenericItemButton) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt index e07150ed4f..7a3d38f649 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index c28dca8dbf..60c974c291 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt index 907c019f39..a9af166c1d 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt @@ -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)) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt index fff177f43d..5b7875d0ce 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt @@ -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)) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt index b792afe666..8ed4d0ef64 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt @@ -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() { 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() { 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 diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt index 7ee79a279f..47b64df927 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt @@ -37,7 +37,10 @@ import im.vector.riotx.core.platform.VectorViewModel data class DeviceVerificationInfoBottomSheetViewState( val cryptoDeviceInfo: Async = Uninitialized, - val deviceInfo: Async = Uninitialized + val deviceInfo: Async = 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 { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt index 90724166a0..4123e260e2 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt index e4b1b98cc8..22dcc9cfc3 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt index 1d275c7da2..817a3a3c53 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -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, cryptoDevices: List?, myDeviceId: String) { - // Current device - genericItemHeader { - id("current") - text(stringProvider.getString(R.string.devices_current_device)) - } - + private fun buildDevicesList(devices: List, + cryptoDevices: List?, + 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) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewEvents.kt index 075eb2050e..2cbdbe9485 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt index 79a5fe84aa..560e6f396d 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -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> = Uninitialized, val cryptoDevices: Async> = Uninitialized, // TODO Replace by isLoading boolean - val request: Async = Uninitialized + val request: Async = 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 { + 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) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/TrustUtils.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/TrustUtils.kt new file mode 100644 index 0000000000..7f987b327b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/TrustUtils.kt @@ -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 + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt index 603b0b6b78..aedeb7d651 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -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 } } diff --git a/vector/src/main/res/layout/alerter_verification_layout.xml b/vector/src/main/res/layout/alerter_verification_layout.xml index b06883b056..7098c3152d 100644 --- a/vector/src/main/res/layout/alerter_verification_layout.xml +++ b/vector/src/main/res/layout/alerter_verification_layout.xml @@ -33,7 +33,7 @@ diff --git a/vector/src/main/res/layout/item_generic_button.xml b/vector/src/main/res/layout/item_generic_button.xml new file mode 100644 index 0000000000..d2af2663d3 --- /dev/null +++ b/vector/src/main/res/layout/item_generic_button.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 370b7cf8f4..939c50fdff 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1024,7 +1024,7 @@ Never send encrypted messages to unverified sessions from this session. %1$d/%2$d key(s) imported with success. - NOT Verified + Not Verified Verified Blacklisted @@ -1038,8 +1038,8 @@ Unblacklist Verify session - 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: - If it matches, press the verify button below. If it doesnโ€™t, then someone else is intercepting this session and you should probably blacklist it. In the future this verification process will be more sophisticated. + Confirm by comparing the following with the User Settings in your other session: + "If they don't match, the security of your communication may be compromised." I verify that the keys match 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. @@ -2110,7 +2110,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Active Sessions Show All Sessions Manage Sessions - Sign out this session + Sign out of this session No cryptographic information available @@ -2122,7 +2122,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming %d active sessions - Verify this session + Verify this login Other users may not trust it Complete Security diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8b675ee8c1..3d4c8dcf30 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -19,6 +19,12 @@ Select your Recovery Key, or input it manually by typing it or pasting from your clipboard Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key. Failed to access secure storage + + Manually Verify by Text + Verify login + Interactively Verify by Emoji + Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages. + Mark as Trusted