From ea6e8a6789ec5c2a4b9277a0860a01b2517022ef Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 27 Jan 2020 23:51:08 +0100 Subject: [PATCH] Basic debug screen to setup keys --- .../riotx/features/debug/DebugMenuActivity.kt | 64 ------- .../debug/res/layout/activity_debug_menu.xml | 7 - .../im/vector/riotx/core/di/FragmentModule.kt | 6 + .../CrossSigningEpoxyController.kt | 173 ++++++++++++++++++ .../CrossSigningSettingsFragment.kt | 125 +++++++++++++ .../CrossSigningSettingsViewModel.kt | 140 ++++++++++++++ vector/src/main/res/values/strings_riotX.xml | 3 + .../xml/vector_settings_security_privacy.xml | 1 + 8 files changed, 448 insertions(+), 71 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt index 96a6f4bb2d..31998e9b99 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt @@ -22,18 +22,9 @@ import android.app.NotificationManager import android.content.Context import android.content.Intent import android.os.Build -import android.text.InputType -import android.widget.EditText -import androidx.appcompat.app.AlertDialog import androidx.core.app.NotificationCompat import androidx.core.app.Person import butterknife.OnClick -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.internal.auth.data.LoginFlowTypes -import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse -import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth -import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent @@ -178,61 +169,6 @@ class DebugMenuActivity : VectorBaseActivity() { throw RuntimeException("Application crashed from user demand") } - @OnClick(R.id.debug_initialise_xsigning) - fun testXSigning() { - activeSessionHolder.getActiveSession().getCrossSigningService().initializeCrossSigning(null, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - if (failure is Failure.OtherServerError - && failure.httpCode == 401 - ) { - try { - MoshiProvider.providesMoshi() - .adapter(RegistrationFlowResponse::class.java) - .fromJson(failure.errorBody) - } catch (e: Exception) { - null - }?.let { - // Retry with authentication - if (it.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) { - // Ask for password - val inflater = this@DebugMenuActivity.layoutInflater - val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) - - val input = layout.findViewById(R.id.edit_text).also { - it.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD - } - - val activeSession = activeSessionHolder.getActiveSession() - AlertDialog.Builder(this@DebugMenuActivity) - .setTitle("Confirm password") - .setView(layout) - .setPositiveButton(R.string.ok) { _, _ -> - val pass = input.text.toString() - - activeSession.getCrossSigningService().initializeCrossSigning( - UserPasswordAuth( - session = it.session, - user = activeSession.myUserId, - password = pass - ) - ) - } - .setNegativeButton(R.string.cancel, null) - .show() - } else { - // can't do this from here - AlertDialog.Builder(this@DebugMenuActivity) - .setTitle(R.string.dialog_title_error) - .setMessage("You cannot do that from mobile") - .setPositiveButton(R.string.ok, null) - .show() - } - } - } - } - }) - } - @OnClick(R.id.debug_scan_qr_code) fun scanQRCode() { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector/src/debug/res/layout/activity_debug_menu.xml index 52b993e223..272432526a 100644 --- a/vector/src/debug/res/layout/activity_debug_menu.xml +++ b/vector/src/debug/res/layout/activity_debug_menu.xml @@ -61,13 +61,6 @@ android:layout_height="wrap_content" android:text="Crash the app" /> - - () { + + interface InteractionListener { + fun onInitializeCrossSigningKeys() + fun onResetCrossSigningKeys() + } + + var interactionListener: InteractionListener? = null + + override fun buildModels(data: CrossSigningSettingsViewState?) { + if (data == null) return + if (data.xSigningKeyCanSign) { + genericItem { + id("can") + titleIconResourceId(R.drawable.ic_shield_trusted) + title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete)) + } + bottomSheetVerificationActionItem { + id("resetkeys") + title("Reset keys") + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + listener { + interactionListener?.onResetCrossSigningKeys() + } + } + } else if (data.xSigningKeysAreTrusted) { + genericItem { + id("trusted") + titleIconResourceId(R.drawable.ic_shield_warning) + title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted)) + } + bottomSheetVerificationActionItem { + id("resetkeys") + title("Reset keys") + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + listener { + interactionListener?.onResetCrossSigningKeys() + } + } + } else if (data.xSigningIsEnableInAccount) { + genericItem { + id("enable") + titleIconResourceId(R.drawable.ic_shield_black) + title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted)) + } + bottomSheetVerificationActionItem { + id("resetkeys") + title("Reset keys") + titleColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + listener { + interactionListener?.onResetCrossSigningKeys() + } + } + } else { + genericItem { + id("not") + title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled)) + } + + bottomSheetVerificationActionItem { + id("initKeys") + title("Initialize keys") + titleColor(colorProvider.getColor(R.color.riotx_positive_accent)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColor(R.color.riotx_positive_accent)) + listener { + interactionListener?.onInitializeCrossSigningKeys() + } + } + } + + when (data.crossSigningInfo) { + is Loading -> { + loadingItem { + id("loading") + } + } + is Success -> { + val crossSigningKeys = data.crossSigningInfo.invoke() + + crossSigningKeys?.masterKey()?.let { + genericItemWithValue { + id("msk") + titleIconResourceId(R.drawable.key_small) + title( + span { + +"Master Key:\n" + span { + text = it.unpaddedBase64PublicKey ?: "" + textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = dimensionConverter.spToPx(12) + } + } + ) + } + } + crossSigningKeys?.userKey()?.let { + genericItemWithValue { + id("usk") + titleIconResourceId(R.drawable.key_small) + title( + span { + +"User Key:\n" + span { + text = it.unpaddedBase64PublicKey ?: "" + textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = dimensionConverter.spToPx(12) + } + } + ) + } + } + crossSigningKeys?.selfSigningKey()?.let { + genericItemWithValue { + id("ssk") + titleIconResourceId(R.drawable.key_small) + title( + span { + +"Self Signed Key:\n" + span { + text = it.unpaddedBase64PublicKey ?: "" + textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) + textSize = dimensionConverter.spToPx(12) + } + } + ) + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt new file mode 100644 index 0000000000..6c756ec445 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.settings.crosssigning + +import android.os.Bundle +import android.text.InputType +import android.view.View +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import javax.inject.Inject + +class CrossSigningSettingsFragment @Inject constructor( + private val epoxyController: CrossSigningEpoxyController, + val viewModelFactory: CrossSigningSettingsViewModel.Factory +) : VectorBaseFragment(), CrossSigningEpoxyController.InteractionListener { + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + private val viewModel: CrossSigningSettingsViewModel by fragmentViewModel() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel.requestLiveData.observeEvent(this) { + when (it) { + is Fail -> { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_title_error) + .setMessage(it.error.message) + .setPositiveButton(R.string.ok, null) + .show() + } + is Success -> { + when (val action = it.invoke()) { + is CrossSigningAction.RequestPasswordAuth -> { + requestPassword(action.sessionId) + } + } + } + } + } + } + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.encryption_information_cross_signing_state) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + } + + override fun invalidate() = withState(viewModel) { state -> + epoxyController.setData(state) + } + + private fun setupRecyclerView() { + recyclerView.configureWith(epoxyController, hasFixedSize = false, disableItemAnimation = true) + epoxyController.interactionListener = this + } + + override fun onDestroyView() { + recyclerView.cleanup() + epoxyController.interactionListener = null + super.onDestroyView() + } + + fun requestPassword(sessionId: String) { + // Ask for password + val inflater = this.layoutInflater + val layout = inflater.inflate(R.layout.dialog_base_edit_text, null) + + val input = layout.findViewById(R.id.edit_text).also { + it.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD + } + + AlertDialog.Builder(requireContext()) + .setTitle("Confirm password") + .setView(layout) + .setPositiveButton(R.string.ok) { _, _ -> + val pass = input.text.toString() + + viewModel.handle(CrossSigningAction.InitializeCrossSigning(UserPasswordAuth( + session = sessionId, + password = pass + ))) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + override fun onInitializeCrossSigningKeys() { + viewModel.handle(CrossSigningAction.InitializeCrossSigning()) + } + + override fun onResetCrossSigningKeys() { + viewModel.handle(CrossSigningAction.InitializeCrossSigning()) + } +} + diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt new file mode 100644 index 0000000000..44da3ce5c9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.settings.crosssigning + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse +import im.vector.matrix.android.internal.crypto.crosssigning.isVerified +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.rx.rx +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.LiveEvent + +data class CrossSigningSettingsViewState( + val crossSigningInfo: Async = Uninitialized, + val xSigningIsEnableInAccount: Boolean = false, + val xSigningKeysAreTrusted: Boolean = false, + val xSigningKeyCanSign: Boolean = true +) : MvRxState + +sealed class CrossSigningAction : VectorViewModelAction { + data class InitializeCrossSigning(val auth: UserPasswordAuth? = null) : CrossSigningAction() + data class RequestPasswordAuth(val sessionId: String) : CrossSigningAction() +} + +class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState, + private val stringProvider: StringProvider, + private val session: Session) + : VectorViewModel(initialState) { + + // Can be used for several actions, for a one shot result + private val _requestLiveData = MutableLiveData>>() + val requestLiveData: LiveData>> + get() = _requestLiveData + + init { + session.rx().liveCrossSigningInfo(session.myUserId) + .map { + it.getOrNull() + } + .execute { + val crossSigningKeys = it.invoke() + val xSigningIsEnableInAccount = crossSigningKeys != null + val xSigningKeysAreTrusted = session.getCrossSigningService().checkUserTrust(session.myUserId).isVerified() + val xSigningKeyCanSign = session.getCrossSigningService().canCrossSign() + copy( + crossSigningInfo = it, + xSigningIsEnableInAccount = xSigningIsEnableInAccount, + xSigningKeysAreTrusted = xSigningKeysAreTrusted, + xSigningKeyCanSign = xSigningKeyCanSign + ) + } + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel + } + + override fun handle(action: CrossSigningAction) { + when (action) { + is CrossSigningAction.InitializeCrossSigning -> { + initializeCrossSigning(action.auth?.also { it.user = session.myUserId }) + } + } + } + + private fun initializeCrossSigning(auth: UserPasswordAuth?) { + setState { + copy(crossSigningInfo = Loading()) + } + session.getCrossSigningService().initializeCrossSigning(auth, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + if (failure is Failure.OtherServerError + && failure.httpCode == 401 + ) { + try { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(failure.errorBody) + } catch (e: Exception) { + null + }?.let { flowResponse -> + // Retry with authentication + if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) { + _requestLiveData.postValue(LiveEvent(Success(CrossSigningAction.RequestPasswordAuth(flowResponse.session ?: "")))) + return + } else { + _requestLiveData.postValue(LiveEvent(Fail(Throwable("You cannot do that from mobile")))) + // can't do this from here + return + } + } + } + _requestLiveData.postValue(LiveEvent(Fail(Throwable("You cannot do that from mobile")))) + } + }) + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: CrossSigningSettingsViewState): CrossSigningSettingsViewModel? { + val fragment: CrossSigningSettingsFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } +} diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 900ac3ea99..4d53341482 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -134,4 +134,7 @@ %1$s (%2$s) signed in using a new device: Until this user trusts this device, messages sent to and from it are labelled with warnings. Alternatively, you can manually verify it. + + Initialize CrossSigning + Reset Keys diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index 913765bc3a..19bc340500 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -15,6 +15,7 @@ android:persistent="false" android:title="@string/encryption_information_cross_signing_state" tools:summary="@string/encryption_information_dg_xsigning_complete" + app:fragment="im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment" />