mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 20:10:04 +03:00
UIA fixes + better error support
This commit is contained in:
parent
1244d00b31
commit
da16ec0af3
30 changed files with 524 additions and 401 deletions
|
@ -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>)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
68
vector/src/main/res/layout/fragment_bootstrap_reauth.xml
Normal file
68
vector/src/main/res/layout/fragment_bootstrap_reauth.xml
Normal 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>
|
|
@ -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>
|
Loading…
Add table
Reference in a new issue