diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 83fb53424e..d0a08b17ab 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -239,7 +239,7 @@ internal class DefaultCryptoService @Inject constructor( override fun getDevicesList(callback: MatrixCallback) { getDevicesTask .configureWith { - this.executionThread = TaskThread.CRYPTO +// this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -729,30 +729,30 @@ internal class DefaultCryptoService @Inject constructor( */ private fun onRoomKeyEvent(event: Event) { val roomKeyContent = event.getClearContent().toModel() ?: return - Timber.v("## onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>") + Timber.v("## GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>") if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { - Timber.e("## onRoomKeyEvent() : missing fields") + Timber.e("## GOSSIP onRoomKeyEvent() : missing fields") return } val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) if (alg == null) { - Timber.e("## onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") + Timber.e("## GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") return } alg.onRoomKeyEvent(event, keysBackupService) } private fun onSecretSendReceived(event: Event) { - Timber.i("## onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}") + Timber.i("## GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}") if (!event.isEncrypted()) { // secret send messages must be encrypted - Timber.e("## onSecretSend() :Received unencrypted secret send event") + Timber.e("## GOSSIP onSecretSend() :Received unencrypted secret send event") return } // Was that sent by us? if (event.senderId != credentials.userId) { - Timber.e("## onSecretSend() : Ignore secret from other user ${event.senderId}") + Timber.e("## GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") return } @@ -762,7 +762,7 @@ internal class DefaultCryptoService @Inject constructor( .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } if (existingRequest == null) { - Timber.i("## onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + Timber.i("## GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index a0420b0125..86289301e4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -757,6 +757,7 @@ internal class DefaultVerificationService @Inject constructor( private suspend fun onReadyReceived(event: Event) { val readyReq = event.getClearContent().toModel()?.asValidObject() + Timber.v("## SAS onReadyReceived $readyReq") if (readyReq == null || event.senderId == null) { // ignore diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 4e60a1bdf7..4deaef32ab 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -26,6 +26,8 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment +import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment +import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment @@ -336,6 +338,16 @@ interface FragmentModule { @FragmentKey(VerificationConclusionFragment::class) fun bindVerificationConclusionFragment(fragment: VerificationConclusionFragment): Fragment + @Binds + @IntoMap + @FragmentKey(VerificationCancelFragment::class) + fun bindVerificationCancelFragment(fragment: VerificationCancelFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VerificationNotMeFragment::class) + fun bindVerificationNotMeFragment(fragment: VerificationNotMeFragment): Fragment + @Binds @IntoMap @FragmentKey(QrCodeScannerFragment::class) 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 e1218ec4a9..9edf57e39d 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 @@ -16,9 +16,11 @@ package im.vector.riotx.features.crypto.verification import android.app.Activity +import android.app.Dialog import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.view.KeyEvent import android.view.View import android.widget.ImageView import android.widget.TextView @@ -39,14 +41,18 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity +import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment +import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.settings.VectorSettingsActivity import kotlinx.android.parcel.Parcelize import timber.log.Timber import javax.inject.Inject @@ -58,6 +64,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { data class VerificationArgs( val otherUserId: String, val verificationId: String? = null, + val verificationLocalId: String? = null, val roomId: String? = null, // Special mode where UX should show loading wheel until other session sends a request/tx val selfVerificationMode: Boolean = false @@ -80,13 +87,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { lateinit var otherUserNameText: TextView @BindView(R.id.verificationRequestShield) - lateinit var otherUserShield: View + lateinit var otherUserShield: ImageView @BindView(R.id.verificationRequestAvatar) lateinit var otherUserAvatarImageView: ImageView override fun getLayoutResId() = R.layout.bottom_sheet_verification + init { + isCancelable = false + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -110,10 +121,27 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { .show() Unit } + VerificationBottomSheetViewEvents.GoToSettings -> { + dismiss() + (activity as? VectorBaseActivity)?.navigator?.openSettings(requireContext(), VectorSettingsActivity.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY) + } }.exhaustive } } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + setOnKeyListener { _, keyCode, keyEvent -> + if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) { + viewModel.queryCancel() + true + } else { + false + } + } + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) { data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let { @@ -127,15 +155,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { state.otherUserMxItem?.let { matrixItem -> if (state.isMe) { + + avatarRenderer.render(matrixItem, otherUserAvatarImageView) if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified || state.verifiedFromPrivateKeys) { - otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_trusted) + otherUserShield.setImageResource(R.drawable.ic_shield_trusted) } else { - otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_warning) + otherUserShield.setImageResource(R.drawable.ic_shield_warning) } otherUserNameText.text = getString(R.string.complete_security) - otherUserShield.isVisible = false + otherUserShield.isVisible = true } else { avatarRenderer.render(matrixItem, otherUserAvatarImageView) @@ -149,6 +179,18 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } + if (state.userThinkItsNotHim) { + otherUserNameText.text = getString(R.string.dialog_title_warning) + showFragment(VerificationNotMeFragment::class, Bundle()) + return@withState + } + + if (state.userWantsToCancel) { + otherUserNameText.text = getString(R.string.are_you_sure) + showFragment(VerificationCancelFragment::class, Bundle()) + return@withState + } + if (state.selfVerificationMode && state.verifiedFromPrivateKeys) { showFragment(VerificationConclusionFragment::class, Bundle().apply { putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt index d7c02a8d3b..7e3a5441de 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt @@ -24,5 +24,6 @@ import im.vector.riotx.core.platform.VectorViewEvents sealed class VerificationBottomSheetViewEvents : VectorViewEvents { object Dismiss : VerificationBottomSheetViewEvents() object AccessSecretStore : VerificationBottomSheetViewEvents() + object GoToSettings : VerificationBottomSheetViewEvents() data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index db8dd895b4..fab59fe5af 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -31,7 +31,9 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod @@ -44,7 +46,6 @@ import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 import im.vector.matrix.android.internal.crypto.crosssigning.isVerified -import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel import timber.log.Timber @@ -60,7 +61,10 @@ data class VerificationBottomSheetViewState( // true when we display the loading and we wait for the other (incoming request) val selfVerificationMode: Boolean = false, val verifiedFromPrivateKeys: Boolean = false, - val isMe: Boolean = false + val isMe: Boolean = false, + val currentDeviceCanCrossSign: Boolean = false, + val userWantsToCancel: Boolean = false, + val userThinkItsNotHim: Boolean = false ) : MvRxState class VerificationBottomSheetViewModel @AssistedInject constructor( @@ -111,7 +115,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( pendingRequest = if (pr != null) Success(pr) else Uninitialized, selfVerificationMode = selfVerificationMode, roomId = args.roomId, - isMe = args.otherUserId == session.myUserId + isMe = args.otherUserId == session.myUserId, + currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign() ) } @@ -137,6 +142,46 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( args: VerificationBottomSheet.VerificationArgs): VerificationBottomSheetViewModel } + fun queryCancel() { + setState { + copy(userWantsToCancel = true) + } + } + + fun confirmCancel() = withState { state -> + session.cryptoService() + .verificationService() + .getExistingTransaction(state.otherUserMxItem?.id ?: "", state.transactionId ?: "") + ?.cancel(CancelCode.User) + _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) + } + + fun continueFromCancel() { + setState { + copy(userWantsToCancel = false) + } + } + + fun continueFromWasNotMe() { + setState { + copy(userThinkItsNotHim = false) + } + } + + fun itWasNotMe() { + setState { + copy(userThinkItsNotHim = true) + } + } + + fun goToSettings() = withState { state -> + session.cryptoService() + .verificationService() + .getExistingTransaction(state.otherUserMxItem?.id ?: "", state.transactionId ?: "") + ?.cancel(CancelCode.User) + _viewEvents.post(VerificationBottomSheetViewEvents.GoToSettings) + } + companion object : MvRxViewModelFactory { override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelController.kt new file mode 100644 index 0000000000..1beea4ae9f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelController.kt @@ -0,0 +1,96 @@ +/* + * 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.crypto.verification.cancel + +import com.airbnb.epoxy.EpoxyController +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.dividerItem +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import javax.inject.Inject + +class VerificationCancelController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider +) : EpoxyController() { + + var listener: Listener? = null + + private var viewState: VerificationBottomSheetViewState? = null + + fun update(viewState: VerificationBottomSheetViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val state = viewState ?: return + + if (state.isMe) { + if (state.currentDeviceCanCrossSign) { + bottomSheetVerificationNoticeItem { + id("notice") + notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_trusted)) + } + } else { + bottomSheetVerificationNoticeItem { + id("notice") + notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted)) + } + } + } else { + bottomSheetVerificationNoticeItem { + id("notice") + notice(stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted)) + } + } + + dividerItem { + id("sep0") + } + + bottomSheetVerificationActionItem { + id("cancel") + title(stringProvider.getString(R.string.cancel)) + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + listener { listener?.onTapCancel() } + } + + dividerItem { + id("sep1") + } + + bottomSheetVerificationActionItem { + id("continue") + title(stringProvider.getString(R.string._continue)) + titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) + listener { listener?.onTapContinue() } + } + } + + interface Listener { + fun onTapCancel() + fun onTapContinue() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelFragment.kt new file mode 100644 index 0000000000..0c5c070156 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationCancelFragment.kt @@ -0,0 +1,66 @@ +/* + * 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.crypto.verification.cancel + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel +import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.* +import javax.inject.Inject + +class VerificationCancelFragment @Inject constructor( + val controller: VerificationCancelController +) : VectorBaseFragment(), VerificationCancelController.Listener { + + private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + } + + override fun onDestroyView() { + bottomSheetVerificationRecyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + private fun setupRecyclerView() { + bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true) + controller.listener = this + } + + override fun invalidate() = withState(viewModel) { state -> + controller.update(state) + } + + override fun onTapCancel() { + viewModel.confirmCancel() + } + + override fun onTapContinue() { + viewModel.continueFromCancel() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeController.kt new file mode 100644 index 0000000000..3929c1a166 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeController.kt @@ -0,0 +1,83 @@ +/* + * 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.crypto.verification.cancel + +import com.airbnb.epoxy.EpoxyController +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.dividerItem +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import im.vector.riotx.features.html.EventHtmlRenderer +import javax.inject.Inject + +class VerificationNotMeController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, + private val eventHtmlRenderer: EventHtmlRenderer +) : EpoxyController() { + + var listener: Listener? = null + + private var viewState: VerificationBottomSheetViewState? = null + + fun update(viewState: VerificationBottomSheetViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + + bottomSheetVerificationNoticeItem { + id("notice") + notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verify_not_me_self_verification))) + } + + dividerItem { + id("sep0") + } + + bottomSheetVerificationActionItem { + id("skip") + title(stringProvider.getString(R.string.skip)) + titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { listener?.onTapSkip() } + } + + dividerItem { + id("sep1") + } + + bottomSheetVerificationActionItem { + id("settings") + title(stringProvider.getString(R.string.settings)) + titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) + listener { listener?.onTapSettings() } + } + } + + interface Listener { + fun onTapSkip() + fun onTapSettings() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeFragment.kt new file mode 100644 index 0000000000..b764639078 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/cancel/VerificationNotMeFragment.kt @@ -0,0 +1,66 @@ +/* + * 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.crypto.verification.cancel + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel +import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.* +import javax.inject.Inject + +class VerificationNotMeFragment @Inject constructor( + val controller: VerificationNotMeController +) : VectorBaseFragment(), VerificationNotMeController.Listener { + + private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + } + + override fun onDestroyView() { + bottomSheetVerificationRecyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + private fun setupRecyclerView() { + bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true) + controller.listener = this + } + + override fun invalidate() = withState(viewModel) { state -> + controller.update(state) + } + + override fun onTapSkip() { + viewModel.continueFromWasNotMe() + } + + override fun onTapSettings() { + viewModel.goToSettings() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt index 87bb843291..919869500f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt @@ -95,10 +95,27 @@ class VerificationChooseMethodController @Inject constructor( listener { listener?.doVerifyBySas() } } } + + if (state.isMe && state.canCrossSign) { + dividerItem { + id("sep_notMe") + } + + bottomSheetVerificationActionItem { + id("wasnote") + title(stringProvider.getString(R.string.verify_new_session_was_not_me)) + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + subTitle(stringProvider.getString(R.string.verify_new_session_compromized)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { listener?.onClickOnWasNotMe() } + } + } } interface Listener { fun openCamera() fun doVerifyBySas() + fun onClickOnWasNotMe() } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt index e0b7f97383..eb32f5b0e3 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt @@ -89,6 +89,10 @@ class VerificationChooseMethodFragment @Inject constructor( } } + override fun onClickOnWasNotMe() { + sharedViewModel.itWasNotMe() + } + private fun doOpenQRCodeScanner() { QrCodeScannerActivity.startForResult(this) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt index c7fdf77123..3c3009ed01 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt @@ -39,7 +39,9 @@ data class VerificationChooseMethodViewState( val otherCanShowQrCode: Boolean = false, val otherCanScanQrCode: Boolean = false, val qrCodeText: String? = null, - val SASModeAvailable: Boolean = false + val SASModeAvailable: Boolean = false, + val isMe: Boolean = false, + val canCrossSign: Boolean = false ) : MvRxState class VerificationChooseMethodViewModel @AssistedInject constructor( @@ -61,6 +63,10 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( } } + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + verificationRequestUpdated(pr) + } + override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state -> val pvr = session.cryptoService().verificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId) @@ -103,6 +109,8 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( val qrCodeVerificationTransaction = verificationService.getExistingTransaction(args.otherUserId, args.verificationId ?: "") return VerificationChooseMethodViewState(otherUserId = args.otherUserId, + isMe = session.myUserId == pvr?.otherUserId, + canCrossSign = session.cryptoService().crossSigningService().canCrossSign(), transactionId = args.verificationId ?: "", otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(), otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(), diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt index 05ed2f1799..9eb464ab06 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt @@ -84,11 +84,17 @@ class VerificationRequestController @Inject constructor( listener { listener?.onClickDismiss() } } } 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)) - } + val styledText = + if (state.isMe) { + stringProvider.getString(R.string.verify_new_session_notice) + } else { + 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") @@ -119,18 +125,43 @@ class VerificationRequestController @Inject constructor( } is Success -> { if (!pr.invoke().isReady) { - bottomSheetVerificationWaitingItem { - id("waiting") - title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) + if (state.isMe) { + bottomSheetVerificationWaitingItem { + id("waiting") + title(stringProvider.getString(R.string.verification_request_waiting)) + } + } else { + bottomSheetVerificationWaitingItem { + id("waiting") + title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName())) + } } } } } } + + if (state.isMe && state.currentDeviceCanCrossSign) { + + dividerItem { + id("sep_notMe") + } + + bottomSheetVerificationActionItem { + id("wasnote") + title(stringProvider.getString(R.string.verify_new_session_was_not_me)) + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + subTitle(stringProvider.getString(R.string.verify_new_session_compromized)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { listener?.onClickOnWasNotMe() } + } + } } interface Listener { fun onClickOnVerificationStart() + fun onClickOnWasNotMe() fun onClickRecoverFromPassphrase() fun onClickDismiss() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt index 64000d07a1..b6c3659988 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt @@ -69,4 +69,8 @@ class VerificationRequestFragment @Inject constructor( override fun onClickDismiss() { viewModel.handle(VerificationAction.SkipVerification) } + + override fun onClickOnWasNotMe() { + viewModel.itWasNotMe() + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index 85f14e99a8..40b92923ec 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -1,3 +1,4 @@ + /* * Copyright 2019 New Vector Ltd * @@ -19,8 +20,10 @@ package im.vector.riotx.features.home import android.os.Bundle import android.view.LayoutInflater import android.view.View +import androidx.core.content.ContextCompat import androidx.core.view.forEachIndexed import androidx.lifecycle.Observer +import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.bottomnavigation.BottomNavigationItemView @@ -32,11 +35,13 @@ import im.vector.riotx.R import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.ToolbarConfigurable +import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.KeysBackupBanner import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListParams import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView +import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.workers.signout.SignOutViewModel import kotlinx.android.synthetic.main.fragment_home_detail.* import timber.log.Timber @@ -54,6 +59,8 @@ class HomeDetailFragment @Inject constructor( private val unreadCounterBadgeViews = arrayListOf() private val viewModel: HomeDetailViewModel by fragmentViewModel() + private val unknownDeviceDetectorSharedViewModel : UnknownDeviceDetectorSharedViewModel by activityViewModel() + private lateinit var sharedActionViewModel: HomeSharedActionViewModel override fun getLayoutResId() = R.layout.fragment_home_detail @@ -77,6 +84,36 @@ class HomeDetailFragment @Inject constructor( viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode -> switchDisplayMode(displayMode) } + + unknownDeviceDetectorSharedViewModel.subscribe { + it.unknownSessions.invoke()?.let { unknownDevices -> + Timber.v("## Detector - ${unknownDevices.size} Unknown sessions") + unknownDevices.forEachIndexed { index, deviceInfo -> + Timber.v("## Detector - #${index} deviceId:${deviceInfo.deviceId} lastSeenTs:${deviceInfo.lastSeenTs}") + } + if (it.canCrossSign && unknownDevices.isNotEmpty()) { + val newest = unknownDevices.first() + val uid = "ND_${newest.deviceId}" + PopupAlertManager.cancelAlert(uid) + PopupAlertManager.postVectorAlert( + PopupAlertManager.VectorAlert( + uid = uid, + title = getString(R.string.new_session), + description = getString(R.string.new_session_review), + iconId = R.drawable.ic_shield_warning + ).apply { + colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent) + contentAction = Runnable { + (weakCurrentActivity?.get() as? VectorBaseActivity) + ?.navigator + ?.requestSessionVerification(requireContext(), newest.deviceId ?: "") + } + dismissedAction = Runnable {} + } + ) + } + } + } } private fun onGroupChange(groupSummary: GroupSummary?) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt index ecbe460b90..7aa9cfa0a1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt @@ -16,7 +16,15 @@ package im.vector.riotx.features.home +import androidx.lifecycle.MutableLiveData +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.NoOpCancellable +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.singleBuilder import im.vector.riotx.core.platform.VectorSharedActionViewModel +import io.reactivex.android.schedulers.AndroidSchedulers import javax.inject.Inject class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt new file mode 100644 index 0000000000..addcf33684 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt @@ -0,0 +1,75 @@ +/* + * 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.home + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.NoOpCancellable +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.singleBuilder +import im.vector.riotx.core.di.HasScreenInjector +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.EmptyViewEvents +import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.android.schedulers.AndroidSchedulers + +data class UnknownDevicesState( + val unknownSessions: Async?> = Uninitialized, + val canCrossSign: Boolean = false +) : MvRxState + +class UnknownDeviceDetectorSharedViewModel(session: Session, initialState: UnknownDevicesState) : VectorViewModel(initialState) { + + init { + session.rx().liveUserCryptoDevices(session.myUserId) + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { deviceList -> + singleBuilder { + session.cryptoService().getDevicesList(it) + NoOpCancellable + }.map { resp -> + resp.devices?.filter { info -> + deviceList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not() ?: false + }?.sortedByDescending { it.lastSeenTs } + } + .toObservable() + } + .execute { async -> + copy(unknownSessions = async) + } + + session.rx().liveCrossSigningInfo(session.myUserId) + .execute { + copy(canCrossSign = session.cryptoService().crossSigningService().canCrossSign()) + } + } + + override fun handle(action: EmptyAction) {} + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? { + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + return UnknownDeviceDetectorSharedViewModel(session, state) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index a080cabf1b..2e91090ec4 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -83,12 +83,12 @@ class DefaultNavigator @Inject constructor( } } - override fun requestSessionVerification(context: Context) { + override fun requestSessionVerification(context: Context, otherSessionId: String) { val session = sessionHolder.getSafeActiveSession() ?: return val pr = session.cryptoService().verificationService().requestKeyVerification( supportedVerificationMethodsProvider.provide(), session.myUserId, - session.cryptoService().getUserDevices(session.myUserId).map { it.deviceId } + listOf(otherSessionId) ) if (context is VectorBaseActivity) { VerificationBottomSheet.withArgs( diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index fcb3d7bb44..65ef08dd05 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -30,7 +30,7 @@ interface Navigator { fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) - fun requestSessionVerification(context: Context) + fun requestSessionVerification(context: Context, otherSessionId: String) fun waitSessionVerification(context: Context) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index 909d40a74c..5db14fdbd2 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -57,6 +57,8 @@ class VectorSettingsActivity : VectorBaseActivity(), when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG) + EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> + replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG) else -> replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) } @@ -116,6 +118,7 @@ class VectorSettingsActivity : VectorBaseActivity(), const val EXTRA_DIRECT_ACCESS_ROOT = 0 const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1 + const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2 private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" } 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 cf74e83b1f..e33b12d19a 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 @@ -78,6 +78,17 @@ class CrossSigningEpoxyController @Inject constructor( interactionListener?.onResetCrossSigningKeys() } } + + bottomSheetVerificationActionItem { + id("verify") + title(stringProvider.getString(R.string.complete_security)) + titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) + listener { + interactionListener?.verifySession() + } + } } } else if (data.xSigningIsEnableInAccount) { genericItem { diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 45fc3a3781..c18932da1b 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -14,6 +14,20 @@ Refresh + + New Session + Tap to review & verify + Use this session to verify your new one, granting it access to encrypted messages. + This wasn’t me + Your account may be compromised + + If you cancel, you won’t be able to read encrypted messages on this device, and other users won’t trust it + If you cancel, you won’t be able to read encrypted messages on your new device, and other users won’t trust it + + + One of the following may be compromised:\n\n- Your password\n- Your homeserver\n- This device, or the other device\n- The internet connection either device is using\n\nWe recommend you change your password & recovery key in Settings immediately. + +