UIA fixes + better error support

This commit is contained in:
Valere 2021-02-01 14:31:23 +01:00
parent 1244d00b31
commit da16ec0af3
30 changed files with 524 additions and 401 deletions

View file

@ -44,5 +44,5 @@ interface UserInteractiveAuthInterceptor {
* Updated auth should be provider using promise.resume, this allow implementation to perform
* an async operation (prompt for user password, open sso fallback) and then resume initial API call when done.
*/
fun performStage(flowResponse: RegistrationFlowResponse, promise : Continuation<UIABaseAuth>)
fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>)
}

View file

@ -53,6 +53,16 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
.adapter(RegistrationFlowResponse::class.java)
.fromJson(this.errorBody)
}
} else if (this is Failure.ServerError && this.error.code == MatrixError.M_FORBIDDEN) {
// This happens when the submission for this stage was bad (like bad password)
if (this.error.session != null && this.error.flows != null) {
RegistrationFlowResponse(
flows = this.error.flows,
session = this.error.session,
completedStages = this.error.completedStages,
params = this.error.params
)
} else null
} else {
null
}

View file

@ -18,6 +18,8 @@ package org.matrix.android.sdk.api.failure
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow
/**
* This data class holds the error defined by the matrix specifications.
@ -42,7 +44,17 @@ data class MatrixError(
@Json(name = "soft_logout") val isSoftLogout: Boolean = false,
// For M_INVALID_PEPPER
// {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"}
@Json(name = "lookup_pepper") val newLookupPepper: String? = null
@Json(name = "lookup_pepper") val newLookupPepper: String? = null,
// For M_FORBIDDEN UIA
@Json(name = "session")
val session: String? = null,
@Json(name = "completed")
val completedStages: List<String>? = null,
@Json(name = "flows")
val flows: List<InteractiveAuthenticationFlow>? = null,
@Json(name = "params")
val params: JsonDict? = null
) {
companion object {

View file

@ -0,0 +1,58 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.auth.registration
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
import timber.log.Timber
import kotlin.coroutines.suspendCoroutine
fun RegistrationFlowResponse.nextUncompletedStage(flowIndex: Int = 0): String? {
val completed = completedStages ?: emptyList()
return flows?.getOrNull(flowIndex)?.stages?.firstOrNull { completed.contains(it).not() }
}
suspend fun handleUIA(failure: Throwable, interceptor: UserInteractiveAuthInterceptor, retryBlock: suspend (UIABaseAuth) -> Unit): Boolean {
Timber.d("## UIA: check error ${failure.message}")
val flowResponse = failure.toRegistrationFlowResponse()
?: return false.also {
Timber.d("## UIA: not a UIA error")
}
Timber.d("## UIA: error can be passed to interceptor")
Timber.d("## UIA: type = ${flowResponse.flows}")
Timber.d("## UIA: delegate to interceptor...")
val authUpdate = try {
suspendCoroutine<UIABaseAuth> { continuation ->
interceptor.performStage(flowResponse, (failure as? Failure.ServerError)?.error?.code, continuation)
}
} catch (failure: Throwable) {
Timber.w(failure, "## UIA: failed to participate")
return false
}
Timber.d("## UIA: updated auth $authUpdate")
return try {
retryBlock(authUpdate)
true
} catch (failure: Throwable) {
handleUIA(failure, interceptor, retryBlock)
}
}

View file

@ -17,8 +17,7 @@
package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
@ -27,7 +26,6 @@ import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.suspendCoroutine
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
data class Params(
@ -48,46 +46,14 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap()))
}
} catch (throwable: Throwable) {
if (params.userInteractiveAuthInterceptor == null || !handleUIA(throwable, params)) {
if (params.userInteractiveAuthInterceptor == null
|| !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
execute(params.copy(userAuthParam = auth))
}
) {
Timber.d("## UIA: propagate failure")
throw throwable
}
}
}
private suspend fun handleUIA(failure: Throwable, params: DeleteDeviceTask.Params): Boolean {
Timber.d("## UIA: check error delete device ${failure.message}")
if (failure is Failure.OtherServerError && failure.httpCode == 401) {
Timber.d("## UIA: error can be passed to interceptor")
// give a chance to the reauth helper?
val flowResponse = failure.toRegistrationFlowResponse()
?: return false.also {
Timber.d("## UIA: failed to parse flow response")
}
Timber.d("## UIA: type = ${flowResponse.flows}")
Timber.d("## UIA: has interceptor = ${params.userInteractiveAuthInterceptor != null}")
Timber.d("## UIA: delegate to interceptor...")
val authUpdate = try {
suspendCoroutine<UIABaseAuth> { continuation ->
params.userInteractiveAuthInterceptor!!.performStage(flowResponse, continuation)
}
} catch (failure: Throwable) {
Timber.w(failure, "## UIA: failed to participate")
return false
}
Timber.d("## UIA: delete device updated auth $authUpdate")
return try {
execute(params.copy(userAuthParam = authUpdate))
true
} catch (failure: Throwable) {
handleUIA(failure, params)
}
} else {
Timber.d("## UIA: not a UIA error")
return false
}
}
}

View file

@ -18,15 +18,13 @@ package org.matrix.android.sdk.internal.crypto.tasks
import dagger.Lazy
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
import org.matrix.android.sdk.internal.crypto.model.KeyUsage
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.task.Task
@ -34,7 +32,6 @@ import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.olm.OlmPkSigning
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.suspendCoroutine
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
data class Params(
@ -128,7 +125,10 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor(
try {
uploadSigningKeysTask.execute(uploadSigningKeysParams)
} catch (failure: Throwable) {
if (params.interactiveAuthInterceptor == null || !handleUIA(failure, params, uploadSigningKeysParams)) {
if (params.interactiveAuthInterceptor == null ||
!handleUIA(failure, params.interactiveAuthInterceptor) { authUpdate ->
uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate))
}) {
Timber.d("## UIA: propagate failure")
throw failure
}
@ -181,42 +181,4 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor(
selfSigningPkOlm?.releaseSigning()
}
}
private suspend fun handleUIA(failure: Throwable,
params: InitializeCrossSigningTask.Params,
uploadSigningKeysParams: UploadSigningKeysTask.Params): Boolean {
Timber.d("## UIA: check error initialize xsigning ${failure.message}")
if (failure is Failure.OtherServerError && failure.httpCode == 401) {
Timber.d("## UIA: error can be passed to interceptor")
// give a chance to the reauth helper?
val flowResponse = failure.toRegistrationFlowResponse()
?: return false.also {
Timber.d("## UIA: failed to parse flow response")
}
Timber.d("## UIA: type = ${flowResponse.flows}")
Timber.d("## UIA: has interceptor = ${params.interactiveAuthInterceptor != null}")
Timber.d("## UIA: delegate to interceptor...")
val authUpdate = try {
suspendCoroutine<UIABaseAuth> { continuation ->
params.interactiveAuthInterceptor!!.performStage(flowResponse, continuation)
}
} catch (failure: Throwable) {
Timber.w(failure, "## UIA: failed to participate")
return false
}
Timber.d("## UIA: initialize xsigning updated auth $authUpdate")
try {
uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate))
return true
} catch (failure: Throwable) {
return handleUIA(failure, params, uploadSigningKeysParams)
}
} else {
Timber.d("## UIA: not a UIA error")
return false
}
}
}

View file

@ -28,7 +28,7 @@ import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragm
import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment
import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment
import im.vector.app.features.crypto.recover.BootstrapAccountPasswordFragment
import im.vector.app.features.crypto.recover.BootstrapReAuthFragment
import im.vector.app.features.crypto.recover.BootstrapConclusionFragment
import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment
import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment
@ -522,8 +522,8 @@ interface FragmentModule {
@Binds
@IntoMap
@FragmentKey(BootstrapAccountPasswordFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
@FragmentKey(BootstrapReAuthFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapReAuthFragment): Fragment
@Binds
@IntoMap

View file

@ -51,19 +51,23 @@ class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
}
private fun onButtonClicked() = withState(viewModel) { state ->
if (state.flowType == LoginFlowTypes.SSO) {
viewModel.handle(ReAuthActions.StartSSOFallback)
} else if (state.flowType == LoginFlowTypes.PASSWORD) {
val password = views.passwordField.text.toString()
if (password.isBlank()) {
// Prompt to enter something
views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password)
} else {
views.passwordFieldTil.error = null
viewModel.handle(ReAuthActions.ReAuthWithPass(password))
when (state.flowType) {
LoginFlowTypes.SSO -> {
viewModel.handle(ReAuthActions.StartSSOFallback)
}
LoginFlowTypes.PASSWORD -> {
val password = views.passwordField.text.toString()
if (password.isBlank()) {
// Prompt to enter something
views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password)
} else {
views.passwordFieldTil.error = null
viewModel.handle(ReAuthActions.ReAuthWithPass(password))
}
}
else -> {
// not supported
}
} else {
// not supported
}
}
@ -91,5 +95,23 @@ class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
views.passwordReveal.setImageResource(R.drawable.ic_eye)
views.passwordReveal.contentDescription = getString(R.string.a11y_show_password)
}
if (it.lastErrorCode != null) {
when (it.flowType) {
LoginFlowTypes.SSO -> {
views.genericErrorText.isVisible = true
views.genericErrorText.text = getString(R.string.authentication_error)
}
LoginFlowTypes.PASSWORD -> {
views.passwordFieldTil.error = getString(R.string.authentication_error)
}
else -> {
// nop
}
}
} else {
views.passwordFieldTil.error = null
views.genericErrorText.isVisible = false
}
}
}

View file

@ -38,6 +38,7 @@ import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.auth.registration.nextUncompletedStage
import timber.log.Timber
import javax.inject.Inject
@ -47,7 +48,8 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory {
data class Args(
val flowType: String?,
val title: String?,
val session: String?
val session: String?,
val lastErrorCode: String?
) : Parcelable
// For sso
@ -196,17 +198,21 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory {
const val RESULT_FLOW_TYPE = "RESULT_FLOW_TYPE"
const val RESULT_VALUE = "RESULT_VALUE"
fun newIntent(context: Context, fromError: RegistrationFlowResponse, reasonTitle: String?): Intent {
val authType = if (fromError.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }) {
LoginFlowTypes.PASSWORD
} else if (fromError.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.SSO) == true }) {
LoginFlowTypes.SSO
} else {
// TODO, support more auth type?
null
fun newIntent(context: Context, fromError: RegistrationFlowResponse, lastErrorCode: String?, reasonTitle: String?): Intent {
val authType = when (fromError.nextUncompletedStage()) {
LoginFlowTypes.PASSWORD -> {
LoginFlowTypes.PASSWORD
}
LoginFlowTypes.SSO -> {
LoginFlowTypes.SSO
}
else -> {
// TODO, support more auth type?
null
}
}
return Intent(context, ReAuthActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session))
putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode))
}
}
}

View file

@ -23,12 +23,14 @@ data class ReAuthState(
val session: String? = null,
val flowType: String? = null,
val ssoFallbackPageWasShown: Boolean = false,
val passwordVisible: Boolean = false
val passwordVisible: Boolean = false,
val lastErrorCode: String? = null
) : MvRxState {
constructor(args: ReAuthActivity.Args) : this(
args.title,
args.session,
args.flowType
args.flowType,
lastErrorCode = args.lastErrorCode
)
constructor() : this(null, null)

View file

@ -1,110 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.crypto.recover
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.core.text.toSpannable
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.editorActionEvents
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.colorizeMatchingText
import im.vector.app.databinding.FragmentBootstrapEnterAccountPasswordBinding
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class BootstrapAccountPasswordFragment @Inject constructor(
private val colorProvider: ColorProvider
) : VectorBaseFragment<FragmentBootstrapEnterAccountPasswordBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapEnterAccountPasswordBinding {
return FragmentBootstrapEnterAccountPasswordBinding.inflate(inflater, container, false)
}
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recPassPhrase = getString(R.string.account_password)
views.bootstrapDescriptionText.text = getString(R.string.enter_account_password, recPassPhrase)
.toSpannable()
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
views.bootstrapAccountPasswordEditText.hint = getString(R.string.account_password)
views.bootstrapAccountPasswordEditText.editorActionEvents()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
submit()
}
}
.disposeOnDestroyView()
views.bootstrapAccountPasswordEditText.textChanges()
.distinctUntilChanged()
.subscribe {
if (!it.isNullOrBlank()) {
views.bootstrapAccountPasswordTil.error = null
}
}
.disposeOnDestroyView()
views.ssssViewShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) }
views.bootstrapPasswordButton.debouncedClicks { submit() }
withState(sharedViewModel) { state ->
(state.step as? BootstrapStep.AccountPassword)?.failure?.let {
views.bootstrapAccountPasswordTil.error = it
}
}
}
private fun submit() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.AccountPassword) {
return@withState
}
val accountPassword = views.bootstrapAccountPasswordEditText.text?.toString()
if (accountPassword.isNullOrBlank()) {
views.bootstrapAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
} else {
view?.hideKeyboard()
sharedViewModel.handle(BootstrapActions.ReAuth(accountPassword))
}
}
override fun invalidate() = withState(sharedViewModel) { state ->
if (state.step is BootstrapStep.AccountPassword) {
val isPasswordVisible = state.step.isPasswordVisible
views.bootstrapAccountPasswordEditText.showPassword(isPasswordVisible, updateCursor = false)
views.ssssViewShowPassword.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye)
}
}
}

View file

@ -37,7 +37,7 @@ sealed class BootstrapActions : VectorViewModelAction {
object TogglePasswordVisibility : BootstrapActions()
data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions()
data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions()
data class ReAuth(val pass: String) : BootstrapActions()
// data class ReAuth(val pass: String) : BootstrapActions()
object RecoveryKeySaved : BootstrapActions()
object Completed : BootstrapActions()
object SaveReqQueryStarted : BootstrapActions()
@ -47,4 +47,8 @@ sealed class BootstrapActions : VectorViewModelAction {
object HandleForgotBackupPassphrase : BootstrapActions()
data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions()
data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions()
object SsoAuthDone: BootstrapActions()
data class PasswordAuthDone(val password: String): BootstrapActions()
object ReAuthCancelled: BootstrapActions()
}

View file

@ -16,6 +16,7 @@
package im.vector.app.features.crypto.recover
import android.app.Activity
import android.app.Dialog
import android.os.Build
import android.os.Bundle
@ -36,9 +37,12 @@ import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetBootstrapBinding
import im.vector.app.features.auth.ReAuthActivity
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import javax.inject.Inject
import kotlin.reflect.KClass
@ -64,6 +68,25 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
return BottomSheetBootstrapBinding.inflate(inflater, container, false)
}
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
LoginFlowTypes.SSO -> {
viewModel.handle(BootstrapActions.SsoAuthDone)
}
LoginFlowTypes.PASSWORD -> {
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
viewModel.handle(BootstrapActions.PasswordAuthDone(password))
}
else -> {
viewModel.handle(BootstrapActions.ReAuthCancelled)
}
}
} else {
viewModel.handle(BootstrapActions.ReAuthCancelled)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.observeViewEvents { event ->
@ -85,6 +108,14 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
is BootstrapViewEvents.SkipBootstrap -> {
promptSkip()
}
is BootstrapViewEvents.RequestReAuth -> {
ReAuthActivity.newIntent(requireContext(),
event.flowResponse,
event.lastErrorCode,
getString(R.string.initialize_cross_signing)).let { intent ->
reAuthActivityResultLauncher.launch(intent)
}
}
}
}
}
@ -149,11 +180,11 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
views.bootstrapTitleText.text = getString(R.string.set_a_security_phrase_title)
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
}
is BootstrapStep.AccountPassword -> {
is BootstrapStep.AccountReAuth -> {
views.bootstrapIcon.isVisible = true
views.bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
views.bootstrapTitleText.text = getString(R.string.account_password)
showFragment(BootstrapAccountPasswordFragment::class, Bundle())
views.bootstrapTitleText.text = getString(R.string.re_authentication_activity_title)
showFragment(BootstrapReAuthFragment::class, Bundle())
}
is BootstrapStep.Initializing -> {
views.bootstrapIcon.isVisible = true

View file

@ -21,10 +21,8 @@ import im.vector.app.core.platform.ViewModelTask
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@ -34,21 +32,15 @@ import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.util.awaitCallback
import timber.log.Timber
import java.lang.UnsupportedOperationException
import java.util.UUID
import javax.inject.Inject
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
sealed class BootstrapResult {
@ -57,16 +49,12 @@ sealed class BootstrapResult {
abstract class Failure(val error: String?) : BootstrapResult()
class UnsupportedAuthFlow : Failure(null)
data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage)
data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null)
class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage)
object MissingPrivateKey : Failure(null)
data class PasswordAuthFlowMissing(val sessionId: String) : Failure(null)
}
interface BootstrapProgressListener {
@ -74,7 +62,7 @@ interface BootstrapProgressListener {
}
data class Params(
val userPasswordAuth: UserPasswordAuth? = null,
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
val progressListener: BootstrapProgressListener? = null,
val passphrase: String?,
val keySpec: SsssKeySpec? = null,
@ -107,21 +95,10 @@ class BootstrapCrossSigningTask @Inject constructor(
try {
awaitCallback<Unit> {
crossSigningService.initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
if (flowResponse.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) {
val updatedAuth = params.userPasswordAuth?.copy(session = flowResponse.session)
if (updatedAuth == null) {
promise.resumeWith(Result.failure(UnsupportedOperationException()))
} else {
promise.resume(updatedAuth)
}
} else {
promise.resumeWith(Result.failure(UnsupportedOperationException()))
}
}
},
it)
crossSigningService.initializeCrossSigning(
params.userInteractiveAuthInterceptor,
it
)
}
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
return BootstrapResult.SuccessCrossSigningOnly
@ -332,16 +309,6 @@ class BootstrapCrossSigningTask @Inject constructor(
private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult {
if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) {
return BootstrapResult.InvalidPasswordError(failure.error)
} else {
val registrationFlowResponse = failure.toRegistrationFlowResponse()
if (registrationFlowResponse != null) {
return if (registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }) {
BootstrapResult.PasswordAuthFlowMissing(registrationFlowResponse.session ?: "")
} else {
// can't do this from here
BootstrapResult.UnsupportedAuthFlow()
}
}
}
return BootstrapResult.GenericError(failure)
}

View file

@ -0,0 +1,86 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.crypto.recover
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.databinding.FragmentBootstrapReauthBinding
import javax.inject.Inject
class BootstrapReAuthFragment @Inject constructor(
private val colorProvider: ColorProvider
) : VectorBaseFragment<FragmentBootstrapReauthBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapReauthBinding {
return FragmentBootstrapReauthBinding.inflate(inflater, container, false)
}
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.bootstrapRetryButton.debouncedClicks { submit() }
views.bootstrapCancelButton.debouncedClicks { cancel() }
}
private fun submit() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.AccountReAuth) {
return@withState
}
if (state.passphrase != null) {
sharedViewModel.handle(BootstrapActions.DoInitialize(state.passphrase))
} else {
sharedViewModel.handle(BootstrapActions.DoInitializeGeneratedKey)
}
}
private fun cancel() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.AccountReAuth) {
return@withState
}
sharedViewModel.handle(BootstrapActions.GoBack)
}
override fun invalidate() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.AccountReAuth) {
return@withState
}
val failure = state.step.failure
if (failure == null) {
views.reAuthFailureText.text = null
views.reAuthFailureText.isVisible = false
views.waitingProgress.isVisible = true
views.bootstrapCancelButton.isVisible = false
views.bootstrapRetryButton.isVisible = false
} else {
views.reAuthFailureText.text = failure
views.reAuthFailureText.isVisible = true
views.waitingProgress.isVisible = false
views.bootstrapCancelButton.isVisible = true
views.bootstrapRetryButton.isVisible = true
}
}
}

View file

@ -26,8 +26,8 @@ import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.nulabinc.zxcvbn.Zxcvbn
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.exhaustive
@ -37,14 +37,22 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.features.login.ReAuthHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.util.awaitCallback
import java.io.OutputStream
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class BootstrapSharedViewModel @AssistedInject constructor(
@Assisted initialState: BootstrapViewState,
@ -66,14 +74,17 @@ class BootstrapSharedViewModel @AssistedInject constructor(
fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel
}
private var _pendingSession: String? = null
// private var _pendingSession: String? = null
var uiaContinuation: Continuation<UIABaseAuth>? = null
var pendingAuth: UIABaseAuth? = null
init {
when (args.setUpMode) {
SetupMode.PASSPHRASE_RESET,
SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET,
SetupMode.HARD_RESET -> {
SetupMode.HARD_RESET -> {
setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
}
@ -81,10 +92,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
SetupMode.CROSS_SIGNING_ONLY -> {
// Go straight to account password
setState {
copy(step = BootstrapStep.AccountPassword(false))
copy(step = BootstrapStep.AccountReAuth())
}
}
SetupMode.NORMAL -> {
SetupMode.NORMAL -> {
// need to check if user have an existing keybackup
setState {
copy(step = BootstrapStep.CheckingMigration)
@ -136,8 +147,8 @@ class BootstrapSharedViewModel @AssistedInject constructor(
override fun handle(action: BootstrapActions) = withState { state ->
when (action) {
is BootstrapActions.GoBack -> queryBack()
BootstrapActions.TogglePasswordVisibility -> {
is BootstrapActions.GoBack -> queryBack()
BootstrapActions.TogglePasswordVisibility -> {
when (state.step) {
is BootstrapStep.SetupPassphrase -> {
setState {
@ -149,10 +160,8 @@ class BootstrapSharedViewModel @AssistedInject constructor(
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
}
}
is BootstrapStep.AccountPassword -> {
setState {
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
}
is BootstrapStep.AccountReAuth -> {
// nop
}
is BootstrapStep.GetBackupSecretPassForMigration -> {
setState {
@ -162,13 +171,13 @@ class BootstrapSharedViewModel @AssistedInject constructor(
else -> Unit
}
}
BootstrapActions.StartKeyBackupMigration -> {
BootstrapActions.StartKeyBackupMigration -> {
handleStartMigratingKeyBackup()
}
is BootstrapActions.Start -> {
is BootstrapActions.Start -> {
handleStart(action)
}
is BootstrapActions.UpdateCandidatePassphrase -> {
is BootstrapActions.UpdateCandidatePassphrase -> {
val strength = zxcvbn.measure(action.pass)
setState {
copy(
@ -177,7 +186,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
}
is BootstrapActions.GoToConfirmPassphrase -> {
is BootstrapActions.GoToConfirmPassphrase -> {
setState {
copy(
passphrase = action.passphrase,
@ -194,18 +203,9 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
}
is BootstrapActions.DoInitialize -> {
is BootstrapActions.DoInitialize -> {
if (state.passphrase == state.passphraseRepeat) {
val userPassword = reAuthHelper.data
if (userPassword == null) {
setState {
copy(
step = BootstrapStep.AccountPassword(false)
)
}
} else {
startInitializeFlow(userPassword)
}
startInitializeFlow(state)
} else {
setState {
copy(
@ -214,74 +214,74 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
}
is BootstrapActions.DoInitializeGeneratedKey -> {
val userPassword = reAuthHelper.data
if (userPassword == null) {
setState {
copy(
passphrase = null,
passphraseRepeat = null,
step = BootstrapStep.AccountPassword(false)
)
}
} else {
setState {
copy(
passphrase = null,
passphraseRepeat = null
)
}
startInitializeFlow(userPassword)
}
is BootstrapActions.DoInitializeGeneratedKey -> {
startInitializeFlow(state)
}
BootstrapActions.RecoveryKeySaved -> {
BootstrapActions.RecoveryKeySaved -> {
_viewEvents.post(BootstrapViewEvents.RecoveryKeySaved)
setState {
copy(step = BootstrapStep.SaveRecoveryKey(true))
}
}
BootstrapActions.Completed -> {
BootstrapActions.Completed -> {
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
}
BootstrapActions.GoToCompleted -> {
BootstrapActions.GoToCompleted -> {
setState {
copy(step = BootstrapStep.DoneSuccess)
}
}
BootstrapActions.SaveReqQueryStarted -> {
BootstrapActions.SaveReqQueryStarted -> {
setState {
copy(recoverySaveFileProcess = Loading())
}
}
is BootstrapActions.SaveKeyToUri -> {
is BootstrapActions.SaveKeyToUri -> {
saveRecoveryKeyToUri(action.os)
}
BootstrapActions.SaveReqFailed -> {
BootstrapActions.SaveReqFailed -> {
setState {
copy(recoverySaveFileProcess = Uninitialized)
}
}
BootstrapActions.GoToEnterAccountPassword -> {
BootstrapActions.GoToEnterAccountPassword -> {
setState {
copy(step = BootstrapStep.AccountPassword(false))
copy(step = BootstrapStep.AccountReAuth())
}
}
BootstrapActions.HandleForgotBackupPassphrase -> {
BootstrapActions.HandleForgotBackupPassphrase -> {
if (state.step is BootstrapStep.GetBackupSecretPassForMigration) {
setState {
copy(step = BootstrapStep.GetBackupSecretPassForMigration(state.step.isPasswordVisible, true))
}
} else return@withState
}
is BootstrapActions.ReAuth -> {
startInitializeFlow(action.pass)
}
is BootstrapActions.DoMigrateWithPassphrase -> {
// is BootstrapActions.ReAuth -> {
// startInitializeFlow(action.pass)
// }
is BootstrapActions.DoMigrateWithPassphrase -> {
startMigrationFlow(state.step, action.passphrase, null)
}
is BootstrapActions.DoMigrateWithRecoveryKey -> {
is BootstrapActions.DoMigrateWithRecoveryKey -> {
startMigrationFlow(state.step, null, action.recoveryKey)
}
BootstrapActions.SsoAuthDone -> {
uiaContinuation?.resume(DefaultBaseAuth(session = pendingAuth?.session ?: ""))
}
is BootstrapActions.PasswordAuthDone -> {
uiaContinuation?.resume(
UserPasswordAuth(
session = pendingAuth?.session,
password = action.password,
user = session.myUserId
)
)
}
BootstrapActions.ReAuthCancelled -> {
setState {
copy(step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.authentication_error)))
}
}
}.exhaustive
}
@ -293,7 +293,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
} else {
startInitializeFlow(null)
startInitializeFlow(it)
}
}
@ -346,16 +346,16 @@ class BootstrapSharedViewModel @AssistedInject constructor(
migrationRecoveryKey = recoveryKey
)
}
val userPassword = reAuthHelper.data
if (userPassword == null) {
setState {
copy(
step = BootstrapStep.AccountPassword(false)
)
}
} else {
startInitializeFlow(userPassword)
}
// val userPassword = reAuthHelper.data
// if (userPassword == null) {
// setState {
// copy(
// step = BootstrapStep.AccountPassword(false)
// )
// }
// } else {
withState { startInitializeFlow(it) }
// }
}
is BackupToQuadSMigrationTask.Result.Failure -> {
_viewEvents.post(
@ -372,7 +372,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
private fun startInitializeFlow(userPassword: String?) = withState { state ->
private fun startInitializeFlow(state: BootstrapViewState) {
val previousStep = state.step
setState {
@ -389,19 +389,45 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
viewModelScope.launch(Dispatchers.IO) {
val userPasswordAuth = userPassword?.let {
UserPasswordAuth(
// Note that _pendingSession may or may not be null, this is OK, it will be managed by the task
session = _pendingSession,
user = session.myUserId,
password = it
)
val interceptor = object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
when (flowResponse.nextUncompletedStage()) {
LoginFlowTypes.PASSWORD -> {
pendingAuth = UserPasswordAuth(
// Note that _pendingSession may or may not be null, this is OK, it will be managed by the task
session = flowResponse.session,
user = session.myUserId,
password = null
)
uiaContinuation = promise
setState {
copy(
step = BootstrapStep.AccountReAuth()
)
}
_viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode))
}
LoginFlowTypes.SSO -> {
pendingAuth = DefaultBaseAuth(flowResponse.session)
uiaContinuation = promise
setState {
copy(
step = BootstrapStep.AccountReAuth()
)
}
_viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode))
}
else -> {
promise.resumeWith(Result.failure(UnsupportedOperationException()))
}
}
}
}
viewModelScope.launch(Dispatchers.IO) {
bootstrapTask.invoke(this,
Params(
userPasswordAuth = userPasswordAuth,
userInteractiveAuthInterceptor = interceptor,
progressListener = progressListener,
passphrase = state.passphrase,
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } },
@ -410,10 +436,9 @@ class BootstrapSharedViewModel @AssistedInject constructor(
) { bootstrapResult ->
when (bootstrapResult) {
is BootstrapResult.SuccessCrossSigningOnly -> {
// TPD
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
}
is BootstrapResult.Success -> {
is BootstrapResult.Success -> {
setState {
copy(
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
@ -424,30 +449,15 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
}
is BootstrapResult.PasswordAuthFlowMissing -> {
// Ask the password to the user
_pendingSession = bootstrapResult.sessionId
is BootstrapResult.InvalidPasswordError -> {
// it's a bad password / auth
setState {
copy(
step = BootstrapStep.AccountPassword(false)
step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.auth_invalid_login_param))
)
}
}
is BootstrapResult.UnsupportedAuthFlow -> {
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
_viewEvents.post(BootstrapViewEvents.Dismiss(false))
}
is BootstrapResult.InvalidPasswordError -> {
// it's a bad password
// We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error
_pendingSession = null
setState {
copy(
step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param))
)
}
}
is BootstrapResult.Failure -> {
is BootstrapResult.Failure -> {
if (bootstrapResult is BootstrapResult.GenericError
&& bootstrapResult.failure is Failure.OtherServerError
&& bootstrapResult.failure.httpCode == 401) {
@ -497,7 +507,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
}
is BootstrapStep.SetupPassphrase -> {
is BootstrapStep.SetupPassphrase -> {
setState {
copy(
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
@ -507,7 +517,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
}
is BootstrapStep.ConfirmPassphrase -> {
is BootstrapStep.ConfirmPassphrase -> {
setState {
copy(
step = BootstrapStep.SetupPassphrase(
@ -516,19 +526,19 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
}
is BootstrapStep.AccountPassword -> {
is BootstrapStep.AccountReAuth -> {
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
}
BootstrapStep.Initializing -> {
BootstrapStep.Initializing -> {
// do we let you cancel from here?
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
}
is BootstrapStep.SaveRecoveryKey,
BootstrapStep.DoneSuccess -> {
BootstrapStep.DoneSuccess -> {
// nop
}
BootstrapStep.CheckingMigration -> Unit
is BootstrapStep.FirstForm -> {
BootstrapStep.CheckingMigration -> Unit
is BootstrapStep.FirstForm -> {
_viewEvents.post(
when (args.setUpMode) {
SetupMode.CROSS_SIGNING_ONLY,
@ -537,7 +547,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
)
}
is BootstrapStep.GetBackupSecretForMigration -> {
is BootstrapStep.GetBackupSecretForMigration -> {
setState {
copy(
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
@ -555,7 +565,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
private fun BackupToQuadSMigrationTask.Result.Failure.toHumanReadable(): String {
return when (this) {
is BackupToQuadSMigrationTask.Result.InvalidRecoverySecret -> stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)
is BackupToQuadSMigrationTask.Result.ErrorFailure -> errorFormatter.toHumanReadable(throwable)
is BackupToQuadSMigrationTask.Result.ErrorFailure -> errorFormatter.toHumanReadable(throwable)
// is BackupToQuadSMigrationTask.Result.NoKeyBackupVersion,
// is BackupToQuadSMigrationTask.Result.IllegalParams,
else -> stringProvider.getString(R.string.unexpected_error)

View file

@ -52,11 +52,11 @@ package im.vector.app.features.crypto.recover
* BootstrapStep.ConfirmPassphrase
*
*
* is password needed?
* is password/reauth needed?
*
*
*
* BootstrapStep.AccountPassword
* BootstrapStep.AccountReAuth
*
*
*
@ -94,7 +94,7 @@ sealed class BootstrapStep {
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep()
data class AccountReAuth(val failure: String? = null) : BootstrapStep()
abstract class GetBackupSecretForMigration : BootstrapStep()
data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration()

View file

@ -17,10 +17,12 @@
package im.vector.app.features.crypto.recover
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
sealed class BootstrapViewEvents : VectorViewEvents {
data class Dismiss(val success: Boolean) : BootstrapViewEvents()
data class ModalError(val error: String) : BootstrapViewEvents()
object RecoveryKeySaved : BootstrapViewEvents()
data class SkipBootstrap(val genKeyOption: Boolean = true) : BootstrapViewEvents()
data class RequestReAuth(val flowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : BootstrapViewEvents()
}

View file

@ -41,6 +41,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth
@ -159,8 +160,8 @@ class HomeActivityViewModel @AssistedInject constructor(
Timber.d("Initialize cross signing")
session.cryptoService().crossSigningService().initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) {
override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation<UIABaseAuth>) {
if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD) {
promise.resume(
UserPasswordAuth(
session = flow.session,
@ -251,8 +252,8 @@ class HomeActivityViewModel @AssistedInject constructor(
Timber.d("Initialize cross signing")
session.cryptoService().crossSigningService().initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) {
override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation<UIABaseAuth>) {
if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD) {
UserPasswordAuth(
session = flow.session,
user = session.myUserId,

View file

@ -229,10 +229,13 @@ class DefaultNavigator @Inject constructor(
}
override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) {
// if cross signing is enabled we should propose full 4S
// if cross signing is enabled and trusted or not set up at all we should propose full 4S
sessionHolder.getSafeActiveSession()?.let { session ->
if (session.cryptoService().crossSigningService().canCrossSign() && context is AppCompatActivity) {
BootstrapBottomSheet.show(context.supportFragmentManager, SetupMode.NORMAL)
if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null
|| session.cryptoService().crossSigningService().canCrossSign()) {
(context as? AppCompatActivity)?.let {
BootstrapBottomSheet.show(it.supportFragmentManager, SetupMode.NORMAL)
}
} else {
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
}

View file

@ -88,7 +88,10 @@ class CrossSigningSettingsFragment @Inject constructor(
Unit
}
is CrossSigningSettingsViewEvents.RequestReAuth -> {
ReAuthActivity.newIntent(requireContext(), event.registrationFlowResponse, getString(R.string.initialize_cross_signing)).let { intent ->
ReAuthActivity.newIntent(requireContext(),
event.registrationFlowResponse,
event.lastErrorCode,
getString(R.string.initialize_cross_signing)).let { intent ->
reAuthActivityResultLauncher.launch(intent)
}
}

View file

@ -24,7 +24,7 @@ import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowRespons
*/
sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse) : CrossSigningSettingsViewEvents()
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : CrossSigningSettingsViewEvents()
data class ShowModalWaitingView(val status: String?) : CrossSigningSettingsViewEvents()
object HideModalWaitingView : CrossSigningSettingsViewEvents()
}

View file

@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
@ -95,9 +96,9 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
awaitCallback<Unit> {
session.cryptoService().crossSigningService().initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation<UIABaseAuth>) {
Timber.d("## UIA : initializeCrossSigning UIA")
if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true && reAuthHelper.data != null) {
if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errorCode == null) {
UserPasswordAuth(
session = null,
user = session.myUserId,
@ -105,7 +106,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(
).let { promise.resume(it) }
} else {
Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity")
_viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flow))
_viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flow, errorCode))
pendingAuth = DefaultBaseAuth(session = flow.session)
uiaContinuation = promise
}

View file

@ -23,6 +23,5 @@ data class CrossSigningSettingsViewState(
val crossSigningInfo: MXCrossSigningInfo? = null,
val xSigningIsEnableInAccount: Boolean = false,
val xSigningKeysAreTrusted: Boolean = false,
val xSigningKeyCanSign: Boolean = true,
// val pendingAuthSession: String? = null
val xSigningKeyCanSign: Boolean = true
) : MvRxState

View file

@ -33,7 +33,7 @@ sealed class DevicesViewEvents : VectorViewEvents {
// object RequestPassword : DevicesViewEvents()
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse) : DevicesViewEvents()
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvents()
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents()

View file

@ -49,6 +49,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
@ -330,9 +331,9 @@ class DevicesViewModel @AssistedInject constructor(
try {
awaitCallback<Unit> {
session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor {
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation<UIABaseAuth>) {
Timber.d("## UIA : deleteDevice UIA")
if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true && reAuthHelper.data != null) {
if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errorCode == null) {
UserPasswordAuth(
session = null,
user = session.myUserId,
@ -340,7 +341,7 @@ class DevicesViewModel @AssistedInject constructor(
).let { promise.resume(it) }
} else {
Timber.d("## UIA : deleteDevice UIA > start reauth activity")
_viewEvents.post(DevicesViewEvents.RequestReAuth(flow))
_viewEvents.post(DevicesViewEvents.RequestReAuth(flow, errorCode))
pendingAuth = DefaultBaseAuth(session = flow.session)
uiaContinuation = promise
}

View file

@ -173,7 +173,10 @@ class VectorSettingsDevicesFragment @Inject constructor(
* Show a dialog to ask for user password, or use a previously entered password.
*/
private fun maybeShowDeleteDeviceWithPasswordDialog(reAuthReq: DevicesViewEvents.RequestReAuth) {
ReAuthActivity.newIntent(requireContext(), reAuthReq.registrationFlowResponse, getString(R.string.devices_delete_dialog_title)).let { intent ->
ReAuthActivity.newIntent(requireContext(),
reAuthReq.registrationFlowResponse,
reAuthReq.lastErrorCode,
getString(R.string.devices_delete_dialog_title)).let { intent ->
reAuthActivityResultLauncher.launch(intent)
}
}

View file

@ -115,8 +115,10 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS
// So recovery is not setup
// Check if cross signing is enabled and local secrets known
if (crossSigningInfo.getOrNull()?.isTrusted() == true
&& pInfo.getOrNull()?.allKnown().orFalse()
if (
crossSigningInfo.getOrNull() == null
|| (crossSigningInfo.getOrNull()?.isTrusted() == true
&& pInfo.getOrNull()?.allKnown().orFalse())
) {
// So 4S is not setup and we have local secrets,
return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup())

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/bootstrapDescriptionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/waitingProgress"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/re_authentication_activity_title" />
<ProgressBar
android:id="@+id/waitingProgress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@id/reAuthFailureText"
app:layout_constraintTop_toBottomOf="@+id/bootstrapDescriptionText" />
<TextView
android:id="@+id/reAuthFailureText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="?colorError"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/buttonFlow"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintTop_toBottomOf="@id/waitingProgress"
tools:text="Authentication failed" />
<com.google.android.material.button.MaterialButton
android:id="@+id/bootstrapCancelButton"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/cancel"
tools:ignore="MissingConstraints" />
<com.google.android.material.button.MaterialButton
android:id="@+id/bootstrapRetryButton"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/global_retry"
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/buttonFlow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:constraint_referenced_ids="bootstrapCancelButton, bootstrapRetryButton"
app:layout_constraintTop_toBottomOf="@id/reAuthFailureText"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -69,14 +69,28 @@
</FrameLayout>
<!-- <TextView-->
<!-- android:id="@+id/loginPasswordNotice"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:gravity="start"-->
<!-- android:text="@string/login_signin_matrix_id_password_notice"-->
<!-- android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"-->
<!-- android:visibility="gone"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toBottomOf="@id/passwordContainer"-->
<!-- tools:visibility="visible" />-->
<TextView
android:id="@+id/loginPasswordNotice"
android:id="@+id/genericErrorText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start"
android:text="@string/login_signin_matrix_id_password_notice"
android:text="@string/authentication_error"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:visibility="gone"
android:textColor="?colorError"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/passwordContainer"
@ -93,7 +107,7 @@
android:layout_marginBottom="20dp"
android:text="@string/_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginPasswordNotice" />
app:layout_constraintTop_toBottomOf="@id/genericErrorText" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>