From 38906084d133b4397684a73f15c33a86760cf4ef Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 19 Dec 2019 10:19:12 +0100 Subject: [PATCH 1/5] WIP --- build.gradle | 2 + .../crypto/sas/SasVerificationService.kt | 2 + .../api/session/events/model/EventType.kt | 1 + .../DefaultSasVerificationService.kt | 65 ++++++++-- .../PendingVerificationRequest.kt | 27 ++++ .../verification/VerificationInfoReady.kt | 38 ++++++ .../verification/VerificationInfoStart.kt | 3 + .../room/EventRelationsAggregationTask.kt | 1 + .../src/main/res/values/strings_RiotX.xml | 17 +++ vector/build.gradle | 3 + .../im/vector/riotx/core/di/FragmentModule.kt | 16 ++- .../vector/riotx/core/di/ScreenComponent.kt | 3 + .../VectorBaseBottomSheetDialogFragment.kt | 20 ++- .../vector/riotx/core/utils/SpannableUtils.kt | 37 ++++++ .../OutgoingVerificationRequestFragment.kt | 77 ++++++++++++ .../OutgoingVerificationRequestViewModel.kt | 73 +++++++++++ .../verification/VerificationBottomSheet.kt | 90 ++++++++++++++ .../VerificationBottomSheetViewModel.kt | 50 ++++++++ .../VerificationChooseMethodFragment.kt | 37 ++++++ .../home/room/detail/RoomDetailAction.kt | 2 + .../home/room/detail/RoomDetailFragment.kt | 11 +- .../home/room/detail/RoomDetailViewModel.kt | 8 +- .../format/DisplayableEventFormatter.kt | 28 +++-- .../notifications/NotifiableEventResolver.kt | 13 +- .../res/layout/bottom_sheet_verification.xml | 55 +++++++++ .../fragment_verification_choose_method.xml | 116 ++++++++++++++++++ .../layout/fragment_verification_request.xml | 73 +++++++++++ vector/src/main/res/values/strings.xml | 2 +- 28 files changed, 833 insertions(+), 37 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt create mode 100644 vector/src/main/res/layout/bottom_sheet_verification.xml create mode 100644 vector/src/main/res/layout/fragment_verification_choose_method.xml create mode 100644 vector/src/main/res/layout/fragment_verification_request.xml diff --git a/build.gradle b/build.gradle index 29351e403f..5b663740e1 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,8 @@ allprojects { includeGroupByRegex "com\\.github\\.jaiselrahman" // And monarchy includeGroupByRegex "com\\.github\\.Zhuinden" + // And QR lib + includeGroupByRegex "com\\.github\\.kenglxn\\.QRGen" } } maven { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt index 3c3c43dbd4..42b5372aae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt @@ -59,6 +59,8 @@ interface SasVerificationService { otherDeviceId: String, callback: MatrixCallback?): String? + fun readyPendingVerificationInDMs(transactionId: String) + // fun transactionUpdated(tx: SasVerificationTransaction) interface SasVerificationListener { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 60d333ec96..1939b1f0e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -73,6 +73,7 @@ object EventType { const val KEY_VERIFICATION_MAC = "m.key.verification.mac" const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel" const val KEY_VERIFICATION_DONE = "m.key.verification.done" + const val KEY_VERIFICATION_READY = "m.key.verification.ready" // Relation Events const val REACTION = "m.reaction" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt index 1d50fc89fe..41925eaa86 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -75,6 +75,16 @@ internal class DefaultSasVerificationService @Inject constructor( // map [sender : [transaction]] private val txMap = HashMap>() + /** + * Map [sender: [PendingVerificationRequest]] + */ + private val incomingRequests = HashMap>() + + /** + * Map [sender: [PendingVerificationRequest]] + */ + private val outgoingRequests = HashMap>() + // Event received from the sync fun onToDeviceEvent(event: Event) { GlobalScope.launch(coroutineDispatchers.crypto) { @@ -190,8 +200,32 @@ internal class DefaultSasVerificationService @Inject constructor( } fun onRoomRequestReceived(event: Event) { - // TODO Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") + val requestInfo = event.getClearContent().toModel() + ?: return + val senderId = event.senderId ?: return + // Remember this request + val requestsForUser = incomingRequests[senderId] + ?: ArrayList().also { + incomingRequests[event.senderId] = it + } + + val pendingVerificationRequest = PendingVerificationRequest( + transactionId = event.eventId, + requestInfo = requestInfo + ) + requestsForUser.add(pendingVerificationRequest) + + /* + * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event + * to begin the verification. + * If both parties send an m.key.verification.start event, and they both specify the same verification method, + * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start + * event is ignored. + * In the case of a single user verifying two of their devices, the device ID is compared instead. + * If both parties send an m.key.verification.start event, but they specify different verification methods, + * the verification should be cancelled with a code of m.unexpected_message. + */ } private suspend fun onRoomStartRequestReceived(event: Event) { @@ -537,17 +571,29 @@ internal class DefaultSasVerificationService @Inject constructor( } override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) { + val requestsForUser = outgoingRequests[userId] + ?: ArrayList().also { + outgoingRequests[userId] = it + } + + val params = requestVerificationDMTask.createParamsAndLocalEcho( + roomId = roomId, + from = credentials.deviceId ?: "", + methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), + to = userId, + cryptoService = cryptoService + ) requestVerificationDMTask.configureWith( - requestVerificationDMTask.createParamsAndLocalEcho( - roomId = roomId, - from = credentials.deviceId ?: "", - methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), - to = userId, - cryptoService = cryptoService - ) + params ) { this.callback = object : MatrixCallback { override fun onSuccess(data: SendResponse) { + params.event.getClearContent().toModel()?.let { + requestsForUser.add(PendingVerificationRequest( + transactionId = data.eventId, + requestInfo = it + )) + } callback?.onSuccess(data.eventId) } @@ -582,6 +628,9 @@ internal class DefaultSasVerificationService @Inject constructor( } } + override fun readyPendingVerificationInDMs(transactionId: String) { + // + } /** * This string must be unique for the pair of users performing verification for the duration that the transaction is valid */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt new file mode 100644 index 0000000000..1d37bcfbfd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 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.matrix.android.internal.crypto.verification + +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent + +/** + * Stores current pending verification requests + */ +internal data class PendingVerificationRequest( + val transactionId: String?, + val requestInfo: MessageVerificationRequestContent?, + val readyInfo: VerificationInfoReady? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt new file mode 100644 index 0000000000..3f3c45901f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 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.matrix.android.internal.crypto.verification + +/** + * A new event type is added to the key verification framework: m.key.verification.ready, + * which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event. + * + * The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly + * with a m.key.verification.start event instead. + */ +internal interface VerificationInfoReady : VerificationInfo { + + val transactionID: String? + + /** + * The ID of the device that sent the m.key.verification.ready message + */ + val fromDevice: String? + + /** + * An array of verification methods that the device supports + */ + val methods: List? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt index 2248a239fb..3160834d57 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt @@ -17,6 +17,9 @@ package im.vector.matrix.android.internal.crypto.verification internal interface VerificationInfoStart : VerificationInfo { + /** + * An array of verification methods that the device supports + */ val method: String? /** * Alice’s device ID diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index cf7a8a9275..f72232b228 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -46,6 +46,7 @@ internal interface EventRelationsAggregationTask : Task%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys. + + You + + Verify by scanning + + Ask the other user to scan this code, or %s to scan theirs + + open your camera + + Verify by emoji + If you can’t scan the code above, verify by comparing a short, unique selection of emoji. + + QR code image + + Verify %s + Waiting for %s… + For extra security, verify %s by checking a one-time code on both your devices.\n\nFor maximum security, do this in person. diff --git a/vector/build.gradle b/vector/build.gradle index c8d474088f..10a17acf2e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -345,6 +345,9 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:1.0.0" + // QR codes +// implementation 'com.github.kenglxn.QRGen:javase:2.6.0' + // TESTS testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' 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 d457581c8e..2c3061ee55 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 @@ -23,10 +23,7 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment -import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragment -import im.vector.riotx.features.crypto.verification.SASVerificationShortCodeFragment -import im.vector.riotx.features.crypto.verification.SASVerificationStartFragment -import im.vector.riotx.features.crypto.verification.SASVerificationVerifiedFragment +import im.vector.riotx.features.crypto.verification.* import im.vector.riotx.features.home.HomeDetailFragment import im.vector.riotx.features.home.HomeDrawerFragment import im.vector.riotx.features.home.LoadingFragment @@ -272,4 +269,15 @@ interface FragmentModule { @IntoMap @FragmentKey(SoftLogoutFragment::class) fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment + + + @Binds + @IntoMap + @FragmentKey(OutgoingVerificationRequestFragment::class) + fun bindVerificationRequestFragment(fragment: OutgoingVerificationRequestFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VerificationChooseMethodFragment::class) + fun bindVerificationMethodChooserFragment(fragment: VerificationChooseMethodFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index e0b14af9d0..3ac9e7c7e9 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -25,6 +25,7 @@ import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.features.MainActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeModule import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity @@ -133,6 +134,8 @@ interface ScreenComponent { fun inject(activity: SoftLogoutActivity) + fun inject(verificationBottomSheet: VerificationBottomSheet) + fun inject(permalinkHandlerActivity: PermalinkHandlerActivity) @Component.Factory diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt index b3a56f48ee..4052a259a7 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -21,17 +21,17 @@ import android.os.Bundle import android.os.Parcelable import android.widget.FrameLayout import androidx.annotation.CallSuper +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders -import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.MvRxView -import com.airbnb.mvrx.MvRxViewId +import com.airbnb.mvrx.* import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.utils.DimensionConverter +import kotlin.reflect.KClass import timber.log.Timber /** @@ -70,6 +70,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() override fun onAttach(context: Context) { screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) viewModelFactory = screenComponent.viewModelFactory() + childFragmentManager.fragmentFactory = screenComponent.fragmentFactory() super.onAttach(context) injectWith(screenComponent) } @@ -121,3 +122,16 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } } } + +inline fun , reified S : MvRxState> T.parentFragmentViewModel( + viewModelClass: KClass = VM::class, + crossinline keyFactory: () -> String = { viewModelClass.java.name } +) where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { + MvRxViewModelProvider.get( + viewModelClass.java, + S::class.java, + FragmentViewModelContext(this.requireActivity(), _fragmentArgsProvider(), this.parentFragment + ?: this), + keyFactory() + ).apply { subscribe(this@parentFragmentViewModel, subscriber = { postInvalidate() }) } +} diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt new file mode 100644 index 0000000000..91b5ae2ffd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 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.utils + +import android.text.Spannable +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import androidx.annotation.ColorInt + +fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable { + if (match.isEmpty()) return this + indexOf(match).takeIf { it != -1 }?.let { start -> + this.setSpan(StyleSpan(typeFace), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return this +} + +fun Spannable.colorizeMatchingText(match: String, @ColorInt color: Int): Spannable { + if (match.isEmpty()) return this + indexOf(match).takeIf { it != -1 }?.let { start -> + this.setSpan(ForegroundColorSpan(color), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return this +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt new file mode 100644 index 0000000000..f652c7bfdd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2019 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 + +import android.graphics.Typeface +import android.os.Bundle +import androidx.core.text.toSpannable +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager +import butterknife.OnClick +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.platform.parentFragmentViewModel +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.core.utils.styleMatchingText +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.themes.ThemeUtils +import kotlinx.android.synthetic.main.fragment_verification_request.* +import javax.inject.Inject + +class OutgoingVerificationRequestFragment @Inject constructor( + val outgoingVerificationRequestViewModelFactory: OutgoingVerificationRequestViewModel.Factory, + val avatarRenderer: AvatarRenderer +) : VectorBaseFragment() { + + private val viewModel by fragmentViewModel(OutgoingVerificationRequestViewModel::class) + + private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun getLayoutResId() = R.layout.fragment_verification_request + + override fun invalidate() = withState(viewModel) { state -> + state.matrixItem?.let { + val styledText = getString(R.string.verification_request_alert_description, it.id) + .toSpannable() + .styleMatchingText(it.id, Typeface.BOLD) + .colorizeMatchingText(it.id, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color)) + verificationRequestText.text = styledText + } + Unit + } + + @OnClick(R.id.verificationStartButton) + fun onClickOnVerificationStart() = withState(viewModel) { state -> + + sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.otherUserId)) + + getParentCoordinatorLayout()?.let { + TransitionManager.beginDelayedTransition(it, AutoTransition().apply { duration = 150 }) + } + parentFragmentManager.commitTransaction { + replace(R.id.bottomSheetFragmentContainer, + VerificationChooseMethodFragment::class.java, + Bundle().apply { putString(MvRx.KEY_ARG, state.otherUserId) }, + "REQUEST" + ) + } + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt new file mode 100644 index 0000000000..0f16b0786b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 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 + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction + + +data class VerificationRequestViewState( + val otherUserId: String = "", + val matrixItem: MatrixItem? = null, + val started: Async = Success(false) +) : MvRxState + +sealed class VerificationAction : VectorViewModelAction { + data class RequestVerificationByDM(val userID: String) : VerificationAction() +} + +class OutgoingVerificationRequestViewModel @AssistedInject constructor( + @Assisted initialState: VerificationRequestViewState, + private val session: Session +) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: VerificationRequestViewState): OutgoingVerificationRequestViewModel + } + + init { + withState { + val user = session.getUser(it.otherUserId) + setState { + copy(matrixItem = user?.toMatrixItem()) + } + } + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): OutgoingVerificationRequestViewModel? { + val fragment: OutgoingVerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.outgoingVerificationRequestViewModelFactory.create(state) + } + + override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? { + val userID: String = viewModelContext.args() + return VerificationRequestViewState(otherUserId = userID) + } + } + + + override fun handle(action: VerificationAction) { + } + +} 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 new file mode 100644 index 0000000000..abac6c6b72 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -0,0 +1,90 @@ +package im.vector.riotx.features.crypto.verification + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.text.toSpannable +import androidx.fragment.app.Fragment +import butterknife.BindView +import butterknife.ButterKnife +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.themes.ThemeUtils +import javax.inject.Inject + +class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { + + @Inject lateinit var outgoingVerificationRequestViewModelFactory: OutgoingVerificationRequestViewModel.Factory + @Inject lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory + @Inject lateinit var avatarRenderer: AvatarRenderer + + + private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + @BindView(R.id.verificationRequestName) + lateinit var otherUserNameText: TextView + + @BindView(R.id.verificationRequestAvatar) + lateinit var otherUserAvatarImageView: ImageView + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.bottom_sheet_verification, container, false) + ButterKnife.bind(this, view) + return view + } + + override fun invalidate() = withState(viewModel) { + when (it.verificationRequestEvent) { + is Uninitialized -> { + if (childFragmentManager.findFragmentByTag("REQUEST") == null) { + //Verification not yet started, put outgoing verification + childFragmentManager.commitTransaction { + setCustomAnimations(R.anim.fade_in, R.anim.fade_out) + replace(R.id.bottomSheetFragmentContainer, + OutgoingVerificationRequestFragment::class.java, + Bundle().apply { putString(MvRx.KEY_ARG, it.userId) }, + "REQUEST" + ) + } + } + } + } + + it.otherUserId?.let { matrixItem -> + val displayName = matrixItem.displayName ?: "" + otherUserNameText.text = getString(R.string.verification_request_alert_title, displayName) + .toSpannable() + .colorizeMatchingText(displayName, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color)) + + avatarRenderer.render(matrixItem, otherUserAvatarImageView) + } + + super.invalidate() + } +} + + +fun Fragment.getParentCoordinatorLayout(): CoordinatorLayout? { + var current = view?.parent as? View + while (current != null) { + if (current is CoordinatorLayout) return current + current = current.parent as? View + } + return null +} 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 new file mode 100644 index 0000000000..578cedc07e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -0,0 +1,50 @@ +package im.vector.riotx.features.crypto.verification + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.platform.VectorViewModel + + +data class VerificationBottomSheetViewState( + val userId: String = "", + val otherUserId: MatrixItem? = null, + val verificationRequestEvent: Async = Uninitialized +) : MvRxState + +class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState, + private val session: Session) + : VectorViewModel(initialState) { + + init { + withState { + session.getUser(it.userId).let { user -> + setState { + copy(otherUserId = user?.toMatrixItem()) + } + } + } + } + @AssistedInject.Factory + interface Factory { + fun create(initialState: VerificationBottomSheetViewState): VerificationBottomSheetViewModel + } + + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? { + val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + val userId: String = viewModelContext.args() + return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(userId)) + } + } + + override fun handle(action: VerificationAction) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt new file mode 100644 index 0000000000..ca441b8a24 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt @@ -0,0 +1,37 @@ +package im.vector.riotx.features.crypto.verification + +import android.os.Bundle +import androidx.transition.AutoTransition +import androidx.transition.ChangeBounds +import androidx.transition.TransitionManager +import butterknife.OnClick +import com.airbnb.mvrx.MvRx +import im.vector.riotx.R +import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.platform.VectorBaseFragment +import javax.inject.Inject + +class VerificationChooseMethodFragment @Inject constructor() : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_verification_choose_method + +// init { +// sharedElementEnterTransition = ChangeBounds() +// sharedElementReturnTransition = ChangeBounds() +// } + + @OnClick(R.id.verificationByEmojiButton) + fun test() { //withState(viewModel) { state -> + getParentCoordinatorLayout()?.let { + TransitionManager.beginDelayedTransition(it, AutoTransition().apply { duration = 150 }) + } + parentFragmentManager.commitTransaction { + // setCustomAnimations(R.anim.fade_in, R.anim.fade_out) + replace(R.id.bottomSheetFragmentContainer, + OutgoingVerificationRequestFragment::class.java, + Bundle().apply { putString(MvRx.KEY_ARG, "@valere35:matrix.org") }, + "REQUEST" + ) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 5d00b09204..013c908f16 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -66,4 +66,6 @@ sealed class RoomDetailAction : VectorViewModelAction { data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction() data class DeclineVerificationRequest(val transactionId: String) : RoomDetailAction() + + data class RequestVerification(val userId: String) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index e983542ad2..1db153e594 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -90,6 +90,7 @@ import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.command.Command +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.room.detail.composer.TextComposerAction @@ -431,7 +432,8 @@ class RoomDetailFragment @Inject constructor( composerLayout.sendButton.setContentDescription(getString(descriptionRes)) avatarRenderer.render( - MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), + MatrixItem.UserItem(event.root.senderId + ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), composerLayout.composerRelatedMessageAvatar ) composerLayout.expand { @@ -923,7 +925,7 @@ class RoomDetailFragment @Inject constructor( } is Success -> { when (val data = result.invoke()) { - is RoomDetailAction.ReportContent -> { + is RoomDetailAction.ReportContent -> { when { data.spam -> { AlertDialog.Builder(requireActivity()) @@ -960,6 +962,11 @@ class RoomDetailFragment @Inject constructor( } } } + is RoomDetailAction.RequestVerification -> { + VerificationBottomSheet().apply { + arguments = Bundle().apply { putString(MvRx.KEY_ARG, data.userId) } + }.show(parentFragmentManager, "REQ") + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index b0c0144d66..a8623c4cb2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -184,8 +184,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() - is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) - is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) + is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) } } @@ -398,7 +398,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro popDraft() } is ParsedCommand.VerifyUser -> { - session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId, null) +// + _requestLiveData.postValue(LiveEvent(Success(RoomDetailAction.RequestVerification(slashCommandResult.userId)))) +// session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId, null) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) popDraft() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index ed6bc9df62..2dfe908365 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -23,13 +23,14 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import me.gujun.android.span.span import javax.inject.Inject class DisplayableEventFormatter @Inject constructor( -// private val sessionHolder: ActiveSessionHolder, + private val sessionHolder: ActiveSessionHolder, private val stringProvider: StringProvider, private val colorProvider: ColorProvider, private val noticeEventFormatter: NoticeEventFormatter @@ -41,32 +42,36 @@ class DisplayableEventFormatter @Inject constructor( return stringProvider.getString(R.string.encrypted_message) } - val senderName = timelineEvent.getDisambiguatedDisplayName() + sessionHolder.getActiveSession().myUserId + val senderName = if (sessionHolder.getActiveSession().myUserId == timelineEvent.root.senderId) + stringProvider.getString(R.string.you) + else + timelineEvent.getDisambiguatedDisplayName() when (timelineEvent.root.getClearType()) { EventType.MESSAGE -> { timelineEvent.getLastMessageContent()?.let { messageContent -> when (messageContent.type) { MessageType.MSGTYPE_VERIFICATION_REQUEST -> { - return simpleFormat(senderName, stringProvider.getString(R.string.verification_request), appendAuthor) + return simpleFormat(senderName, stringProvider.getString(R.string.verification_request).italicSpan(), appendAuthor) } MessageType.MSGTYPE_IMAGE -> { - return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor) + return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image).italicSpan(), appendAuthor) } MessageType.MSGTYPE_AUDIO -> { - return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor) + return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file).italicSpan(), appendAuthor) } MessageType.MSGTYPE_VIDEO -> { - return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor) + return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video).italicSpan(), appendAuthor) } MessageType.MSGTYPE_FILE -> { - return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor) + return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file).italicSpan(), appendAuthor) } MessageType.MSGTYPE_TEXT -> { if (messageContent.isReply()) { // Skip reply prefix, and show important // TODO add a reply image span ? - return simpleFormat(senderName, timelineEvent.getTextEditableContent() + return simpleFormat(senderName, timelineEvent.getTextEditableContent()?.let { "↩︎ $it" } ?: messageContent.body, appendAuthor) } else { return simpleFormat(senderName, messageContent.body, appendAuthor) @@ -101,4 +106,11 @@ class DisplayableEventFormatter @Inject constructor( body } } + + private fun String.italicSpan(): CharSequence { + return span { + text = this@italicSpan + textStyle = "italic" + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index e38e7d548a..a75c23f65c 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt @@ -26,14 +26,14 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getEditedEventId -import im.vector.matrix.android.api.session.room.timeline.getLastMessageBody import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter import timber.log.Timber -import java.util.UUID +import java.util.* import javax.inject.Inject /** @@ -43,6 +43,7 @@ import javax.inject.Inject * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk. */ class NotifiableEventResolver @Inject constructor(private val stringProvider: StringProvider, + private val displayableEventFormatter: DisplayableEventFormatter, private val noticeEventFormatter: NoticeEventFormatter) { // private val eventDisplay = RiotEventDisplay(context) @@ -86,13 +87,11 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St private fun resolveMessageEvent(event: TimelineEvent, session: Session): NotifiableEvent? { // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) - + val body = displayableEventFormatter.format(event, false).toString() if (room == null) { Timber.e("## Unable to resolve room for eventId [$event]") // Ok room is not known in store, but we can still display something - val body = - event.getLastMessageBody() - ?: stringProvider.getString(R.string.notification_unknown_new_event) + val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val senderDisplayName = event.getDisambiguatedDisplayName() @@ -125,8 +124,6 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } } - val body = event.getLastMessageBody() - ?: stringProvider.getString(R.string.notification_unknown_new_event) val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.getDisambiguatedDisplayName() diff --git a/vector/src/main/res/layout/bottom_sheet_verification.xml b/vector/src/main/res/layout/bottom_sheet_verification.xml new file mode 100644 index 0000000000..7293434f0d --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_verification.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_verification_choose_method.xml b/vector/src/main/res/layout/fragment_verification_choose_method.xml new file mode 100644 index 0000000000..70411c69b6 --- /dev/null +++ b/vector/src/main/res/layout/fragment_verification_choose_method.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_verification_request.xml b/vector/src/main/res/layout/fragment_verification_request.xml new file mode 100644 index 0000000000..1ea7a1233d --- /dev/null +++ b/vector/src/main/res/layout/fragment_verification_request.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 07a2f40bbd..8c1b2b819d 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1168,7 +1168,7 @@ Your unverified device \'%s\' is requesting encryption keys. An unverified device is requesting encryption keys.\nDevice name: %1$s\nLast seen: %2$s\nIf you didn’t log in on another device, ignore this request. - Start verification + Start Verification Verify Share without verifying From 308b15b908aecabb29bcb70df95d991dcd982c0c Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 26 Dec 2019 16:58:14 +0100 Subject: [PATCH 2/5] Fix / m.key.verification.key was not sent in toDevice mode --- .../android/internal/crypto/model/rest/KeyVerificationKey.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt index bf1482ac9f..d012d03add 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt @@ -43,6 +43,8 @@ internal data class KeyVerificationKey( } } + override fun toSendToDeviceObject() = this + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || key.isNullOrBlank()) { return false From 4c0cbca4cb0548e3501472cdd5b11a8780cf7d92 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 26 Dec 2019 16:59:36 +0100 Subject: [PATCH 3/5] Support .verification.ready event --- .../crypto/sas/SasVerificationService.kt | 12 +- .../android/api/session/room/RoomService.kt | 2 + .../MessageVerificationReadyContent.kt | 57 +++++++ .../crypto/model/rest/KeyVerificationReady.kt | 38 +++++ .../crypto/model/rest/KeyVerificationStart.kt | 1 + ...faultIncomingSASVerificationTransaction.kt | 7 +- .../DefaultSasVerificationService.kt | 155 +++++++++++++++--- .../PendingVerificationRequest.kt | 16 +- .../crypto/verification/SasTransport.kt | 3 + .../verification/SasTransportRoomMessage.kt | 11 ++ .../verification/SasTransportToDevice.kt | 8 + .../crypto/verification/VerificationInfo.kt | 2 +- .../verification/VerificationInfoReady.kt | 6 +- .../VerificationMessageLiveObserver.kt | 10 ++ .../session/room/DefaultRoomService.kt | 14 ++ .../room/EventRelationsAggregationTask.kt | 10 +- .../src/main/res/values/strings_RiotX.xml | 2 +- 17 files changed, 319 insertions(+), 35 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationReadyContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationReady.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt index 42b5372aae..a24963ff66 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.crypto.sas import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest /** * https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework @@ -39,6 +40,8 @@ interface SasVerificationService { fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction? + fun getExistingVerificationRequest(otherUser: String): List? + /** * Shortcut for KeyVerificationStart.VERIF_METHOD_SAS * @see beginKeyVerification @@ -50,7 +53,7 @@ interface SasVerificationService { */ fun beginKeyVerification(method: String, userId: String, deviceID: String): String? - fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) + fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) : PendingVerificationRequest fun beginKeyVerificationInDMs(method: String, transactionId: String, @@ -59,13 +62,16 @@ interface SasVerificationService { otherDeviceId: String, callback: MatrixCallback?): String? - fun readyPendingVerificationInDMs(transactionId: String) + fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String) // fun transactionUpdated(tx: SasVerificationTransaction) interface SasVerificationListener { fun transactionCreated(tx: SasVerificationTransaction) fun transactionUpdated(tx: SasVerificationTransaction) - fun markedAsManuallyVerified(userId: String, deviceId: String) + fun markedAsManuallyVerified(userId: String, deviceId: String) {} + + fun verificationRequestCreated(pr: PendingVerificationRequest) {} + fun verificationRequestUpdated(pr: PendingVerificationRequest) {} } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index ba3b5ded78..fe110b7b9c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -89,4 +89,6 @@ interface RoomService { fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable + + fun getExistingDirectRoomWithUser(otherUserId: String) : Room? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationReadyContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationReadyContent.kt new file mode 100644 index 0000000000..af02118d65 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationReadyContent.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019 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.matrix.android.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.RelationType +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent +import im.vector.matrix.android.internal.crypto.verification.MessageVerificationReadyFactory +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationReadyContent( + @Json(name = "from_device") override val fromDevice: String? = null, + @Json(name = "methods") override val methods: List? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoReady { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun toEventContent() = this.toContent() + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) { + return false + } + return true + } + + companion object : MessageVerificationReadyFactory { + override fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady { + return MessageVerificationReadyContent( + fromDevice = fromDevice, + methods = methods, + relatesTo = RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationReady.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationReady.kt new file mode 100644 index 0000000000..7df12b22c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationReady.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 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.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationReady( + @Json(name = "from_device") override val fromDevice: String?, + //TODO add qr? + @Json(name = "methods") override val methods: List? = listOf(KeyVerificationStart.VERIF_METHOD_SAS), + @Json(name = "transaction_id") override var transactionID: String? = null +) : SendToDeviceObject, VerificationInfoReady { + + override fun toSendToDeviceObject() = this + + override fun isValid(): Boolean { + return !transactionID.isNullOrBlank() && !fromDevice.isNullOrBlank() && !methods.isNullOrEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt index e8c0334539..e25ed10a6a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt @@ -43,6 +43,7 @@ data class KeyVerificationStart( companion object { const val VERIF_METHOD_SAS = "m.sas.v1" + const val VERIF_METHOD_SCAN = "m.qr_code.scan.v1" } override fun isValid(): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt index 5eff26a5bb..349d6a79ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt @@ -33,7 +33,8 @@ internal class DefaultIncomingSASVerificationTransaction( private val cryptoStore: IMXCryptoStore, deviceFingerprint: String, transactionId: String, - otherUserID: String + otherUserID: String, + val autoAccept: Boolean = false ) : SASVerificationTransaction( setDeviceVerificationAction, credentials, @@ -76,6 +77,10 @@ internal class DefaultIncomingSASVerificationTransaction( this.startReq = startReq state = SasVerificationTxState.OnStarted this.otherDeviceId = startReq.fromDevice + + if (autoAccept) { + performAccept() + } } override fun performAccept() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt index 41925eaa86..9a466da192 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -78,12 +78,8 @@ internal class DefaultSasVerificationService @Inject constructor( /** * Map [sender: [PendingVerificationRequest]] */ - private val incomingRequests = HashMap>() + private val pendingRequests = HashMap>() - /** - * Map [sender: [PendingVerificationRequest]] - */ - private val outgoingRequests = HashMap>() // Event received from the sync fun onToDeviceEvent(event: Event) { @@ -130,6 +126,9 @@ internal class DefaultSasVerificationService @Inject constructor( EventType.KEY_VERIFICATION_MAC -> { onRoomMacReceived(event) } + EventType.KEY_VERIFICATION_READY -> { + onRoomReadyReceived(event) + } EventType.KEY_VERIFICATION_DONE -> { // TODO? } @@ -185,6 +184,31 @@ internal class DefaultSasVerificationService @Inject constructor( } } + + private fun dispatchRequestAdded(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchRequestUpdated(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, deviceID, @@ -204,13 +228,21 @@ internal class DefaultSasVerificationService @Inject constructor( val requestInfo = event.getClearContent().toModel() ?: return val senderId = event.senderId ?: return + + if (requestInfo.toUserId != credentials.userId) { + //I should ignore this, it's not for me + Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me") + return + } // Remember this request - val requestsForUser = incomingRequests[senderId] + val requestsForUser = pendingRequests[senderId] ?: ArrayList().also { - incomingRequests[event.senderId] = it + pendingRequests[event.senderId] = it } val pendingVerificationRequest = PendingVerificationRequest( + isIncoming = true, + otherUserId = senderId,//requestInfo.toUserId, transactionId = event.eventId, requestInfo = requestInfo ) @@ -320,6 +352,10 @@ internal class DefaultSasVerificationService @Inject constructor( // Ok we can create if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) { Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") + // If there is a corresponding request, we can auto accept + // as we are the one requesting in first place (or we accepted the request) + val autoAccept = getExistingVerificationRequest(otherUserId)?.any { it.transactionId == startReq.transactionID } + ?: false val tx = DefaultIncomingSASVerificationTransaction( // this, setDeviceVerificationAction, @@ -327,7 +363,8 @@ internal class DefaultSasVerificationService @Inject constructor( cryptoStore, myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionID!!, - otherUserId).also { txConfigure(it) } + otherUserId, + autoAccept).also { txConfigure(it) } addTransaction(tx) tx.acceptVerificationEvent(otherUserId, startReq) } else { @@ -490,6 +527,21 @@ internal class DefaultSasVerificationService @Inject constructor( handleMacReceived(event.senderId, macReq) } + private fun onRoomReadyReceived(event: Event) { + val readyReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + if (readyReq == null || readyReq.isValid().not() || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid ready request") + // TODO should we cancel? + return + } + handleReadyReceived(event.senderId, readyReq) + } + private fun onMacReceived(event: Event) { val macReq = event.getClearContent().toModel()!! @@ -515,12 +567,27 @@ internal class DefaultSasVerificationService @Inject constructor( } } + private fun handleReadyReceived(senderId: String, readyReq: VerificationInfoReady) { + val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionID } + if (existingRequest == null) { + Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionID} fromDevice ${readyReq.fromDevice}") + return + } + updateOutgoingPendingRequest(existingRequest.copy(readyInfo = readyReq)) + } + override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? { synchronized(lock = txMap) { return txMap[otherUser]?.get(tid) } } + override fun getExistingVerificationRequest(otherUser: String): List? { + synchronized(lock = pendingRequests) { + return pendingRequests[otherUser] + } + } + private fun getExistingTransactionsForUser(otherUser: String): Collection? { synchronized(txMap) { return txMap[otherUser]?.values @@ -534,13 +601,11 @@ internal class DefaultSasVerificationService @Inject constructor( } private fun addTransaction(tx: VerificationTransaction) { - tx.otherUserId.let { otherUserId -> - synchronized(txMap) { - val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() } - txInnerMap[tx.transactionId] = tx - dispatchTxAdded(tx) - tx.addListener(this) - } + synchronized(txMap) { + val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() } + txInnerMap[tx.transactionId] = tx + dispatchTxAdded(tx) + tx.addListener(this) } } @@ -570,12 +635,14 @@ internal class DefaultSasVerificationService @Inject constructor( } } - override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) { - val requestsForUser = outgoingRequests[userId] + override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) + : PendingVerificationRequest { + Timber.i("Requesting verification to user: $userId in room ${roomId}") + val requestsForUser = pendingRequests[userId] ?: ArrayList().also { - outgoingRequests[userId] = it + pendingRequests[userId] = it } - + val params = requestVerificationDMTask.createParamsAndLocalEcho( roomId = roomId, from = credentials.deviceId ?: "", @@ -583,13 +650,21 @@ internal class DefaultSasVerificationService @Inject constructor( to = userId, cryptoService = cryptoService ) + val verificationRequest = PendingVerificationRequest( + isIncoming = false, + localID = params.event.eventId ?: "", + otherUserId = userId + ) + requestsForUser.add(verificationRequest) + dispatchRequestAdded(verificationRequest) + requestVerificationDMTask.configureWith( params ) { this.callback = object : MatrixCallback { override fun onSuccess(data: SendResponse) { params.event.getClearContent().toModel()?.let { - requestsForUser.add(PendingVerificationRequest( + updateOutgoingPendingRequest(verificationRequest.copy( transactionId = data.eventId, requestInfo = it )) @@ -604,6 +679,24 @@ internal class DefaultSasVerificationService @Inject constructor( constraints = TaskConstraints(true) retryCount = 3 }.executeBy(taskExecutor) + + return verificationRequest + } + + private fun updateOutgoingPendingRequest(updated: PendingVerificationRequest) { + val requestsForUser = pendingRequests[updated.otherUserId] + ?: ArrayList().also { + pendingRequests[updated.otherUserId] = it + } + val index = requestsForUser.indexOfFirst { + it.transactionId == updated.transactionId + || it.transactionId == null && it.localID == updated.localID + } + if (index != -1) { + requestsForUser.removeAt(index) + } + requestsForUser.add(updated) + dispatchRequestUpdated(updated) } override fun beginKeyVerificationInDMs(method: String, transactionId: String, roomId: String, @@ -628,9 +721,27 @@ internal class DefaultSasVerificationService @Inject constructor( } } - override fun readyPendingVerificationInDMs(transactionId: String) { - // + override fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String) { + // Let's find the related request + getExistingVerificationRequest(otherUserId)?.find { it.transactionId == transactionId }?.let { + //we need to send a ready event, with matching methods + val transport = sasTransportRoomMessageFactory.createTransport(roomId, cryptoService, null) + val methods = it.requestInfo?.methods?.intersect(listOf(KeyVerificationStart.VERIF_METHOD_SAS))?.toList() + if (methods.isNullOrEmpty()) { + Timber.i("Cannot ready this request, no common methods found txId:$transactionId") + return@let + } + //TODO this is not yet related to a transaction, maybe we should use another method like for cancel? + val readyMsg = transport.createReady(transactionId, credentials.deviceId ?: "", methods) + transport.sendToOther(EventType.KEY_VERIFICATION_READY, readyMsg, + SasVerificationTxState.None, + CancelCode.User, + null // TODO handle error? + ) + updateOutgoingPendingRequest(it.copy(readyInfo = readyMsg)) + } } + /** * This string must be unique for the pair of users performing verification for the duration that the transaction is valid */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt index 1d37bcfbfd..6394f26178 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -16,12 +16,20 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent +import java.util.* /** * Stores current pending verification requests */ -internal data class PendingVerificationRequest( - val transactionId: String?, - val requestInfo: MessageVerificationRequestContent?, +data class PendingVerificationRequest( + val isIncoming: Boolean = false, + val localID: String = UUID.randomUUID().toString(), + val otherUserId: String, + val transactionId: String? = null, + val requestInfo: MessageVerificationRequestContent? = null, val readyInfo: VerificationInfoReady? = null -) +) { + + val isReady: Boolean = readyInfo != null + val isSent: Boolean = transactionId != null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt index ae5f55b662..1befc74525 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt @@ -58,4 +58,7 @@ internal interface SasTransport { shortAuthenticationStrings: List) : VerificationInfoStart fun createMac(tid: String, mac: Map, keys: String): VerificationInfoMac + + + fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt index 91adbbd705..fa4c370a90 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt @@ -167,6 +167,17 @@ internal class SasTransportRoomMessage( ) ) } + + override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { + return MessageVerificationReadyContent( + fromDevice = fromDevice, + relatesTo = RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = tid + ), + methods = methods + ) + } } internal class SasTransportRoomMessageFactory @Inject constructor( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt index 85e9099972..7a69f212a3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt @@ -126,6 +126,14 @@ internal class SasTransportToDevice( messageAuthenticationCodes, shortAuthenticationStrings) } + + override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { + return KeyVerificationReady( + transactionID = tid, + fromDevice = fromDevice, + methods = methods + ) + } } internal class SasTransportToDeviceFactory @Inject constructor( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt index 44a65aa926..5fe5c62edd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject -internal interface VerificationInfo { +interface VerificationInfo { fun toEventContent(): Content? = null fun toSendToDeviceObject(): SendToDeviceObject? = null fun isValid() : Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt index 3f3c45901f..87436f5686 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt @@ -22,7 +22,7 @@ package im.vector.matrix.android.internal.crypto.verification * The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly * with a m.key.verification.start event instead. */ -internal interface VerificationInfoReady : VerificationInfo { +interface VerificationInfoReady : VerificationInfo { val transactionID: String? @@ -36,3 +36,7 @@ internal interface VerificationInfoReady : VerificationInfo { */ val methods: List? } + +internal interface MessageVerificationReadyFactory { + fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt index 2fee568895..76511c9650 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt @@ -56,6 +56,7 @@ internal class VerificationMessageLiveObserver @Inject constructor( EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_READY, EventType.MESSAGE, EventType.ENCRYPTED) ) @@ -141,6 +142,14 @@ internal class VerificationMessageLiveObserver @Inject constructor( it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } } } + } else if (EventType.KEY_VERIFICATION_READY == event.type) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") + it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } } else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) { event.getClearContent().toModel()?.relatesTo?.eventId?.let { transactionsHandledByOtherDevice.remove(it) @@ -162,6 +171,7 @@ internal class VerificationMessageLiveObserver @Inject constructor( EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_DONE -> { sasVerificationService.onRoomEvent(event) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index b53fa3ce33..20908489e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -86,6 +86,20 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona }) } + override fun getExistingDirectRoomWithUser(otherUserId: String): Room? { + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val roomId = RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll()?.let { dms -> + dms.firstOrNull { + it.otherMemberIds.contains(otherUserId) + } + } + ?.roomId ?: return null + return RoomEntity.where(realm, roomId).findFirst()?.let { roomFactory.create(roomId) } + } + } + override fun liveRoomSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { realm -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index f72232b228..eb7e914d89 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -49,14 +49,16 @@ enum class VerificationState { UNKNOWN, REQUEST, WAITING, + READY, CANCELED_BY_ME, CANCELED_BY_OTHER, DONE } -fun VerificationState.isCanceled() : Boolean { +fun VerificationState.isCanceled(): Boolean { return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER } + /** * Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base. */ @@ -119,6 +121,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_KEY -> { Timber.v("## SAS REF in room $roomId for event ${event.eventId}") event.content.toModel()?.relatesTo?.let { @@ -459,6 +462,9 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( EventType.KEY_VERIFICATION_DONE -> { updateVerificationState(currentState, VerificationState.DONE) } + EventType.KEY_VERIFICATION_READY -> { + updateVerificationState(currentState, VerificationState.READY) + } else -> VerificationState.REQUEST } @@ -474,7 +480,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( } } - private fun updateVerificationState(oldState: VerificationState?, newState: VerificationState) : VerificationState { + private fun updateVerificationState(oldState: VerificationState?, newState: VerificationState): VerificationState { // Cancel is always prioritary ? // Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to // consider as canceled diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml index 034dabcd59..7964b1d1b8 100644 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml @@ -30,7 +30,7 @@ open your camera - Verify by emoji + Verify by Emoji If you can’t scan the code above, verify by comparing a short, unique selection of emoji. QR code image From 4edd5e3530c493f8f5d16f526c636c221bc1c8e3 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 27 Dec 2019 15:46:20 +0100 Subject: [PATCH 4/5] Added SAS do not match api --- .../api/session/crypto/sas/SasVerificationTransaction.kt | 3 +++ .../crypto/verification/SASVerificationTransaction.kt | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt index 9610daf294..b98c5c0167 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt @@ -47,4 +47,7 @@ interface SasVerificationTransaction { * both short codes do match */ fun userHasVerifiedShortCode() + + + fun shortCodeDoNotMatch() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt index 31d6fd4b5c..4e0accd8e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt @@ -169,6 +169,11 @@ internal abstract class SASVerificationTransaction( } // if not wait for it } + override fun shortCodeDoNotMatch() { + Timber.v("## SAS short code do not match for id:$transactionId") + cancel(CancelCode.MismatchedSas) + } + override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) { when (info) { is VerificationInfoStart -> onVerificationStart(info) From a73cd61b9661cdddcc0d1382db29348f0bcaa97e Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 30 Dec 2019 13:44:13 +0100 Subject: [PATCH 5/5] WIP --- .../crypto/sas/SasVerificationService.kt | 2 + .../tasks/RoomVerificationUpdateTask.kt | 163 +++++++++++++++ .../DefaultSasVerificationService.kt | 38 +++- .../PendingVerificationRequest.kt | 6 +- .../SASVerificationTransaction.kt | 3 +- .../VerificationMessageLiveObserver.kt | 130 +----------- .../im/vector/riotx/core/di/FragmentModule.kt | 16 +- .../vector/riotx/core/utils/SpannableUtils.kt | 21 ++ .../OutgoingVerificationRequestViewModel.kt | 73 ------- .../SASVerificationCodeFragment.kt | 164 +++++++++++++++ .../SASVerificationCodeViewModel.kt | 170 +++++++++++++++ .../SASVerificationStartFragment.kt | 2 +- .../verification/SasVerificationViewModel.kt | 1 + .../verification/VerificationBottomSheet.kt | 180 ++++++++++++++-- .../VerificationBottomSheetViewModel.kt | 162 +++++++++++++-- .../VerificationChooseMethodFragment.kt | 78 ++++--- .../VerificationChooseMethodViewModel.kt | 77 +++++++ .../VerificationConclusionFragment.kt | 73 +++++++ .../VerificationConclusionViewModel.kt | 61 ++++++ ...ment.kt => VerificationRequestFragment.kt} | 50 +++-- .../VerificationRequestViewModel.kt | 109 ++++++++++ .../home/room/detail/RoomDetailFragment.kt | 20 +- .../home/room/detail/RoomDetailViewModel.kt | 25 ++- .../timeline/factory/MessageItemFactory.kt | 24 +-- .../timeline/factory/TimelineItemFactory.kt | 1 + .../timeline/format/NoticeEventFormatter.kt | 1 + .../helper/TimelineDisplayableEvents.kt | 3 +- .../fragment_bottom_sas_verification_code.xml | 195 ++++++++++++++++++ .../fragment_verification_choose_method.xml | 5 +- .../fragment_verification_conclusion.xml | 65 ++++++ vector/src/main/res/values/strings.xml | 2 +- vector/src/main/res/values/strings_riotX.xml | 24 ++- 32 files changed, 1622 insertions(+), 322 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt rename vector/src/main/java/im/vector/riotx/features/crypto/verification/{OutgoingVerificationRequestFragment.kt => VerificationRequestFragment.kt} (61%) create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt create mode 100644 vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml create mode 100644 vector/src/main/res/layout/fragment_verification_conclusion.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt index a24963ff66..418b7ac508 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt @@ -42,6 +42,8 @@ interface SasVerificationService { fun getExistingVerificationRequest(otherUser: String): List? + fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? + /** * Shortcut for KeyVerificationStart.VERIF_METHOD_SAS * @see beginKeyVerification diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt new file mode 100644 index 0000000000..c01c121890 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2019 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.matrix.android.internal.crypto.tasks + +import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService +import im.vector.matrix.android.internal.di.DeviceId +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.task.Task +import io.realm.RealmConfiguration +import timber.log.Timber +import java.util.* +import javax.inject.Inject + +internal interface RoomVerificationUpdateTask : Task { + data class Params( + val events: List, + val sasVerificationService: DefaultSasVerificationService, + val cryptoService: CryptoService + ) +} + +internal class DefaultRoomVerificationUpdateTask @Inject constructor( + @UserId private val userId: String, + @DeviceId private val deviceId: String?, + private val cryptoService: CryptoService) : RoomVerificationUpdateTask { + + companion object { + private val transactionsHandledByOtherDevice = ArrayList() + } + + override suspend fun execute(params: RoomVerificationUpdateTask.Params): Unit { + + // TODO ignore initial sync or back pagination? + + val now = System.currentTimeMillis() + val tooInThePast = now - (10 * 60 * 1000) + val fiveMinInMs = 5 * 60 * 1000 + val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs + + params.events.forEach { event -> + Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") + Timber.v("## SAS Verification live observer: received msgId: $event") + + // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, + // the message should be ignored by the receiver. + val ageLocalTs = event.ageLocalTs + if (ageLocalTs != null && (now - ageLocalTs) > fiveMinInMs) { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (age: ${(now - ageLocalTs)})") + return@forEach + } else { + val eventOrigin = event.originServerTs ?: -1 + if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (ts: $eventOrigin") + return@forEach + } + } + + // decrypt if needed? + if (event.isEncrypted() && event.mxDecryptionResult == null) { + // TODO use a global event decryptor? attache to session and that listen to new sessionId? + // for now decrypt sync + try { + val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString()) + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.e("## SAS Failed to decrypt event: ${event.eventId}") + } + } + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") + + if (event.senderId == userId) { + // If it's send from me, we need to keep track of Requests or Start + // done from another device of mine + + if (EventType.MESSAGE == event.type) { + val msgType = event.getClearContent().toModel()?.type + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is requested from another device + Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ") + event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } + } + } else if (EventType.KEY_VERIFICATION_START == event.type) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") + it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } + } else if (EventType.KEY_VERIFICATION_READY == event.type) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") + it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } + } else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) { + event.getClearContent().toModel()?.relatesTo?.eventId?.let { + transactionsHandledByOtherDevice.remove(it) + } + } + + return@forEach + } + + val relatesTo = event.getClearContent().toModel()?.relatesTo?.eventId + if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) { + // Ignore this event, it is directed to another of my devices + Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ") + return@forEach + } + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_DONE -> { + params.sasVerificationService.onRoomEvent(event) + } + EventType.MESSAGE -> { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { + params.sasVerificationService.onRoomRequestReceived(event) + } + } + } + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt index 9a466da192..d214769e97 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -223,7 +223,7 @@ internal class DefaultSasVerificationService @Inject constructor( } } - fun onRoomRequestReceived(event: Event) { + suspend fun onRoomRequestReceived(event: Event) { Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") val requestInfo = event.getClearContent().toModel() ?: return @@ -234,6 +234,14 @@ internal class DefaultSasVerificationService @Inject constructor( Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me") return } + + if(checkKeysAreDownloaded(senderId, requestInfo.fromDevice) == null) { + //I should ignore this, it's not for me + Timber.e("## SAS Verification device ${requestInfo.fromDevice} is not knwon") + // TODO cancel? + return + } + // Remember this request val requestsForUser = pendingRequests[senderId] ?: ArrayList().also { @@ -329,7 +337,7 @@ internal class DefaultSasVerificationService @Inject constructor( private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? { Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}") - if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) { + if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) { Timber.v("## SAS onStartRequestReceived $startReq") val tid = startReq.transactionID!! val existing = getExistingTransaction(otherUserId, tid) @@ -382,11 +390,11 @@ internal class DefaultSasVerificationService @Inject constructor( } private suspend fun checkKeysAreDownloaded(otherUserId: String, - startReq: VerificationInfoStart): MXUsersDevicesMap? { + fromDevice: String): MXUsersDevicesMap? { return try { val keys = deviceListManager.downloadKeys(listOf(otherUserId), true) val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null - keys.takeIf { deviceIds.contains(startReq.fromDevice) } + keys.takeIf { deviceIds.contains(fromDevice) } } catch (e: Exception) { null } @@ -404,6 +412,10 @@ internal class DefaultSasVerificationService @Inject constructor( // TODO should we cancel? return } + getExistingVerificationRequest(event.senderId ?: "", cancelReq.transactionID)?.let { + updateOutgoingPendingRequest(it.copy(cancelConclusion = safeValueOf(cancelReq.code))) + // Should we remove it from the list? + } handleOnCancel(event.senderId!!, cancelReq) } @@ -527,7 +539,7 @@ internal class DefaultSasVerificationService @Inject constructor( handleMacReceived(event.senderId, macReq) } - private fun onRoomReadyReceived(event: Event) { + private suspend fun onRoomReadyReceived(event: Event) { val readyReq = event.getClearContent().toModel() ?.copy( // relates_to is in clear in encrypted payload @@ -539,6 +551,13 @@ internal class DefaultSasVerificationService @Inject constructor( // TODO should we cancel? return } + if(checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) { + Timber.e("## SAS Verification device ${readyReq.fromDevice} is not knwown") + // TODO cancel? + return + } + + handleReadyReceived(event.senderId, readyReq) } @@ -588,6 +607,12 @@ internal class DefaultSasVerificationService @Inject constructor( } } + override fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? { + synchronized(lock = pendingRequests) { + return tid?.let { tid -> pendingRequests[otherUser]?.firstOrNull { it.transactionId == tid } } + } + } + private fun getExistingTransactionsForUser(otherUser: String): Collection? { synchronized(txMap) { return txMap[otherUser]?.values @@ -637,7 +662,8 @@ internal class DefaultSasVerificationService @Inject constructor( override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) : PendingVerificationRequest { - Timber.i("Requesting verification to user: $userId in room ${roomId}") + + Timber.i("## SAS Requesting verification to user: $userId in room ${roomId}") val requestsForUser = pendingRequests[userId] ?: ArrayList().also { pendingRequests[userId] = it diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt index 6394f26178..6447b8668b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -15,6 +15,7 @@ */ package im.vector.matrix.android.internal.crypto.verification +import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import java.util.* @@ -27,7 +28,10 @@ data class PendingVerificationRequest( val otherUserId: String, val transactionId: String? = null, val requestInfo: MessageVerificationRequestContent? = null, - val readyInfo: VerificationInfoReady? = null + val readyInfo: VerificationInfoReady? = null, + val cancelConclusion: CancelCode? = null, + val isSuccessful : Boolean = false + ) { val isReady: Boolean = readyInfo != null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt index 4e0accd8e0..9df9248993 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt @@ -227,13 +227,14 @@ internal abstract class SASVerificationTransaction( val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() if (otherDeviceKey == null) { - Timber.e("Verification: Could not find device $keyIDNoPrefix to verify") + Timber.e("## SAS Verification: Could not find device $keyIDNoPrefix to verify") // just ignore and continue return@forEach } val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) if (mac != theirMac?.mac?.get(it)) { // WRONG! + Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix") cancel(CancelCode.MismatchedKeys) return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt index 76511c9650..5b871b31b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt @@ -17,38 +17,31 @@ package im.vector.matrix.android.internal.crypto.verification import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.LocalEcho -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.* -import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask +import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.types -import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration import io.realm.RealmResults -import timber.log.Timber -import java.util.* import javax.inject.Inject -import kotlin.collections.ArrayList internal class VerificationMessageLiveObserver @Inject constructor( @SessionDatabase realmConfiguration: RealmConfiguration, - @UserId private val userId: String, - @DeviceId private val deviceId: String?, + private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask, private val cryptoService: CryptoService, private val sasVerificationService: DefaultSasVerificationService, private val taskExecutor: TaskExecutor ) : RealmLiveEntityObserver(realmConfiguration) { - override val query = Monarchy.Query { + override val query = Monarchy.Query { EventEntity.types(it, listOf( EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_ACCEPT, @@ -62,11 +55,8 @@ internal class VerificationMessageLiveObserver @Inject constructor( ) } - val transactionsHandledByOtherDevice = ArrayList() - override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - // TODO do that in a task - // TODO how to ignore when it's an initial sync? + // Should we ignore when it's an initial sync? val events = changeSet.insertions .asSequence() .mapNotNull { results[it]?.asDomain() } @@ -76,111 +66,9 @@ internal class VerificationMessageLiveObserver @Inject constructor( } .toList() - // TODO ignore initial sync or back pagination? + roomVerificationUpdateTask.configureWith( + RoomVerificationUpdateTask.Params(events, sasVerificationService, cryptoService) + ).executeBy(taskExecutor) - val now = System.currentTimeMillis() - val tooInThePast = now - (10 * 60 * 1000) - val fiveMinInMs = 5 * 60 * 1000 - val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs - - events.forEach { event -> - Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") - Timber.v("## SAS Verification live observer: received msgId: $event") - - // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, - // the message should be ignored by the receiver. - val ageLocalTs = event.ageLocalTs - if (ageLocalTs != null && (now - ageLocalTs) > fiveMinInMs) { - Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (age: ${(now - ageLocalTs)})") - return@forEach - } else { - val eventOrigin = event.originServerTs ?: -1 - if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) { - Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (ts: $eventOrigin") - return@forEach - } - } - - // decrypt if needed? - if (event.isEncrypted() && event.mxDecryptionResult == null) { - // TODO use a global event decryptor? attache to session and that listen to new sessionId? - // for now decrypt sync - try { - val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString()) - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.e("## SAS Failed to decrypt event: ${event.eventId}") - } - } - Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") - - if (event.senderId == userId) { - // If it's send from me, we need to keep track of Requests or Start - // done from another device of mine - - if (EventType.MESSAGE == event.type) { - val msgType = event.getClearContent().toModel()?.type - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is requested from another device - Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ") - event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - } - } - } - } else if (EventType.KEY_VERIFICATION_START == event.type) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is started from another device - Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") - it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - } - } - } else if (EventType.KEY_VERIFICATION_READY == event.type) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is started from another device - Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") - it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - } - } - } else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) { - event.getClearContent().toModel()?.relatesTo?.eventId?.let { - transactionsHandledByOtherDevice.remove(it) - } - } - - return@forEach - } - - val relatesTo = event.getClearContent().toModel()?.relatesTo?.eventId - if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) { - // Ignore this event, it is directed to another of my devices - Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ") - return@forEach - } - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_DONE -> { - sasVerificationService.onRoomEvent(event) - } - EventType.MESSAGE -> { - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { - sasVerificationService.onRoomRequestReceived(event) - } - } - } - } } } 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 2c3061ee55..5048c81a7f 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 @@ -273,11 +273,23 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(OutgoingVerificationRequestFragment::class) - fun bindVerificationRequestFragment(fragment: OutgoingVerificationRequestFragment): Fragment + @FragmentKey(VerificationRequestFragment::class) + fun bindVerificationRequestFragment(fragment: VerificationRequestFragment): Fragment @Binds @IntoMap @FragmentKey(VerificationChooseMethodFragment::class) fun bindVerificationMethodChooserFragment(fragment: VerificationChooseMethodFragment): Fragment + + + @Binds + @IntoMap + @FragmentKey(SASVerificationCodeFragment::class) + fun bindVerificationSasCodeFragment(fragment: SASVerificationCodeFragment): Fragment + + + @Binds + @IntoMap + @FragmentKey(VerificationConclusionFragment::class) + fun bindVerificationSasConclusionFragment(fragment: VerificationConclusionFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt index 91b5ae2ffd..1b56fc2a57 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt @@ -16,9 +16,12 @@ package im.vector.riotx.core.utils import android.text.Spannable +import android.text.style.BulletSpan +import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import androidx.annotation.ColorInt +import me.gujun.android.span.Span fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable { if (match.isEmpty()) return this @@ -35,3 +38,21 @@ fun Spannable.colorizeMatchingText(match: String, @ColorInt color: Int): Spannab } return this } + +fun Spannable.tappableMatchingText(match: String, clickSpan: ClickableSpan): Spannable { + if (match.isEmpty()) return this + indexOf(match).takeIf { it != -1 }?.let { start -> + this.setSpan(clickSpan, start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return this +} + +fun Span.bullet(text: CharSequence = "", + init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + this.spans.add(BulletSpan()) + init() + build() + }) +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt deleted file mode 100644 index 0f16b0786b..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestViewModel.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2019 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 - -import com.airbnb.mvrx.* -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.util.MatrixItem -import im.vector.matrix.android.api.util.toMatrixItem -import im.vector.riotx.core.platform.VectorViewModel -import im.vector.riotx.core.platform.VectorViewModelAction - - -data class VerificationRequestViewState( - val otherUserId: String = "", - val matrixItem: MatrixItem? = null, - val started: Async = Success(false) -) : MvRxState - -sealed class VerificationAction : VectorViewModelAction { - data class RequestVerificationByDM(val userID: String) : VerificationAction() -} - -class OutgoingVerificationRequestViewModel @AssistedInject constructor( - @Assisted initialState: VerificationRequestViewState, - private val session: Session -) : VectorViewModel(initialState) { - - @AssistedInject.Factory - interface Factory { - fun create(initialState: VerificationRequestViewState): OutgoingVerificationRequestViewModel - } - - init { - withState { - val user = session.getUser(it.otherUserId) - setState { - copy(matrixItem = user?.toMatrixItem()) - } - } - } - - companion object : MvRxViewModelFactory { - override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): OutgoingVerificationRequestViewModel? { - val fragment: OutgoingVerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.outgoingVerificationRequestViewModelFactory.create(state) - } - - override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? { - val userID: String = viewModelContext.args() - return VerificationRequestViewState(otherUserId = userID) - } - } - - - override fun handle(action: VerificationAction) { - } - -} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt new file mode 100644 index 0000000000..ebd7f351a4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2019 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 + +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import butterknife.BindView +import butterknife.OnClick +import com.airbnb.mvrx.* +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.platform.parentFragmentViewModel +import kotlinx.android.synthetic.main.fragment_bottom_sas_verification_code.* +import javax.inject.Inject + +class SASVerificationCodeFragment @Inject constructor( + val viewModelFactory: SASVerificationCodeViewModel.Factory +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bottom_sas_verification_code + + @BindView(R.id.sas_emoji_grid) + lateinit var emojiGrid: ViewGroup + + + @BindView(R.id.sas_decimal_code) + lateinit var decimalTextView: TextView + + @BindView(R.id.emoji0) + lateinit var emoji0View: ViewGroup + @BindView(R.id.emoji1) + lateinit var emoji1View: ViewGroup + @BindView(R.id.emoji2) + lateinit var emoji2View: ViewGroup + @BindView(R.id.emoji3) + lateinit var emoji3View: ViewGroup + @BindView(R.id.emoji4) + lateinit var emoji4View: ViewGroup + @BindView(R.id.emoji5) + lateinit var emoji5View: ViewGroup + @BindView(R.id.emoji6) + lateinit var emoji6View: ViewGroup + + + private val viewModel by fragmentViewModel(SASVerificationCodeViewModel::class) + + private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun invalidate() = withState(viewModel) { state -> + + if (state.supportsEmoji) { + decimalTextView.isVisible = false + when(val emojiDescription = state.emojiDescription) { + is Success -> { + sasLoadingProgress.isVisible = false + emojiGrid.isVisible = true + ButtonsVisibilityGroup.isVisible = true + emojiDescription.invoke().forEachIndexed { index, emojiRepresentation -> + when (index) { + 0 -> { + emoji0View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji0View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 1 -> { + emoji1View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji1View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 2 -> { + emoji2View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji2View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 3 -> { + emoji3View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji3View.findViewById(R.id.item_emoji_name_tv)?.setText(emojiRepresentation.nameResId) + } + 4 -> { + emoji4View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji4View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 5 -> { + emoji5View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji5View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 6 -> { + emoji6View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji6View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + } + } + + if (state.isWaitingFromOther) { + //hide buttons + ButtonsVisibilityGroup.isInvisible = true + sasCodeWaitingPartnerText.isVisible = true + } else { + ButtonsVisibilityGroup.isVisible = true + sasCodeWaitingPartnerText.isVisible = false + } + + } + is Fail -> { + sasLoadingProgress.isVisible = false + emojiGrid.isInvisible = true + ButtonsVisibilityGroup.isInvisible = true + //TODO? + } + else -> { + sasLoadingProgress.isVisible = true + emojiGrid.isInvisible = true + ButtonsVisibilityGroup.isInvisible = true + } + } + } else { + //Decimal + emojiGrid.isInvisible = true + decimalTextView.isVisible = true + val decimalCode = state.decimalDescription.invoke() + decimalTextView.text = decimalCode + + //TODO + if (state.isWaitingFromOther) { + //hide buttons + ButtonsVisibilityGroup.isInvisible = true + sasCodeWaitingPartnerText.isVisible = true + } else { + ButtonsVisibilityGroup.isVisible = decimalCode != null + sasCodeWaitingPartnerText.isVisible = false + } + } + } + + + @OnClick(R.id.sas_request_continue_button) + fun onMatchButtonTapped() = withState(viewModel) { state -> + //UX echo + ButtonsVisibilityGroup.isInvisible = true + sasCodeWaitingPartnerText.isVisible = true + sharedViewModel.handle(VerificationAction.SASMatchAction(state.otherUserId, state.transactionId)) + } + + @OnClick(R.id.sas_request_cancel_button) + fun onDoNotMatchButtonTapped() = withState(viewModel) { state -> + //UX echo + ButtonsVisibilityGroup.isInvisible = true + sasCodeWaitingPartnerText.isVisible = true + sharedViewModel.handle(VerificationAction.SASDoNotMatchAction(state.otherUserId, state.transactionId)) + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt new file mode 100644 index 0000000000..5fa9cc6cff --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2019 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 + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel + +data class SASVerificationCodeViewState( + val transactionId: String, + val otherUserId: String, + val otherUser: MatrixItem? = null, + val supportsEmoji: Boolean = true, + val emojiDescription: Async> = Uninitialized, + val decimalDescription: Async = Uninitialized, + val isWaitingFromOther: Boolean = false +) : MvRxState + +class SASVerificationCodeViewModel @AssistedInject constructor( + @Assisted initialState: SASVerificationCodeViewState, + private val session: Session +) : VectorViewModel(initialState) + , SasVerificationService.SasVerificationListener { + + init { + withState { state -> + val matrixItem = session.getUser(state.otherUserId)?.toMatrixItem() + setState { + copy(otherUser = matrixItem) + } + val sasTx = session.getSasVerificationService() + .getExistingTransaction(state.otherUserId, state.transactionId) + if (sasTx == null) { + setState { + copy( + isWaitingFromOther = false, + emojiDescription = Fail(Throwable("Unknown Transaction")), + decimalDescription = Fail(Throwable("Unknown Transaction")) + ) + } + } else { + refreshStateFromTx(sasTx) + } + } + + session.getSasVerificationService().addListener(this) + } + + override fun onCleared() { + session.getSasVerificationService().removeListener(this) + super.onCleared() + } + + private fun refreshStateFromTx(sasTx: SasVerificationTransaction) { + when (sasTx.state) { + SasVerificationTxState.None, + SasVerificationTxState.SendingStart, + SasVerificationTxState.Started, + SasVerificationTxState.OnStarted, + SasVerificationTxState.SendingAccept, + SasVerificationTxState.Accepted, + SasVerificationTxState.OnAccepted, + SasVerificationTxState.SendingKey, + SasVerificationTxState.KeySent, + SasVerificationTxState.OnKeyReceived -> { + setState { + copy( + isWaitingFromOther = false, + supportsEmoji = sasTx.supportsEmoji(), + emojiDescription = Loading>() + .takeIf { sasTx.supportsEmoji() } + ?: Uninitialized, + decimalDescription = Loading() + .takeIf { sasTx.supportsEmoji().not() } + ?: Uninitialized + ) + } + } + SasVerificationTxState.ShortCodeReady -> { + setState { + copy( + isWaitingFromOther = false, + supportsEmoji = sasTx.supportsEmoji(), + emojiDescription = if (sasTx.supportsEmoji()) Success(sasTx.getEmojiCodeRepresentation()) + else Uninitialized, + decimalDescription = if (!sasTx.supportsEmoji()) Success(sasTx.getDecimalCodeRepresentation()) + else Uninitialized + ) + } + } + SasVerificationTxState.ShortCodeAccepted, + SasVerificationTxState.SendingMac, + SasVerificationTxState.MacSent, + SasVerificationTxState.Verifying, + SasVerificationTxState.Verified -> { + setState { + copy(isWaitingFromOther = true) + } + } + SasVerificationTxState.Cancelled, + SasVerificationTxState.OnCancelled -> { + // The fragment should not be rendered in this state, + // it should have been replaced by a conclusion fragment + setState { + copy( + isWaitingFromOther = false, + supportsEmoji = sasTx.supportsEmoji(), + emojiDescription = Fail(Throwable("Transaction Cancelled")), + decimalDescription = Fail(Throwable("Transaction Cancelled")) + ) + } + } + } + } + + override fun transactionCreated(tx: SasVerificationTransaction) { + transactionUpdated(tx) + } + + override fun transactionUpdated(tx: SasVerificationTransaction) { + refreshStateFromTx(tx) + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SASVerificationCodeViewState): SASVerificationCodeViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: SASVerificationCodeViewState): SASVerificationCodeViewModel? { + val factory = (viewModelContext as FragmentViewModelContext).fragment().viewModelFactory + return factory.create(state) + } + + override fun initialState(viewModelContext: ViewModelContext): SASVerificationCodeViewState? { + val args = viewModelContext.args() + return SASVerificationCodeViewState( + transactionId = args.verificationId ?: "", + otherUserId = args.otherUserId + ) + } + } + + override fun handle(action: EmptyAction) { + + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt index d9c3b1d155..d33167518f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt @@ -91,7 +91,7 @@ class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() { (requireActivity() as VectorBaseActivity).notImplemented() /* - viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserId ?: "", viewModel.otherDeviceId + viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserMxItem ?: "", viewModel.otherDeviceId ?: "", object : SimpleApiCallback() { override fun onSuccess(info: MXDeviceInfo?) { info?.let { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt index f14a85c516..637df8818e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest import im.vector.riotx.core.utils.LiveEvent import javax.inject.Inject 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 abac6c6b72..66d0a918cd 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 @@ -1,6 +1,22 @@ +/* + * Copyright 2019 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 import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -9,28 +25,43 @@ import android.widget.TextView import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.text.toSpannable import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager import butterknife.BindView import butterknife.ButterKnife import com.airbnb.mvrx.MvRx -import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState 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.commitTransactionNow import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.utils.colorizeMatchingText import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.themes.ThemeUtils +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.bottom_sheet_verification.* import javax.inject.Inject +import kotlin.reflect.KClass class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { - @Inject lateinit var outgoingVerificationRequestViewModelFactory: OutgoingVerificationRequestViewModel.Factory + @Parcelize + data class VerificationArgs( + val otherUserId: String, + val verificationId: String? = null, + val roomId: String? = null + ) : Parcelable + + + @Inject + lateinit var verificationRequestViewModelFactory: VerificationRequestViewModel.Factory @Inject lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory @Inject lateinit var avatarRenderer: AvatarRenderer - private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class) override fun injectWith(injector: ScreenComponent) { @@ -49,24 +80,25 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { return view } - override fun invalidate() = withState(viewModel) { - when (it.verificationRequestEvent) { - is Uninitialized -> { - if (childFragmentManager.findFragmentByTag("REQUEST") == null) { - //Verification not yet started, put outgoing verification - childFragmentManager.commitTransaction { - setCustomAnimations(R.anim.fade_in, R.anim.fade_out) - replace(R.id.bottomSheetFragmentContainer, - OutgoingVerificationRequestFragment::class.java, - Bundle().apply { putString(MvRx.KEY_ARG, it.userId) }, - "REQUEST" - ) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.requestLiveData.observe(this, Observer { + it.peekContent().let { va -> + when (va) { + is Success -> { + if (va.invoke() is VerificationAction.GotItConclusion) { + dismiss() + } } } } - } + }) + } - it.otherUserId?.let { matrixItem -> + override fun invalidate() = withState(viewModel) { + + it.otherUserMxItem?.let { matrixItem -> val displayName = matrixItem.displayName ?: "" otherUserNameText.text = getString(R.string.verification_request_alert_title, displayName) .toSpannable() @@ -75,13 +107,121 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { avatarRenderer.render(matrixItem, otherUserAvatarImageView) } + //Did the request result in a SAS transaction? + if (it.sasTransactionState != null) { + + when (it.sasTransactionState) { + SasVerificationTxState.None, + SasVerificationTxState.SendingStart, + SasVerificationTxState.Started, + SasVerificationTxState.OnStarted, + SasVerificationTxState.SendingAccept, + SasVerificationTxState.Accepted, + SasVerificationTxState.OnAccepted, + SasVerificationTxState.SendingKey, + SasVerificationTxState.KeySent, + SasVerificationTxState.OnKeyReceived, + SasVerificationTxState.ShortCodeReady, + SasVerificationTxState.ShortCodeAccepted, + SasVerificationTxState.SendingMac, + SasVerificationTxState.MacSent, + SasVerificationTxState.Verifying -> { + val fragmentTag = SASVerificationCodeFragment::class.simpleName + if (childFragmentManager.findFragmentByTag(fragmentTag) == null) { + + // Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate) + bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout -> + TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 }) + } + childFragmentManager.commitTransactionNow { + replace(R.id.bottomSheetFragmentContainer, + SASVerificationCodeFragment::class.java, + Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs( + it.otherUserMxItem?.id ?: "", + it.pendingRequest?.transactionId)) + }, + SASVerificationCodeFragment::class.simpleName + ) + } + } + } + SasVerificationTxState.Verified, + SasVerificationTxState.Cancelled, + SasVerificationTxState.OnCancelled -> { + val fragmentTag = VerificationConclusionFragment::class.simpleName + if (childFragmentManager.findFragmentByTag(fragmentTag) == null) { + bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout -> + TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 }) + } + childFragmentManager.commitTransactionNow { + replace(R.id.bottomSheetFragmentContainer, + VerificationConclusionFragment::class.java, + Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args( + it.sasTransactionState == SasVerificationTxState.Verified, + it.cancelCode?.value)) + }, + fragmentTag + ) + } + } + } + } + + return@withState + } + + + // Transaction has not yet started + if (it.pendingRequest == null || !it.pendingRequest.isReady) { + if (childFragmentManager.findFragmentByTag("REQUEST") == null) { + bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout -> + TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 }) + } + //Verification not yet started, put outgoing verification + childFragmentManager.commitTransactionNow { + replace(R.id.bottomSheetFragmentContainer, + VerificationRequestFragment::class.java, + Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id ?: "")) + }, + "REQUEST" + ) + } + } + } else if (it.pendingRequest.isReady) { + showFragment(VerificationChooseMethodFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id ?: "", it.pendingRequest.transactionId)) + }) + + } + super.invalidate() } + + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { + if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { + // choose method + bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout -> + TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 }) + } + // Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate) + childFragmentManager.commitTransactionNow { + + replace(R.id.bottomSheetFragmentContainer, + fragmentClass.java, + bundle, + fragmentClass.simpleName + ) + } + } + } } -fun Fragment.getParentCoordinatorLayout(): CoordinatorLayout? { - var current = view?.parent as? View +fun View.getParentCoordinatorLayout(): CoordinatorLayout? { + var current = this as? View while (current != null) { if (current is CoordinatorLayout) return current current = current.parent as? View 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 578cedc07e..a9265293fa 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 @@ -1,50 +1,178 @@ +/* + * Copyright 2019 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 +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.* import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest +import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.core.utils.LiveEvent data class VerificationBottomSheetViewState( - val userId: String = "", - val otherUserId: MatrixItem? = null, - val verificationRequestEvent: Async = Uninitialized + val otherUserMxItem: MatrixItem? = null, + val roomId: String? = null, + val pendingRequest: PendingVerificationRequest? = null, + val sasTransactionState: SasVerificationTxState? = null, + val cancelCode: CancelCode? = null ) : MvRxState + +sealed class VerificationAction : VectorViewModelAction { + data class RequestVerificationByDM(val userID: String, val roomId: String?) : VerificationAction() + data class StartSASVerification(val userID: String, val pendingRequestTransactionId: String) : VerificationAction() + data class SASMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction() + data class SASDoNotMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction() + object GotItConclusion : VerificationAction() +} + class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState, private val session: Session) - : VectorViewModel(initialState) { + : VectorViewModel(initialState), + SasVerificationService.SasVerificationListener { + + + // Can be used for several actions, for a one shot result + private val _requestLiveData = MutableLiveData>>() + val requestLiveData: LiveData>> + get() = _requestLiveData init { - withState { - session.getUser(it.userId).let { user -> - setState { - copy(otherUserId = user?.toMatrixItem()) - } - } - } + session.getSasVerificationService().addListener(this) } + + override fun onCleared() { + session.getSasVerificationService().removeListener(this) + super.onCleared() + } + @AssistedInject.Factory interface Factory { fun create(initialState: VerificationBottomSheetViewState): VerificationBottomSheetViewModel } - companion object : MvRxViewModelFactory { override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? { val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - val userId: String = viewModelContext.args() - return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(userId)) + val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args() + + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + + val userItem = session.getUser(args.otherUserId) + + val sasTx = state.pendingRequest?.transactionId?.let { + session.getSasVerificationService().getExistingTransaction(args.otherUserId, it) + } + + val pr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId) + ?.firstOrNull { it.transactionId == args.verificationId } + + return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState( + otherUserMxItem = userItem?.toMatrixItem(), + sasTransactionState = sasTx?.state, + pendingRequest = pr, + roomId = args.roomId) + ) } } - override fun handle(action: VerificationAction) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + override fun handle(action: VerificationAction) = withState { state -> + val otherUserId = state.otherUserMxItem?.id ?: return@withState + val roomId = state.roomId + ?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId + ?: return@withState + when (action) { + is VerificationAction.RequestVerificationByDM -> { +// session + setState { + copy(pendingRequest = session.getSasVerificationService().requestKeyVerificationInDMs(otherUserId, roomId, null)) + } + } + is VerificationAction.StartSASVerification -> { + val request = session.getSasVerificationService().getExistingVerificationRequest(otherUserId) + ?.firstOrNull { it.transactionId == action.pendingRequestTransactionId } + ?: return@withState + + val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice + session.getSasVerificationService().beginKeyVerificationInDMs( + KeyVerificationStart.VERIF_METHOD_SAS, + transactionId = action.pendingRequestTransactionId, + roomId = roomId, + otherUserId = request.otherUserId, + otherDeviceId = otherDevice ?: "", + callback = null + ) + } + is VerificationAction.SASMatchAction -> { + session.getSasVerificationService() + .getExistingTransaction(action.userID, action.sasTransactionId) + ?.userHasVerifiedShortCode() + } + is VerificationAction.SASDoNotMatchAction -> { + session.getSasVerificationService() + .getExistingTransaction(action.userID, action.sasTransactionId) + ?.shortCodeDoNotMatch() + } + is VerificationAction.GotItConclusion -> { + _requestLiveData.postValue(LiveEvent(Success(action))) + } + } + } + + + override fun transactionCreated(tx: SasVerificationTransaction) { + transactionUpdated(tx) + } + + override fun transactionUpdated(tx: SasVerificationTransaction) = withState { state -> + if (tx.transactionId == state.pendingRequest?.transactionId) { + // A SAS tx has been started following this request + setState { + copy( + sasTransactionState = tx.state, + cancelCode = tx.cancelledReason + ) + } + } + } + + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + verificationRequestUpdated(pr) + } + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state -> + + if (pr.localID == state.pendingRequest?.localID || state.pendingRequest?.transactionId == pr.transactionId) { + setState { + copy(pendingRequest = pr) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt index ca441b8a24..69c599d335 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt @@ -1,37 +1,69 @@ +/* + * Copyright 2019 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 -import android.os.Bundle -import androidx.transition.AutoTransition -import androidx.transition.ChangeBounds -import androidx.transition.TransitionManager +import android.text.style.ClickableSpan +import android.view.View +import androidx.core.text.toSpannable +import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.platform.parentFragmentViewModel +import im.vector.riotx.core.utils.tappableMatchingText +import kotlinx.android.synthetic.main.fragment_verification_choose_method.* import javax.inject.Inject -class VerificationChooseMethodFragment @Inject constructor() : VectorBaseFragment() { +class VerificationChooseMethodFragment @Inject constructor( + val verificationChooseMethodViewModelFactory: VerificationChooseMethodViewModel.Factory +) : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_verification_choose_method -// init { -// sharedElementEnterTransition = ChangeBounds() -// sharedElementReturnTransition = ChangeBounds() -// } + private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + private val viewModel by fragmentViewModel(VerificationChooseMethodViewModel::class) + + override fun invalidate() = withState(viewModel) { state -> + if (state.QRModeAvailable) { + val cSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + + } + } + val openLink = getString(R.string.verify_open_camera_link) + val descCharSequence = + getString(R.string.verify_by_scanning_description, openLink) + .toSpannable() + .tappableMatchingText(openLink, cSpan) + verifyQRDescription.text = descCharSequence + verifyQRGroup.isVisible = true + } else { + verifyQRGroup.isVisible = false + } + + verifyEmojiGroup.isVisible = state.SASMOdeAvailable + } @OnClick(R.id.verificationByEmojiButton) - fun test() { //withState(viewModel) { state -> - getParentCoordinatorLayout()?.let { - TransitionManager.beginDelayedTransition(it, AutoTransition().apply { duration = 150 }) - } - parentFragmentManager.commitTransaction { - // setCustomAnimations(R.anim.fade_in, R.anim.fade_out) - replace(R.id.bottomSheetFragmentContainer, - OutgoingVerificationRequestFragment::class.java, - Bundle().apply { putString(MvRx.KEY_ARG, "@valere35:matrix.org") }, - "REQUEST" - ) - } + fun doVerifyBySas() = withState(sharedViewModel) { + sharedViewModel.handle(VerificationAction.StartSASVerification(it.otherUserMxItem?.id ?: "", it.pendingRequest?.transactionId + ?: "")) } + } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt new file mode 100644 index 0000000000..38482e9b09 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2019 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 + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel + +data class VerificationChooseMethodViewState( + val otherUserId: String = "", + val transactionId: String = "", + val QRModeAvailable: Boolean = false, + val SASMOdeAvailable: Boolean = false +) : MvRxState + + +class VerificationChooseMethodViewModel @AssistedInject constructor( + @Assisted initialState: VerificationChooseMethodViewState, + private val session: Session +) : VectorViewModel(initialState) { + + + init { + withState { state -> + val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId)?.first { + it.transactionId == state.transactionId + } + val qrAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SCAN) ?: false + val emojiAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SAS) ?: false + setState { + copy(QRModeAvailable = qrAvailable, SASMOdeAvailable = emojiAvailable) + } + } + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: VerificationChooseMethodViewState): VerificationChooseMethodViewModel + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: VerificationChooseMethodViewState): VerificationChooseMethodViewModel? { + val fragment: VerificationChooseMethodFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.verificationChooseMethodViewModelFactory.create(state) + } + + override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? { + val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args() + return VerificationChooseMethodViewState(otherUserId = args.otherUserId, transactionId = args.verificationId ?: "") + } + } + + + override fun handle(action: EmptyAction) {} + + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt new file mode 100644 index 0000000000..da3b0dd187 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 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 + +import android.os.Parcelable +import androidx.core.content.ContextCompat +import butterknife.OnClick +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.platform.parentFragmentViewModel +import io.noties.markwon.Markwon +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_verification_conclusion.* +import javax.inject.Inject + +class VerificationConclusionFragment @Inject constructor() : VectorBaseFragment() { + + @Parcelize + data class Args( + val isSuccessFull: Boolean, + val cancelReason: String? + ) : Parcelable + + override fun getLayoutResId() = R.layout.fragment_verification_conclusion + + private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + private val viewModel by fragmentViewModel(VerificationConclusionViewModel::class) + + override fun invalidate() = withState(viewModel) { + when (it.conclusionState) { + ConclusionState.SUCCESS -> { + verificationConclusionTitle.text = getString(R.string.sas_verified) + verifyConclusionDescription.setTextOrHide(getString(R.string.sas_verified_successful_description)) + verifyConclusionBottomDescription.text = getString(R.string.verification_green_shield) + verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_trusted)) + + } + ConclusionState.WARNING -> { + verificationConclusionTitle.text = getString(R.string.verification_conclusion_not_secure) + verifyConclusionDescription.setTextOrHide(null) + verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_warning)) + + verifyConclusionBottomDescription.text = Markwon.builder(requireContext()).build().toMarkdown(getString(R.string.verification_conclusion_compromised)) + } + ConclusionState.CANCELLED -> { + // Just dismiss in this case + sharedViewModel.handle(VerificationAction.GotItConclusion) + } + } + } + + @OnClick(R.id.verificationConclusionButton) + fun onButtonTapped() { + sharedViewModel.handle(VerificationAction.GotItConclusion) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt new file mode 100644 index 0000000000..ca069bf853 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 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 + +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.safeValueOf +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel + +data class SASVerificationConclusionViewState( + val conclusionState: ConclusionState = ConclusionState.CANCELLED +) : MvRxState + +enum class ConclusionState { + SUCCESS, + WARNING, + CANCELLED +} + +class VerificationConclusionViewModel(initialState: SASVerificationConclusionViewState) + : VectorViewModel(initialState) { + + companion object : MvRxViewModelFactory { + + override fun initialState(viewModelContext: ViewModelContext): SASVerificationConclusionViewState? { + val args = viewModelContext.args() + + return when (safeValueOf(args.cancelReason)) { + CancelCode.MismatchedSas, + CancelCode.MismatchedCommitment, + CancelCode.MismatchedKeys -> { + SASVerificationConclusionViewState(ConclusionState.WARNING) + } + else -> { + SASVerificationConclusionViewState( + if (args.isSuccessFull) ConclusionState.SUCCESS + else ConclusionState.CANCELLED + ) + } + } + } + } + + override fun handle(action: EmptyAction) {} +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt similarity index 61% rename from vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt rename to vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt index f652c7bfdd..60b89c19d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/OutgoingVerificationRequestFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt @@ -16,16 +16,14 @@ package im.vector.riotx.features.crypto.verification import android.graphics.Typeface -import android.os.Bundle import androidx.core.text.toSpannable -import androidx.transition.AutoTransition -import androidx.transition.TransitionManager +import androidx.core.view.isInvisible +import androidx.core.view.isVisible import butterknife.OnClick -import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R -import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.parentFragmentViewModel import im.vector.riotx.core.utils.colorizeMatchingText @@ -35,43 +33,51 @@ import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.synthetic.main.fragment_verification_request.* import javax.inject.Inject -class OutgoingVerificationRequestFragment @Inject constructor( - val outgoingVerificationRequestViewModelFactory: OutgoingVerificationRequestViewModel.Factory, +class VerificationRequestFragment @Inject constructor( + val verificationRequestViewModelFactory: VerificationRequestViewModel.Factory, val avatarRenderer: AvatarRenderer ) : VectorBaseFragment() { - private val viewModel by fragmentViewModel(OutgoingVerificationRequestViewModel::class) + private val viewModel by fragmentViewModel(VerificationRequestViewModel::class) private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) override fun getLayoutResId() = R.layout.fragment_verification_request override fun invalidate() = withState(viewModel) { state -> - state.matrixItem?.let { + state.matrixItem.let { val styledText = getString(R.string.verification_request_alert_description, it.id) .toSpannable() .styleMatchingText(it.id, Typeface.BOLD) .colorizeMatchingText(it.id, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color)) verificationRequestText.text = styledText } + + when (state.started) { + is Loading -> { + //Hide the start button, show waiting + verificationStartButton.isInvisible = true + verificationWaitingText.isVisible = true + val otherUser = state.matrixItem.displayName ?: state.matrixItem.id + verificationWaitingText.text = getString(R.string.verification_request_waiting_for, otherUser) + .toSpannable() + .styleMatchingText(otherUser, Typeface.BOLD) + .colorizeMatchingText(otherUser, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color)) + } + else -> { + verificationStartButton.isEnabled = true + verificationStartButton.isVisible = true + verificationWaitingText.isInvisible = true + } + } + Unit } @OnClick(R.id.verificationStartButton) fun onClickOnVerificationStart() = withState(viewModel) { state -> - - sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.otherUserId)) - - getParentCoordinatorLayout()?.let { - TransitionManager.beginDelayedTransition(it, AutoTransition().apply { duration = 150 }) - } - parentFragmentManager.commitTransaction { - replace(R.id.bottomSheetFragmentContainer, - VerificationChooseMethodFragment::class.java, - Bundle().apply { putString(MvRx.KEY_ARG, state.otherUserId) }, - "REQUEST" - ) - } + verificationStartButton.isEnabled = false + sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.matrixItem.id, state.roomId)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt new file mode 100644 index 0000000000..752d6a6a8a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2019 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 + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest +import im.vector.riotx.core.di.HasScreenInjector +import im.vector.riotx.core.platform.VectorViewModel + + +data class VerificationRequestViewState( + val roomId: String? = null, + val matrixItem: MatrixItem, + val started: Async = Success(false) +) : MvRxState + + +class VerificationRequestViewModel @AssistedInject constructor( + @Assisted initialState: VerificationRequestViewState, + private val session: Session +) : VectorViewModel(initialState), SasVerificationService.SasVerificationListener { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: VerificationRequestViewState): VerificationRequestViewModel + } + + init { + withState { + val pr = session.getSasVerificationService() + .getExistingVerificationRequest(it.matrixItem.id) + ?.firstOrNull() + setState { + copy( + started = Success(false).takeIf { pr == null } + ?: Success(true).takeIf { pr?.isReady == true } + ?: Loading() + ) + } + } + session.getSasVerificationService().addListener(this) + } + + override fun onCleared() { + session.getSasVerificationService().removeListener(this) + super.onCleared() + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): VerificationRequestViewModel? { + val fragment: VerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.verificationRequestViewModelFactory.create(state) + } + + override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? { + val otherUserId = viewModelContext.args().otherUserId + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + + return session.getUser(otherUserId)?.let { + VerificationRequestViewState(matrixItem = it.toMatrixItem()) + } + } + } + + override fun handle(action: VerificationAction) { + } + + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) {} + + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + verificationRequestUpdated(pr) + } + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state -> + if (pr.otherUserId == state.matrixItem.id) { + if (pr.isReady) { + setState { + copy(started = Success(true)) + } + } else { + setState { + copy(started = Loading()) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 1db153e594..f0c4ce5971 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -925,7 +925,7 @@ class RoomDetailFragment @Inject constructor( } is Success -> { when (val data = result.invoke()) { - is RoomDetailAction.ReportContent -> { + is RoomDetailAction.ReportContent -> { when { data.spam -> { AlertDialog.Builder(requireActivity()) @@ -962,9 +962,20 @@ class RoomDetailFragment @Inject constructor( } } } - is RoomDetailAction.RequestVerification -> { + is RoomDetailAction.RequestVerification -> { VerificationBottomSheet().apply { - arguments = Bundle().apply { putString(MvRx.KEY_ARG, data.userId) } + arguments = Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(data.userId, roomId = roomDetailArgs.roomId)) + } +// setArguments() + }.show(parentFragmentManager, "REQ") + } + is RoomDetailAction.AcceptVerificationRequest -> { + VerificationBottomSheet().apply { + arguments = Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs( + data.otherUserId, data.transactionId, roomId = roomDetailArgs.roomId)) + } }.show(parentFragmentManager, "REQ") } } @@ -1121,7 +1132,8 @@ class RoomDetailFragment @Inject constructor( } override fun onAvatarClicked(informationData: MessageInformationData) { - vectorBaseActivity.notImplemented("Click on user avatar") + //vectorBaseActivity.notImplemented("Click on user avatar") + roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.senderId)) } override fun onMemberNameClicked(informationData: MessageInformationData) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index a8623c4cb2..36c0442e2a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -49,7 +49,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.R @@ -186,6 +185,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.RequestVerification -> handleRequestVerification(action) } } @@ -798,20 +798,27 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { - session.getSasVerificationService().beginKeyVerificationInDMs( - KeyVerificationStart.VERIF_METHOD_SAS, - action.transactionId, - room.roomId, - action.otherUserId, - action.otherdDeviceId, - null - ) + session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId,room.roomId, + action.transactionId) + _requestLiveData.postValue(LiveEvent(Success(action))) +// session.getSasVerificationService().beginKeyVerificationInDMs( +// KeyVerificationStart.VERIF_METHOD_SAS, +// action.transactionId, +// room.roomId, +// action.otherUserMxItem, +// action.otherdDeviceId, +// null +// ) } private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) { Timber.e("TODO implement $action") } + private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) { + _requestLiveData.postValue(LiveEvent(Success(action))) + } + private fun observeSyncState() { session.rx() .liveSyncState() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index dadf267dd7..36856eebe4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -151,18 +151,18 @@ class MessageItemFactory @Inject constructor( return VerificationRequestItem_() .attributes( VerificationRequestItem.Attributes( - otherUserId, - otherUserName.toString(), - messageContent.fromDevice, - informationData.eventId, - informationData, - attributes.avatarRenderer, - attributes.colorProvider, - attributes.itemLongClickListener, - attributes.itemClickListener, - attributes.reactionPillCallback, - attributes.readReceiptsCallback, - attributes.emojiTypeFace + otherUserId = otherUserId, + otherUserName = otherUserName.toString(), + fromDevide = messageContent.fromDevice, + referenceId = informationData.eventId, + informationData = informationData, + avatarRenderer = attributes.avatarRenderer, + colorProvider = attributes.colorProvider, + itemLongClickListener = attributes.itemLongClickListener, + itemClickListener = attributes.itemClickListener, + reactionPillCallback = attributes.reactionPillCallback, + readReceiptsCallback = attributes.readReceiptsCallback, + emojiTypeFace = attributes.emojiTypeFace ) ) .callback(callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 13679cecaf..3ff4af27c2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -70,6 +70,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_MAC -> { // These events are filtered from timeline in normal case // Only visible in developer mode diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 39afebf5af..c8058d0fa8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -52,6 +52,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_READY, EventType.REDACTION -> formatDebug(timelineEvent.root) else -> { Timber.v("Type $type not handled by this formatter") diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 71272fe815..2864fe6802 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -50,7 +50,8 @@ object TimelineDisplayableEvents { EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_KEY + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_READY ) } diff --git a/vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml b/vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml new file mode 100644 index 0000000000..bf51aab3df --- /dev/null +++ b/vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_verification_choose_method.xml b/vector/src/main/res/layout/fragment_verification_choose_method.xml index 70411c69b6..37b3c6e53a 100644 --- a/vector/src/main/res/layout/fragment_verification_choose_method.xml +++ b/vector/src/main/res/layout/fragment_verification_choose_method.xml @@ -97,14 +97,15 @@ style="@style/VectorButtonStylePositive" android:layout_width="match_parent" android:layout_marginTop="16dp" - android:text="@string/accept" + android:text="@string/verify_by_emoji_title" app:layout_constraintTop_toBottomOf="@id/verifyEmojiDescription" /> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 8c1b2b819d..a320caeef7 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1500,7 +1500,7 @@ Why choose Riot.im? Verified! You\'ve successfully verified this device. - Secure messages with this user are end-to-end encrypted and not able to be read by third parties. + Messages with this user in this room are end-to-end encrypted and can‘t be read by third parties. Got it Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification. diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 944a5336b3..e9db6a4f39 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -2,16 +2,28 @@ + Request to verify the given userID + Prepends ¯\\_(ツ)_/¯ to a plain-text message + Initial Sync… - File - Audio - Image. - Video. - - Untrusted sign in + They match + They don‘t match + Verify this user by confirming the following unique emoji appear on their screen, in the same order." + For ultimate security, use another trusted means of communication or do this in person. + Look for the green shield to ensure a user is trusted. Trust all users in a room to ensure the room is secure. + + Not secure + One of the following may be compromised:\n\n - Your homeserver\n - The homeserver the user you’re verifying is connected to\n - Yours, or the other users’ internet connection\n - Yours, or the other users’ device + + + Video. + Image. + Audio + File + Waiting… %s cancelled You cancelled