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)) - 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 | 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 - Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
- Manage Session Settings / Cross Signing update (#1295)
Bugfix 🐛: Bugfix 🐛:
- Fix summary notification staying after "mark as read" - 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 { } else {
otherUserShield.setImageResource(R.drawable.ic_shield_warning) 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 otherUserShield.isVisible = true
} else { } else {
avatarRenderer.render(matrixItem, otherUserAvatarImageView) avatarRenderer.render(matrixItem, otherUserAvatarImageView)

View File

@ -185,8 +185,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
// We need to ask // We need to ask
promptSecurityEvent( promptSecurityEvent(
session, 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) it.navigator.waitSessionVerification(it)
} }

View File

@ -101,7 +101,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("verify") 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)) titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent)) iconColor(colorProvider.getColor(R.color.riotx_accent))

View File

@ -59,7 +59,7 @@ class CrossSigningEpoxyController @Inject constructor(
if (!data.isUploadingKeys) { if (!data.isUploadingKeys) {
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("verify") 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)) titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
@ -76,7 +76,7 @@ class CrossSigningEpoxyController @Inject constructor(
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("verify") 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)) titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) iconColor(colorProvider.getColor(R.color.riotx_positive_accent))

View File

@ -20,15 +20,17 @@ import android.graphics.Typeface
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass 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.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel 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.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -53,22 +55,31 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
var detailedMode = false var detailedMode = false
@EpoxyAttribute @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) { override fun bind(holder: Holder) {
holder.root.setOnClickListener { itemClickAction?.invoke() } holder.root.setOnClickListener { itemClickAction?.invoke() }
if (trusted != null) { val shield = TrustUtils.shieldForTrust(
holder.trustIcon.setImageDrawable( currentDevice,
ContextCompat.getDrawable( trustedSession,
holder.view.context, legacyMode,
if (trusted!!) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning trusted
) )
)
holder.trustIcon.isInvisible = false holder.trustIcon.setImageResource(shield)
} else {
holder.trustIcon.isInvisible = true
}
val detailedModeLabels = listOf( val detailedModeLabels = listOf(
holder.displayNameLabelText, holder.displayNameLabelText,
@ -103,7 +114,28 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL) it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
} }
} else { } 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 holder.summaryLabelText.isVisible = true
detailedModeLabels.map { detailedModeLabels.map {
it.isVisible = false it.isVisible = false

View File

@ -37,7 +37,10 @@ import im.vector.riotx.core.platform.VectorViewModel
data class DeviceVerificationInfoBottomSheetViewState( data class DeviceVerificationInfoBottomSheetViewState(
val cryptoDeviceInfo: Async<CryptoDeviceInfo?> = Uninitialized, 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 ) : MvRxState
class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState, class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState,
@ -51,13 +54,29 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As
} }
init { 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) session.rx().liveUserCryptoDevices(session.myUserId)
.map { list -> .map { list ->
list.firstOrNull { it.deviceId == deviceId } list.firstOrNull { it.deviceId == deviceId }
} }
.execute { .execute {
copy( copy(
cryptoDeviceInfo = it cryptoDeviceInfo = it,
isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId
) )
} }
setState { setState {

View File

@ -16,7 +16,9 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import com.airbnb.epoxy.TypedEpoxyController 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.api.session.Session
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.epoxy.loadingItem 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.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider, class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
@ -37,37 +40,162 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) { override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) {
val cryptoDeviceInfo = data?.cryptoDeviceInfo?.invoke() val cryptoDeviceInfo = data?.cryptoDeviceInfo?.invoke()
if (cryptoDeviceInfo != null) { when {
if (cryptoDeviceInfo.isVerified) { 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 { genericItem {
id("trust${cryptoDeviceInfo.deviceId}") id("trust${cryptoDeviceInfo.deviceId}")
style(GenericItem.STYLE.BIG_TEXT) 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}")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(shield)
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
description(stringProvider.getString(R.string.confirm_your_identity))
}
}
} else {
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)) title(stringProvider.getString(R.string.encryption_information_verified))
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc)) description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
} }
} else { } else {
genericItem { genericItem {
id("trust${cryptoDeviceInfo.deviceId}") id("trust${cryptoDeviceInfo.deviceId}")
titleIconResourceId(R.drawable.ic_shield_warning) titleIconResourceId(shield)
style(GenericItem.STYLE.BIG_TEXT) style(GenericItem.STYLE.BIG_TEXT)
title(stringProvider.getString(R.string.encryption_information_not_verified)) title(stringProvider.getString(R.string.encryption_information_not_verified))
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc)) description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
} }
} }
}
}
// DEVICE INFO SECTION
genericItem { genericItem {
id("info${cryptoDeviceInfo.deviceId}") id("info${cryptoDeviceInfo.deviceId}")
title(cryptoDeviceInfo.displayName() ?: "") title(cryptoDeviceInfo.displayName() ?: "")
description("(${cryptoDeviceInfo.deviceId})") description("(${cryptoDeviceInfo.deviceId})")
} }
if (!cryptoDeviceInfo.isVerified) { 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 { dividerItem {
id("d1") id("d1")
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("verify") id("verify${cryptoDeviceInfo.deviceId}")
title(stringProvider.getString(R.string.verification_verify_device)) title(stringProvider.getString(R.string.verification_verify_device))
titleColor(colorProvider.getColor(R.color.riotx_accent)) titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
@ -77,11 +205,43 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
} }
} }
} }
}
if (cryptoDeviceInfo.deviceId != session.sessionParams.credentials.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 // Add the delete option
dividerItem { dividerItem {
id("d2") id("manageD1")
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("delete") id("delete")
@ -90,13 +250,14 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener { listener {
callback?.onAction(DevicesAction.Delete(cryptoDeviceInfo.deviceId)) callback?.onAction(DevicesAction.Delete(deviceId))
} }
} }
} }
// Always offer rename
dividerItem { dividerItem {
id("d3") id("manageD2")
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("rename") id("rename")
@ -105,43 +266,25 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener {
callback?.onAction(DevicesAction.PromptRename(cryptoDeviceInfo.deviceId)) callback?.onAction(DevicesAction.PromptRename(deviceId))
} }
} }
} else if (data?.deviceInfo?.invoke() != null) { }
val info = data.deviceInfo.invoke()
private fun handleNonE2EDevice(data: DeviceVerificationInfoBottomSheetViewState) {
val info = data.deviceInfo.invoke() ?: return
genericItem { genericItem {
id("info${info?.deviceId}") id("info${info.deviceId}")
title(info?.displayName ?: "") title(info.displayName ?: "")
description("(${info?.deviceId})") description("(${info.deviceId})")
} }
genericFooterItem { genericFooterItem {
id("infoCrypto${info?.deviceId}") id("infoCrypto${info.deviceId}")
text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info)) text(stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info))
} }
if (info?.deviceId != session.sessionParams.credentials.deviceId) { info.deviceId?.let { addGenericDeviceManageActions(data, it) }
// 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 ?: ""))
}
}
}
} else {
loadingItem {
id("loading")
}
}
} }
interface Callback { interface Callback {

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
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 {
@ -26,4 +27,7 @@ sealed class DevicesAction : VectorViewModelAction {
data class PromptRename(val deviceId: String) : DevicesAction() data class PromptRename(val deviceId: String) : DevicesAction()
data class VerifyMyDevice(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.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.extensions.sortByLastSeen 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.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo 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.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter 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.resources.StringProvider
import im.vector.riotx.core.ui.list.genericItemHeader import im.vector.riotx.core.ui.list.genericItemHeader
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Inject import javax.inject.Inject
class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter, class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val vectorPreferences: VectorPreferences) : EpoxyController() { private val vectorPreferences: VectorPreferences) : EpoxyController() {
var callback: Callback? = null var callback: Callback? = null
@ -68,30 +73,51 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
listener { callback?.retry() } listener { callback?.retry() }
} }
is Success -> 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) { private fun buildDevicesList(devices: List<DeviceInfo>,
cryptoDevices: List<CryptoDeviceInfo>?,
myDeviceId: String,
legacyMode: Boolean,
currentSessionCrossTrusted: Boolean) {
devices
.firstOrNull() {
it.deviceId == myDeviceId
}?.let { deviceInfo ->
// Current device // Current device
genericItemHeader { genericItemHeader {
id("current") id("current")
text(stringProvider.getString(R.string.devices_current_device)) text(stringProvider.getString(R.string.devices_current_device))
} }
devices
.filter {
it.deviceId == myDeviceId
}
.forEachIndexed { idx, deviceInfo ->
deviceItem { deviceItem {
id("myDevice$idx") id("myDevice${deviceInfo.deviceId}")
legacyMode(legacyMode)
trustedSession(currentSessionCrossTrusted)
dimensionConverter(dimensionConverter)
colorProvider(colorProvider)
detailedMode(vectorPreferences.developerMode()) detailedMode(vectorPreferences.developerMode())
deviceInfo(deviceInfo) deviceInfo(deviceInfo)
currentDevice(true) currentDevice(true)
itemClickAction { callback?.onDeviceClicked(deviceInfo) } 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 // Other devices
@ -111,11 +137,15 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
val isCurrentDevice = deviceInfo.deviceId == myDeviceId val isCurrentDevice = deviceInfo.deviceId == myDeviceId
deviceItem { deviceItem {
id("device$idx") id("device$idx")
legacyMode(legacyMode)
trustedSession(currentSessionCrossTrusted)
dimensionConverter(dimensionConverter)
colorProvider(colorProvider)
detailedMode(vectorPreferences.developerMode()) detailedMode(vectorPreferences.developerMode())
deviceInfo(deviceInfo) deviceInfo(deviceInfo)
currentDevice(isCurrentDevice) currentDevice(isCurrentDevice)
itemClickAction { callback?.onDeviceClicked(deviceInfo) } 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 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.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.core.platform.VectorViewEvents import im.vector.riotx.core.platform.VectorViewEvents
@ -35,4 +37,10 @@ sealed class DevicesViewEvents : VectorViewEvents {
val userId: String, val userId: String,
val transactionId: String? val transactionId: String?
) : DevicesViewEvents() ) : 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 package im.vector.riotx.features.settings.devices
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext 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.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.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
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.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.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.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.matrix.android.internal.crypto.model.rest.DevicesListResponse
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 im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
import kotlinx.coroutines.launch
data class DevicesViewState( data class DevicesViewState(
val myDeviceId: String = "", val myDeviceId: String = "",
val devices: Async<List<DeviceInfo>> = Uninitialized, val devices: Async<List<DeviceInfo>> = Uninitialized,
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized, val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized,
// TODO Replace by isLoading boolean // TODO Replace by isLoading boolean
val request: Async<Unit> = Uninitialized val request: Async<Unit> = Uninitialized,
val hasAccountCrossSigning: Boolean = false,
val accountCrossSigningIsTrusted: Boolean = false
) : MvRxState ) : MvRxState
class DevicesViewModel @AssistedInject constructor( class DevicesViewModel @AssistedInject constructor(
@ -75,6 +83,21 @@ class DevicesViewModel @AssistedInject constructor(
private var _currentSession: String? = null private var _currentSession: String? = null
init { 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() refreshDevicesList()
session.cryptoService().verificationService().addListener(this) session.cryptoService().verificationService().addListener(this)
@ -169,20 +192,51 @@ class DevicesViewModel @AssistedInject constructor(
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 -> handleVerify(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() val txID = session.cryptoService()
.verificationService() .verificationService()
.requestKeyVerification(supportedVerificationMethodsProvider.provide(), session.myUserId, listOf(action.deviceId)) .beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId, null)
_viewEvents.post(DevicesViewEvents.ShowVerifyDevice( _viewEvents.post(DevicesViewEvents.ShowVerifyDevice(
session.myUserId, 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 -> private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId } val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId }
if (info != null) { 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 com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo 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.dialogs.ManuallyVerifyDialog
import im.vector.riotx.core.dialogs.PromptPasswordDialog import im.vector.riotx.core.dialogs.PromptPasswordDialog
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.configureWith
@ -73,6 +74,15 @@ class VectorSettingsDevicesFragment @Inject constructor(
transactionId = it.transactionId transactionId = it.transactionId
).show(childFragmentManager, "REQPOP") ).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 }.exhaustive
} }
} }

View File

@ -33,7 +33,7 @@
<LinearLayout <LinearLayout
android:id="@+id/alerter_texts" android:id="@+id/alerter_texts"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -68,6 +68,7 @@
android:textAppearance="@style/AlertTextAppearance.Text" android:textAppearance="@style/AlertTextAppearance.Text"
android:visibility="gone" android:visibility="gone"
tools:text="Text" tools:text="Text"
android:maxLines="3"
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout> </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_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_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_verified">Verified</string>
<string name="encryption_information_blocked">Blacklisted</string> <string name="encryption_information_blocked">Blacklisted</string>
@ -1038,8 +1038,8 @@
<string name="encryption_information_unblock">Unblacklist</string> <string name="encryption_information_unblock">Unblacklist</string>
<string name="encryption_information_verify_device">Verify session</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_warning">Confirm by comparing the following with the User Settings in your other session:</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_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="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> <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_list">Active Sessions</string>
<string name="settings_active_sessions_show_all">Show All 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_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> <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> <item quantity="other">%d active sessions</item>
</plurals> </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="crosssigning_other_user_not_trust">Other users may not trust it</string>
<string name="complete_security">Complete Security</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="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="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="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 --> <!-- END Strings added by Valere -->