Self verification + toDevice Request

This commit is contained in:
Valere 2020-01-30 16:35:42 +01:00
parent 03c5e61b2e
commit 50d5ad3625
12 changed files with 250 additions and 58 deletions

View File

@ -54,7 +54,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
data class VerificationArgs(
val otherUserId: String,
val verificationId: String? = null,
val roomId: String? = null
val roomId: String? = null,
// Special mode where UX should show loading wheel until other user sends a request/tx
val waitForIncomingRequest : Boolean = false
) : Parcelable
@Inject
@ -97,14 +99,25 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun invalidate() = withState(viewModel) {
it.otherUserMxItem?.let { matrixItem ->
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
if (it.sasTransactionState == VerificationTxState.Verified || it.qrTransactionState == VerificationTxState.Verified) {
otherUserNameText.text = getString(R.string.verification_verified_user, matrixItem.getBestName())
otherUserShield.isVisible = true
} else {
otherUserNameText.text = getString(R.string.verification_verify_user, matrixItem.getBestName())
if (it.waitForOtherUserMode) {
if (it.sasTransactionState == VerificationTxState.Verified || it.qrTransactionState == VerificationTxState.Verified) {
otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_trusted)
} else {
otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_warning)
}
otherUserNameText.text = getString(R.string.complete_security)
otherUserShield.isVisible = false
} else {
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
if (it.sasTransactionState == VerificationTxState.Verified || it.qrTransactionState == VerificationTxState.Verified) {
otherUserNameText.text = getString(R.string.verification_verified_user, matrixItem.getBestName())
otherUserShield.isVisible = true
} else {
otherUserNameText.text = getString(R.string.verification_verify_user, matrixItem.getBestName())
otherUserShield.isVisible = false
}
}
}
@ -136,12 +149,12 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
is VerificationTxState.Verified -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null))
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, it.isMe))
})
}
is VerificationTxState.Cancelled -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, it.sasTransactionState.cancelCode.value))
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, it.sasTransactionState.cancelCode.value, it.isMe))
})
}
}
@ -156,13 +169,13 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
is VerificationTxState.Verified -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null))
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, it.isMe))
})
return@withState
}
is VerificationTxState.Cancelled -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, it.qrTransactionState.cancelCode.value))
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, it.qrTransactionState.cancelCode.value, it.isMe))
})
return@withState
}
@ -178,7 +191,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
// If it's an outgoing
if (it.pendingRequest.invoke() == null || it.pendingRequest.invoke()?.isIncoming == false) {
if (it.pendingRequest.invoke() == null || it.pendingRequest.invoke()?.isIncoming == false || it.waitForOtherUserMode) {
Timber.v("## SAS show bottom sheet for outgoing request")
if (it.pendingRequest.invoke()?.isReady == true) {
Timber.v("## SAS show bottom sheet for outgoing and ready request")
@ -225,17 +238,20 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
companion object {
fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null): VerificationBottomSheet {
fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null, waitForIncomingRequest: Boolean = false): VerificationBottomSheet {
return VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs(
otherUserId = otherUserId,
roomId = roomId,
verificationId = transactionId
verificationId = transactionId,
waitForIncomingRequest = waitForIncomingRequest
))
}
}
}
val WAITING_SELF_VERIF_TAG : String = "WAITING_SELF_VERIF_TAG"
}
}

View File

@ -30,6 +30,7 @@ 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.session.Session
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
@ -55,7 +56,9 @@ data class VerificationBottomSheetViewState(
val pendingLocalId: String? = null,
val sasTransactionState: VerificationTxState? = null,
val qrTransactionState: VerificationTxState? = null,
val transactionId: String? = null
val transactionId: String? = null,
val waitForOtherUserMode: Boolean = false,
val isMe: Boolean = false
) : MvRxState
class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
@ -102,13 +105,18 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
session.getVerificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
}
val isWaitingForOtherMode = args.waitForIncomingRequest
// TODO see if active tx for this user and take it
return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(
otherUserMxItem = userItem?.toMatrixItem(),
sasTransactionState = sasTx?.state,
qrTransactionState = qrTx?.state,
transactionId = args.verificationId,
pendingRequest = if (pr != null) Success(pr) else Uninitialized,
roomId = args.roomId)
waitForOtherUserMode = isWaitingForOtherMode,
roomId = args.roomId,
isMe = args.otherUserId == session.myUserId)
)
}
}
@ -230,6 +238,27 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
}
override fun transactionUpdated(tx: VerificationTransaction) = withState { state ->
if (state.waitForOtherUserMode && state.transactionId == null) {
// is this an incoming with that user
if (tx.isIncoming && tx.otherUserId == state.otherUserMxItem?.id) {
// Also auto accept incoming if needed!
if (tx is IncomingSasVerificationTransaction) {
if (tx.uxState == IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
tx.performAccept()
}
}
// Use this one!
setState {
copy(
transactionId = tx.transactionId,
sasTransactionState = tx.state.takeIf { tx is SasVerificationTransaction },
qrTransactionState = tx.state.takeIf { tx is QrCodeVerificationTransaction }
)
}
}
}
when (tx) {
is SasVerificationTransaction -> {
if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) {
@ -260,6 +289,20 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
if (state.waitForOtherUserMode && state.pendingRequest.invoke() == null && state.transactionId == null) {
// is this an incoming with that user
if (pr.isIncoming && pr.otherUserId == state.otherUserMxItem?.id) {
// Use this one!
setState {
copy(
transactionId = pr.transactionId,
pendingRequest = Success(pr)
)
}
return@withState
}
}
if (pr.localID == state.pendingLocalId
|| pr.localID == state.pendingRequest.invoke()?.localID
|| state.pendingRequest.invoke()?.transactionId == pr.transactionId) {

View File

@ -49,7 +49,10 @@ class VerificationConclusionController @Inject constructor(
ConclusionState.SUCCESS -> {
bottomSheetVerificationNoticeItem {
id("notice")
notice(stringProvider.getString(R.string.verification_conclusion_ok_notice))
notice(stringProvider.getString(
if (state.isSelfVerification) R.string.verification_conclusion_ok_self_notice
else R.string.verification_conclusion_ok_notice))
}
bottomSheetVerificationBigImageItem {

View File

@ -38,7 +38,8 @@ class VerificationConclusionFragment @Inject constructor(
@Parcelize
data class Args(
val isSuccessFull: Boolean,
val cancelReason: String?
val cancelReason: String?,
val isMe: Boolean
) : Parcelable
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)

View File

@ -25,7 +25,8 @@ import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
data class VerificationConclusionViewState(
val conclusionState: ConclusionState = ConclusionState.CANCELLED
val conclusionState: ConclusionState = ConclusionState.CANCELLED,
val isSelfVerification: Boolean = false
) : MvRxState
enum class ConclusionState {
@ -48,13 +49,13 @@ class VerificationConclusionViewModel(initialState: VerificationConclusionViewSt
CancelCode.MismatchedSas,
CancelCode.MismatchedCommitment,
CancelCode.MismatchedKeys -> {
VerificationConclusionViewState(ConclusionState.WARNING)
VerificationConclusionViewState(ConclusionState.WARNING, args.isMe)
}
else -> {
VerificationConclusionViewState(
if (args.isSuccessFull) ConclusionState.SUCCESS
else ConclusionState.CANCELLED
)
, args.isMe)
}
}
}

View File

@ -50,46 +50,66 @@ class VerificationRequestController @Inject constructor(
val state = viewState ?: return
val matrixItem = viewState?.otherUserMxItem ?: return
val styledText = matrixItem.let {
stringProvider.getString(R.string.verification_request_notice, it.id)
.toSpannable()
.colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color))
}
bottomSheetVerificationNoticeItem {
id("notice")
notice(styledText)
}
dividerItem {
id("sep")
}
when (val pr = state.pendingRequest) {
is Uninitialized -> {
bottomSheetVerificationActionItem {
id("start")
title(stringProvider.getString(R.string.start_verification))
titleColor(colorProvider.getColor(R.color.riotx_accent))
subTitle(stringProvider.getString(R.string.verification_request_start_notice))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener?.onClickOnVerificationStart() }
}
if (state.waitForOtherUserMode) {
bottomSheetVerificationNoticeItem {
id("notice")
notice(stringProvider.getString(R.string.verification_open_other_to_verify))
}
is Loading -> {
bottomSheetVerificationWaitingItem {
id("waiting")
title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
}
dividerItem {
id("sep")
}
is Success -> {
if (!pr.invoke().isReady) {
bottomSheetVerificationWaitingItem {
id("waiting")
title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
}
} else {
val styledText = matrixItem.let {
stringProvider.getString(R.string.verification_request_notice, it.id)
.toSpannable()
.colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color))
}
bottomSheetVerificationNoticeItem {
id("notice")
notice(styledText)
}
dividerItem {
id("sep")
}
when (val pr = state.pendingRequest) {
is Uninitialized -> {
bottomSheetVerificationActionItem {
id("start")
title(stringProvider.getString(R.string.start_verification))
titleColor(colorProvider.getColor(R.color.riotx_accent))
subTitle(stringProvider.getString(R.string.verification_request_start_notice))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener?.onClickOnVerificationStart() }
}
}
is Loading -> {
bottomSheetVerificationWaitingItem {
id("waiting")
title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
}
}
is Success -> {
if (!pr.invoke().isReady) {
bottomSheetVerificationWaitingItem {
id("waiting")
title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
}
}
}
}
}
}

View File

@ -22,10 +22,15 @@ import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Observer
import im.vector.matrix.android.api.MatrixCallback
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.MXUsersDevicesMap
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
@ -36,6 +41,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.workers.signout.SignOutViewModel
import im.vector.riotx.push.fcm.FcmHelper
@ -96,7 +102,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
activeSessionHolder.getSafeActiveSession()?.getInitialSyncProgressStatus()?.observe(this, Observer { status ->
if (status == null) {
waiting_view.isVisible = false
promptCompleteSecurityIfNeeded()
} else {
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = false
Timber.v("${getString(status.statusText)} ${status.percentProgress}")
waiting_view.setOnClickListener {
// block interactions
@ -116,6 +124,66 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
})
}
private fun promptCompleteSecurityIfNeeded() {
val session = activeSessionHolder.getSafeActiveSession() ?: return
if (!session.hasAlreadySynced()) return
if (sharedActionViewModel.hasDisplayedCompleteSecurityPrompt) return
// ensure keys are downloaded
session.downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
runOnUiThread {
alertCompleteSecurity(session)
}
}
})
}
private fun alertCompleteSecurity(session: Session) {
val myCrossSigningKeys = session.getCrossSigningService()
.getMyCrossSigningKeys()
val crossSigningEnabledOnAccount = myCrossSigningKeys != null
if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) {
// We need to ask
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
PopupAlertManager.postVectorAlert(
PopupAlertManager.VectorAlert(
uid = "completeSecurity",
title = getString(R.string.crosssigning_verify_this_session),
description = getString(R.string.crosssigning_other_user_not_trust),
iconId = R.drawable.ic_shield_warning
).apply {
colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent)
contentAction = Runnable {
Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
it.navigator.waitSessionVerification(it)
}
}
}
dismissedAction = Runnable {
// tx.cancel()
}
addButton(
getString(R.string.later),
Runnable {
}
)
addButton(
getString(R.string.verification_profile_verify),
Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
it.navigator.waitSessionVerification(it)
}
}
)
}
)
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {

View File

@ -19,4 +19,6 @@ package im.vector.riotx.features.home
import im.vector.riotx.core.platform.VectorSharedActionViewModel
import javax.inject.Inject
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>()
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() {
var hasDisplayedCompleteSecurityPrompt : Boolean = false
}

View File

@ -153,7 +153,7 @@ class MessageItemFactory @Inject constructor(
VerificationRequestItem.Attributes(
otherUserId = otherUserId,
otherUserName = otherUserName.toString(),
fromDevide = messageContent.fromDevice,
fromDevide = messageContent.fromDevice ?: "",
referenceId = informationData.eventId,
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,

View File

@ -21,6 +21,7 @@ import android.content.Context
import android.content.Intent
import androidx.core.app.TaskStackBuilder
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
@ -75,6 +76,34 @@ class DefaultNavigator @Inject constructor(
).show(context.supportFragmentManager, "REQPOP")
}
}
override fun requestSessionVerification(context: Context) {
val session = sessionHolder.getSafeActiveSession() ?: return
val pr = session.getVerificationService().requestKeyVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
session.myUserId,
session.getUserDevices(session.myUserId).map { it.deviceId })
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = session.myUserId,
transactionId = pr.transactionId
).show(context.supportFragmentManager, "REQPOP")
}
}
override fun waitSessionVerification(context: Context) {
val session = sessionHolder.getSafeActiveSession() ?: return
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = session.myUserId,
waitForIncomingRequest = true
).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
}
}
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) {
if (context is VectorBaseActivity) {
context.notImplemented("Open not joined room")

View File

@ -27,6 +27,8 @@ interface Navigator {
fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false)
fun performDeviceVerification(context: Context, otherUserId: String, sasTransationId: String)
fun requestSessionVerification(context: Context)
fun waitSessionVerification(context: Context)
fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData)

View File

@ -100,18 +100,19 @@
<string name="room_settings_enable_encryption_dialog_content">Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly.</string>
<string name="room_settings_enable_encryption_dialog_submit">Enable encryption</string>
<string name="verification_request_notice">For extra security, verify %s by checking a one-time code.</string>
<string name="verification_request_start_notice">For maximum security, do this in person.</string>
<string name="verification_request_notice">To be secure, verify %s by checking a one-time code.</string>
<string name="verification_request_start_notice">To be secure, do this in person or use another way to communicate.</string>
<string name="verification_emoji_notice">Compare the unique emoji, ensuring they appear in the same order.</string>
<string name="verification_code_notice">Compare the code with the one displayed on the other user\'s screen.</string>
<string name="verification_conclusion_ok_notice">Messages with this user are end-to-end encrypted and can\'t be read by third parties.</string>
<string name="verification_conclusion_ok_self_notice">Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.</string>
<string name="encryption_information_cross_signing_state">Cross-Signing</string>
<string name="encryption_information_dg_xsigning_complete">Cross-Signing is enabled\nPrivate Keys on device.</string>
<string name="encryption_information_dg_xsigning_trusted">Cross-Signing is enabled\nKeys are trusted\n.Private keys are not known</string>
<string name="encryption_information_dg_xsigning_not_trusted">Cross-Signing is enabled\nKeys are not trusted</string>
<string name="encryption_information_dg_xsigning_trusted">Cross-Signing is enabled\nKeys are trusted.\nPrivate keys are not known</string>
<string name="encryption_information_dg_xsigning_not_trusted">Cross-Signing is enabled.\nKeys are not trusted</string>
<string name="encryption_information_dg_xsigning_disabled">Cross-Signing is not enabled</string>
@ -128,6 +129,12 @@
<item quantity="other">%d active sessions</item>
</plurals>
<string name="crosssigning_verify_this_session">Verify this session</string>
<string name="crosssigning_other_user_not_trust">Other users may not trust it</string>
<string name="complete_security">Complete Security</string>
<string name="verification_open_other_to_verify">Open an existing session &amp; use it to verify this one, granting it access to encrypted messages. If you cant access one, use your recovery key or passphrase.</string>
<string name="verification_profile_verify">Verify</string>
<string name="verification_profile_verified">Verified</string>