Basic debug screen to setup keys

This commit is contained in:
Valere 2020-01-27 23:51:08 +01:00
parent 6cece03998
commit ea6e8a6789
8 changed files with 448 additions and 71 deletions

View file

@ -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<Unit> {
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<EditText>(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)) {

View file

@ -61,13 +61,6 @@
android:layout_height="wrap_content"
android:text="Crash the app" />
<com.google.android.material.button.MaterialButton
android:id="@+id/debug_initialise_xsigning"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Initialize XSigning" />
<com.google.android.material.button.MaterialButton
android:id="@+id/debug_scan_qr_code"
style="@style/VectorButtonStyle"

View file

@ -68,6 +68,7 @@ import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFra
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment
@ -331,4 +332,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(DeviceTrustInfoActionFragment::class)
fun bindDeviceTrustInfoActionFragment(fragment: DeviceTrustInfoActionFragment): Fragment
@Binds
@IntoMap
@FragmentKey(CrossSigningSettingsFragment::class)
fun bindCrossSigningSettingsFragment(fragment: CrossSigningSettingsFragment): Fragment
}

View file

@ -0,0 +1,173 @@
/*
* 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 com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemWithValue
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import me.gujun.android.span.span
import javax.inject.Inject
class CrossSigningEpoxyController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
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)
}
}
)
}
}
}
}
}
}

View file

@ -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<EditText>(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())
}
}

View file

@ -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<MXCrossSigningInfo?> = 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<CrossSigningSettingsViewState, CrossSigningAction>(initialState) {
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<CrossSigningAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<CrossSigningAction>>>
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<Unit> {
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<CrossSigningSettingsViewModel, CrossSigningSettingsViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: CrossSigningSettingsViewState): CrossSigningSettingsViewModel? {
val fragment: CrossSigningSettingsFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
}
}
}

View file

@ -134,4 +134,7 @@
<string name="verification_profile_device_new_signing">%1$s (%2$s) signed in using a new device:</string>
<string name="verification_profile_device_untrust_info">Until this user trusts this device, messages sent to and from it are labelled with warnings. Alternatively, you can manually verify it.</string>
<string name="initialize_cross_signing">Initialize CrossSigning</string>
<string name="reset_cross_signing">Reset Keys</string>
</resources>

View file

@ -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"
/>