From 1244d00b31fce45734a7519bb792ed8692b7bfa1 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 27 Jan 2021 09:52:18 +0100 Subject: [PATCH 01/18] SSO UIA --- .../auth/UserInteractiveAuthInterceptor.kt | 48 ++++ .../matrix/android/sdk/api/session/Session.kt | 2 + .../sdk/api/session/crypto/CryptoService.kt | 3 +- .../crosssigning/CrossSigningService.kt | 4 +- .../internal/crypto/DefaultCryptoService.kt | 5 +- .../DefaultCrossSigningService.kt | 16 +- .../crypto/model/rest/DefaultBaseAuth.kt | 32 +++ .../crypto/model/rest/DeleteDeviceParams.kt | 2 +- .../crypto/model/rest/TokenBasedAuth.kt | 69 ++++++ .../internal/crypto/model/rest/UIABaseAuth.kt | 31 +++ .../model/rest/UploadSigningKeysBody.kt | 2 +- .../crypto/model/rest/UserPasswordAuth.kt | 16 +- .../internal/crypto/tasks/DeleteDeviceTask.kt | 53 ++++- .../tasks/DeleteDeviceWithUserPasswordTask.kt | 4 +- .../tasks/InitializeCrossSigningTask.kt | 58 ++++- .../crypto/tasks/UploadSigningKeysTask.kt | 36 +-- .../sdk/internal/session/DefaultSession.kt | 13 ++ .../src/main/res/values/strings.xml | 1 + vector/src/main/AndroidManifest.xml | 21 ++ .../im/vector/app/core/di/ScreenComponent.kt | 2 + .../core/ui/list/GenericPositiveButtonItem.kt | 63 ++++++ .../app/features/auth/PromptFragment.kt | 95 ++++++++ .../vector/app/features/auth/ReAuthActions.kt | 27 +++ .../app/features/auth/ReAuthActivity.kt | 213 ++++++++++++++++++ .../vector/app/features/auth/ReAuthEvents.kt | 25 ++ .../vector/app/features/auth/ReAuthState.kt | 35 +++ .../app/features/auth/ReAuthViewModel.kt | 79 +++++++ .../recover/BootstrapCrossSigningTask.kt | 22 +- .../features/home/HomeActivityViewModel.kt | 44 +++- .../VectorSettingsSecurityPrivacyFragment.kt | 4 - .../CrossSigningSettingsAction.kt | 7 +- .../CrossSigningSettingsController.kt | 36 ++- .../CrossSigningSettingsFragment.kt | 49 +++- .../CrossSigningSettingsViewEvents.kt | 4 + .../CrossSigningSettingsViewModel.kt | 103 ++++++++- .../CrossSigningSettingsViewState.kt | 3 +- .../settings/devices/DevicesAction.kt | 6 +- .../settings/devices/DevicesViewEvents.kt | 6 +- .../settings/devices/DevicesViewModel.kt | 174 +++++++------- .../devices/VectorSettingsDevicesFragment.kt | 46 ++-- .../res/layout/fragment_reauth_confirm.xml | 99 ++++++++ .../main/res/layout/item_positive_button.xml | 19 ++ vector/src/main/res/values/strings.xml | 4 + 43 files changed, 1381 insertions(+), 200 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/TokenBasedAuth.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UIABaseAuth.kt create mode 100644 vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt create mode 100644 vector/src/main/res/layout/fragment_reauth_confirm.xml create mode 100644 vector/src/main/res/layout/item_positive_button.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt new file mode 100644 index 0000000000..11cf2d2cfb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt @@ -0,0 +1,48 @@ +/* + * 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.api.auth + +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth +import kotlin.coroutines.Continuation + +/** + * Some API endpoints require authentication that interacts with the user. + * The homeserver may provide many different ways of authenticating, such as user/password auth, login via a social network (OAuth2), + * login by confirming a token sent to their email address, etc. + * + * The process takes the form of one or more 'stages'. + * At each stage the client submits a set of data for a given authentication type and awaits a response from the server, + * which will either be a final success or a request to perform an additional stage. + * This exchange continues until the final success. + * + * For each endpoint, a server offers one or more 'flows' that the client can use to authenticate itself. + * Each flow comprises a series of stages, as described above. + * The client is free to choose which flow it follows, however the flow's stages must be completed in order. + * Failing to follow the flows in order must result in an HTTP 401 response. + * When all stages in a flow are complete, authentication is complete and the API call succeeds. + */ +interface UserInteractiveAuthInterceptor { + + /** + * When the API needs additional auth, this will be called. + * Implementation should check the flows from flow response and act accordingly. + * 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) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 8a95baf3cb..bc6fd3adad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -245,6 +245,8 @@ interface Session : val sharedSecretStorageService: SharedSecretStorageService + fun getUIASsoFallbackUrl(authenticationSessionId: String): String + /** * Maintenance API, allows to print outs info on DB size to logcat */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 0eefca1b4c..fa5ea359e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.lifecycle.LiveData import androidx.paging.PagedList import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService @@ -53,7 +54,7 @@ interface CryptoService { fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) - fun deleteDevice(deviceId: String, callback: MatrixCallback) + fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt index 6a646cd4c7..359e33cc2c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt @@ -18,10 +18,10 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult -import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo interface CrossSigningService { @@ -40,7 +40,7 @@ interface CrossSigningService { * Initialize cross signing for this user. * Users needs to enter credentials */ - fun initializeCrossSigning(authParams: UserPasswordAuth?, + fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback) fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index ebd809f777..678bc9819f 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure @@ -207,9 +208,9 @@ internal class DefaultCryptoService @Inject constructor( .executeBy(taskExecutor) } - override fun deleteDevice(deviceId: String, callback: MatrixCallback) { + override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDeviceTask - .configureWith(DeleteDeviceTask.Params(deviceId)) { + .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) { this.executionThread = TaskThread.CRYPTO this.callback = callback } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index bcad448eb6..e85cea30a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -19,30 +19,30 @@ package org.matrix.android.sdk.internal.crypto.crosssigning import androidx.lifecycle.LiveData import androidx.work.BackoffPolicy import androidx.work.ExistingWorkPolicy +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder -import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask +import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmUtility @@ -147,11 +147,11 @@ internal class DefaultCrossSigningService @Inject constructor( * - Sign the keys and upload them * - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures */ - override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback) { + override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback) { Timber.d("## CrossSigning initializeCrossSigning") val params = InitializeCrossSigningTask.Params( - authParams = authParams + interactiveAuthInterceptor = uiaInterceptor ) initializeCrossSigningTask.configureWith(params) { this.callbackThread = TaskThread.CRYPTO diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt new file mode 100644 index 0000000000..7d55f3b6cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt @@ -0,0 +1,32 @@ +/* + * 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.crypto.model.rest + +data class DefaultBaseAuth( + /** + * This is a session identifier that the client must pass back to the homeserver, + * if one is provided, in subsequent attempts to authenticate in the same API call. + */ + override val session: String? = null + +) : UIABaseAuth { + override fun hasAuthInfo() = true + + override fun copyWithSession(session: String) = this.copy(session = session) + + override fun asMap(): Map = mapOf("session" to session) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt index 0ce6f1f41c..f636ab890d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt @@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class DeleteDeviceParams( @Json(name = "auth") - val userPasswordAuth: UserPasswordAuth? = null + val auth: Map? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/TokenBasedAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/TokenBasedAuth.kt new file mode 100644 index 0000000000..479ac3b0c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/TokenBasedAuth.kt @@ -0,0 +1,69 @@ +/* + * 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.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes + +/** + * This class provides the authentication data by using user and password + */ +@JsonClass(generateAdapter = true) +data class TokenBasedAuth( + + /** + * This is a session identifier that the client must pass back to the homeserver, + * if one is provided, in subsequent attempts to authenticate in the same API call. + */ + @Json(name = "session") + override val session: String? = null, + + /** + * A client may receive a login token via some external service, such as email or SMS. + * Note that a login token is separate from an access token, the latter providing general authentication to various API endpoints. + */ + @Json(name = "token") + val token: String? = null, + + /** + * The txn_id should be a random string generated by the client for the request. + * The same txn_id should be used if retrying the request. + * The txn_id may be used by the server to disallow other devices from using the token, + * thus providing "single use" tokens while still allowing the device to retry the request. + * This would be done by tying the token to the txn_id server side, as well as potentially invalidating + * the token completely once the device has successfully logged in + * (e.g. when we receive a request from the newly provisioned access_token). + */ + @Json(name = "txn_id") + val transactionId: String? = null, + + // registration information + @Json(name = "type") + val type: String? = LoginFlowTypes.TOKEN + +) : UIABaseAuth { + override fun hasAuthInfo() = token != null + + override fun copyWithSession(session: String) = this.copy(session = session) + + override fun asMap(): Map = mapOf( + "session" to session, + "token" to token, + "transactionId" to transactionId, + "type" to type + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UIABaseAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UIABaseAuth.kt new file mode 100644 index 0000000000..246bec42e6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UIABaseAuth.kt @@ -0,0 +1,31 @@ +/* + * 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.crypto.model.rest + +interface UIABaseAuth { + /** + * This is a session identifier that the client must pass back to the homeserver, + * if one is provided, in subsequent attempts to authenticate in the same API call. + */ + val session: String? + + fun hasAuthInfo(): Boolean + + fun copyWithSession(session: String): UIABaseAuth + + fun asMap() : Map +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt index 3418bb327d..d24b7ae5f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt @@ -30,5 +30,5 @@ internal data class UploadSigningKeysBody( val userSigningKey: RestKeyInfo? = null, @Json(name = "auth") - val auth: UserPasswordAuth? = null + val auth: Map? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt index ba8b34096c..0db6ecb7ab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt @@ -27,7 +27,7 @@ data class UserPasswordAuth( // device device session id @Json(name = "session") - val session: String? = null, + override val session: String? = null, // registration information @Json(name = "type") @@ -38,4 +38,16 @@ data class UserPasswordAuth( @Json(name = "password") val password: String? = null -) +) : UIABaseAuth { + + override fun hasAuthInfo() = password != null + + override fun copyWithSession(session: String) = this.copy(session = session) + + override fun asMap(): Map = mapOf( + "session" to session, + "user" to user, + "password" to password, + "type" to type + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 8f1569a037..3c1721b06b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -16,18 +16,24 @@ 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.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth import org.matrix.android.sdk.internal.network.GlobalErrorReceiver 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 { data class Params( - val deviceId: String + val deviceId: String, + val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?, + val userAuthParam: UIABaseAuth? ) } @@ -39,12 +45,49 @@ internal class DefaultDeleteDeviceTask @Inject constructor( override suspend fun execute(params: DeleteDeviceTask.Params) { try { executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) + apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap())) } } catch (throwable: Throwable) { - throw throwable.toRegistrationFlowResponse() - ?.let { Failure.RegistrationFlowError(it) } - ?: throwable + if (params.userInteractiveAuthInterceptor == null || !handleUIA(throwable, params)) { + 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 { 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 } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt index b4c1e6d27c..3cfe26fa82 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -44,12 +44,12 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor( return executeRequest(globalErrorReceiver) { apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams( - userPasswordAuth = UserPasswordAuth( + auth = UserPasswordAuth( type = LoginFlowTypes.PASSWORD, session = params.authSession, user = userId, password = params.password - ) + ).asMap() ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt index 6c0a76fa7d..e261828e94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt @@ -17,24 +17,28 @@ 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.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.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.task.Task 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 { data class Params( - val authParams: UserPasswordAuth? + val interactiveAuthInterceptor: UserInteractiveAuthInterceptor? ) data class Result( @@ -117,10 +121,18 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor( .key(sskPublicKey) .signature(userId, masterPublicKey, signedSSK) .build(), - userPasswordAuth = params.authParams + userAuthParam = null +// userAuthParam = params.authParams ) - uploadSigningKeysTask.execute(uploadSigningKeysParams) + try { + uploadSigningKeysTask.execute(uploadSigningKeysParams) + } catch (failure: Throwable) { + if (params.interactiveAuthInterceptor == null || !handleUIA(failure, params, uploadSigningKeysParams)) { + Timber.d("## UIA: propagate failure") + throw failure + } + } // Sign the current device with SSK val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() @@ -169,4 +181,42 @@ 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 { 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 + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt index cceff355bb..1723e21ed4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt @@ -16,14 +16,12 @@ package org.matrix.android.sdk.internal.crypto.tasks -import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse +import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody -import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.crypto.model.toRest import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest @@ -39,15 +37,9 @@ internal interface UploadSigningKeysTask : Task%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys. + Failed to set up Cross Signing diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 0341059674..1028f200af 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -242,6 +242,27 @@ + + + + + + + + + + + + + + + + + () { + + @EpoxyAttribute + var text: String? = null + + @EpoxyAttribute + var buttonClickAction: View.OnClickListener? = null + + @EpoxyAttribute + @ColorInt + var textColor: Int? = null + + @EpoxyAttribute + @DrawableRes + var iconRes: Int? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.button.text = text + if (iconRes != null) { + holder.button.setIconResource(iconRes!!) + } else { + holder.button.icon = null + } + + buttonClickAction?.let { holder.button.setOnClickListener(it) } + } + + class Holder : VectorEpoxyHolder() { + val button by bind(R.id.itemGenericItemButton) + } +} diff --git a/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt new file mode 100644 index 0000000000..6556e6ae65 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2021 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.auth + +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.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.extensions.showPassword +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentReauthConfirmBinding +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes + +class PromptFragment : VectorBaseFragment() { + + private val viewModel: ReAuthViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentReauthConfirmBinding.inflate(layoutInflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.reAuthConfirmButton.debouncedClicks { + onButtonClicked() + } + views.passwordReveal.debouncedClicks { + viewModel.handle(ReAuthActions.StartSSOFallback) + } + + views.passwordReveal.debouncedClicks { + viewModel.handle(ReAuthActions.TogglePassVisibility) + } + } + + 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)) + } + } else { + // not supported + } + } + + override fun invalidate() = withState(viewModel) { + when (it.flowType) { + LoginFlowTypes.SSO -> { + views.passwordContainer.isVisible = false + views.reAuthConfirmButton.text = getString(R.string.auth_login_sso) + } + LoginFlowTypes.PASSWORD -> { + views.passwordContainer.isVisible = true + views.reAuthConfirmButton.text = getString(R.string._continue) + } + else -> { + // This login flow is not supported, you should use web? + } + } + + views.passwordField.showPassword(it.passwordVisible) + + if (it.passwordVisible) { + views.passwordReveal.setImageResource(R.drawable.ic_eye_closed) + views.passwordReveal.contentDescription = getString(R.string.a11y_hide_password) + } else { + views.passwordReveal.setImageResource(R.drawable.ic_eye) + views.passwordReveal.contentDescription = getString(R.string.a11y_show_password) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt new file mode 100644 index 0000000000..036afda405 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 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.auth + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class ReAuthActions : VectorViewModelAction { + object StartSSOFallback : ReAuthActions() + object FallBackPageLoaded : ReAuthActions() + object FallBackPageClosed : ReAuthActions() + object TogglePassVisibility : ReAuthActions() + data class ReAuthWithPass(val password: String) : ReAuthActions() +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt new file mode 100644 index 0000000000..0c27911e0e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2021 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.auth + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Parcelable +import androidx.browser.customtabs.CustomTabsCallback +import androidx.browser.customtabs.CustomTabsClient +import androidx.browser.customtabs.CustomTabsServiceConnection +import androidx.browser.customtabs.CustomTabsSession +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.SimpleFragmentActivity +import im.vector.app.core.utils.openUrlInChromeCustomTab +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 timber.log.Timber +import javax.inject.Inject + +class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { + + @Parcelize + data class Args( + val flowType: String?, + val title: String?, + val session: String? + ) : Parcelable + + // For sso + private var customTabsServiceConnection: CustomTabsServiceConnection? = null + private var customTabsClient: CustomTabsClient? = null + private var customTabsSession: CustomTabsSession? = null + + @Inject lateinit var authenticationService: AuthenticationService + @Inject lateinit var reAuthViewModelFactory: ReAuthViewModel.Factory + + override fun create(initialState: ReAuthState) = reAuthViewModelFactory.create(initialState) + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + private val sharedViewModel: ReAuthViewModel by viewModel() + + // override fun getTitleRes() = R.string.re_authentication_activity_title + + override fun initUiAndData() { + super.initUiAndData() + + val title = intent.extras?.getString(EXTRA_REASON_TITLE) ?: getString(R.string.re_authentication_activity_title) + supportActionBar?.setTitle(title) ?: run { setTitle(title) } + +// val authArgs = intent.getParcelableExtra(MvRx.KEY_ARG) + + // For the sso flow we can for now only rely on the fallback flow, that handles all + // the UI, due to the sandbox nature of CCT (chrome custom tab) we cannot get much information + // on how the process did go :/ + // so we assume that after the user close the tab we return success and let caller retry the UIA flow :/ + + addFragment( + R.id.container, + PromptFragment::class.java + ) + + sharedViewModel.observeViewEvents { + when (it) { + is ReAuthEvents.OpenSsoURl -> { + openInCustomTab(it.url) + } + ReAuthEvents.Dismiss -> { + setResult(RESULT_CANCELED) + finish() + } + is ReAuthEvents.PasswordFinishSuccess -> { + setResult(RESULT_OK, Intent().apply { + putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.PASSWORD) + putExtra(RESULT_VALUE, it.password) + }) + finish() + } + } + } + } + + override fun onResume() { + super.onResume() + // It's the only way we have to know if sso falback flow was successful + withState(sharedViewModel) { + if (it.ssoFallbackPageWasShown) { + Timber.d("## UIA ssoFallbackPageWasShown tentative success") + setResult(RESULT_OK, Intent().apply { + putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.SSO) + }) + finish() + } + } + } + + override fun onStart() = withState(sharedViewModel) { state -> + super.onStart() + + if (state.ssoFallbackPageWasShown) { + sharedViewModel.handle(ReAuthActions.FallBackPageClosed) + return@withState + } + + val packageName = CustomTabsClient.getPackageName(this, null) + + // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device + if (packageName != null) { + customTabsServiceConnection = object : CustomTabsServiceConnection() { + override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { + Timber.d("## CustomTab onCustomTabsServiceConnected($name)") + customTabsClient = client + .also { it.warmup(0L) } + customTabsSession = customTabsClient?.newSession(object : CustomTabsCallback() { +// override fun onPostMessage(message: String, extras: Bundle?) { +// Timber.v("## CustomTab onPostMessage($message)") +// } +// +// override fun onMessageChannelReady(extras: Bundle?) { +// Timber.v("## CustomTab onMessageChannelReady()") +// } + + override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) { + Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras") + super.onNavigationEvent(navigationEvent, extras) + if (navigationEvent == NAVIGATION_FINISHED) { + sharedViewModel.handle(ReAuthActions.FallBackPageLoaded) + } + } + + override fun onRelationshipValidationResult(relation: Int, requestedOrigin: Uri, result: Boolean, extras: Bundle?) { + Timber.v("## CustomTab onRelationshipValidationResult($relation), $requestedOrigin") + super.onRelationshipValidationResult(relation, requestedOrigin, result, extras) + } + }) + } + + override fun onServiceDisconnected(name: ComponentName?) { + Timber.d("## CustomTab onServiceDisconnected($name)") + } + }.also { + CustomTabsClient.bindCustomTabsService( + this, + // Despite the API, packageName cannot be null + packageName, + it + ) + } + } + } + + override fun onStop() { + super.onStop() + customTabsServiceConnection?.let { this.unbindService(it) } + customTabsServiceConnection = null + customTabsSession = null + } + + private fun openInCustomTab(ssoUrl: String) { + openUrlInChromeCustomTab(this, customTabsSession, ssoUrl) + val channelOpened = customTabsSession?.requestPostMessageChannel(Uri.parse("https://element.io")) + Timber.d("## CustomTab channelOpened: $channelOpened") + } + + companion object { + + const val EXTRA_AUTH_TYPE = "EXTRA_AUTH_TYPE" + const val EXTRA_REASON_TITLE = "EXTRA_REASON_TITLE" + 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 + } + return Intent(context, ReAuthActivity::class.java).apply { + putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt new file mode 100644 index 0000000000..303d87a4c7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 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.auth + +import im.vector.app.core.platform.VectorViewEvents + +sealed class ReAuthEvents : VectorViewEvents { + data class OpenSsoURl(val url: String) : ReAuthEvents() + object Dismiss : ReAuthEvents() + data class PasswordFinishSuccess(val password: String) : ReAuthEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt new file mode 100644 index 0000000000..f80c9acdd2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 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.auth + +import com.airbnb.mvrx.MvRxState + +data class ReAuthState( + val title: String? = null, + val session: String? = null, + val flowType: String? = null, + val ssoFallbackPageWasShown: Boolean = false, + val passwordVisible: Boolean = false +) : MvRxState { + constructor(args: ReAuthActivity.Args) : this( + args.title, + args.session, + args.flowType + ) + + constructor() : this(null, null) +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt new file mode 100644 index 0000000000..d29bf2828d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2021 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.auth + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.platform.VectorViewModel +import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.session.Session + +class ReAuthViewModel @AssistedInject constructor( + @Assisted val initialState: ReAuthState, + private val session: Session, + private val authenticationService: AuthenticationService +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: ReAuthState): ReAuthViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: ReAuthState): ReAuthViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + override fun handle(action: ReAuthActions) = withState { state -> + when (action) { + ReAuthActions.StartSSOFallback -> { + if (state.flowType == LoginFlowTypes.SSO) { + val ssoURL = session.getUIASsoFallbackUrl(initialState.session ?: "") + _viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL)) + } + } + ReAuthActions.FallBackPageLoaded -> { + setState { copy(ssoFallbackPageWasShown = true) } + } + ReAuthActions.FallBackPageClosed -> { + // Should we do something here? + } + ReAuthActions.TogglePassVisibility -> { + setState { + copy( + passwordVisible = !state.passwordVisible + ) + } + } + is ReAuthActions.ReAuthWithPass -> { + _viewEvents.post(ReAuthEvents.PasswordFinishSuccess(action.password)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index 47e373ed0a..ebc8239765 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -20,6 +20,7 @@ import im.vector.app.R 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 @@ -33,16 +34,21 @@ 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 { @@ -101,7 +107,21 @@ class BootstrapCrossSigningTask @Inject constructor( try { awaitCallback { - crossSigningService.initializeCrossSigning(params.userPasswordAuth, it) + crossSigningService.initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, promise: Continuation) { + 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) } if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) { return BootstrapResult.SuccessCrossSigningOnly diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 45fce13ab9..5ba617b379 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -21,8 +21,8 @@ import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -33,17 +33,23 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.session.InitialSyncProgressService 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.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +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.rx.asObservable import org.matrix.android.sdk.rx.rx import timber.log.Timber +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume class HomeActivityViewModel @AssistedInject constructor( @Assisted initialState: HomeActivityViewState, @@ -122,7 +128,7 @@ class HomeActivityViewModel @AssistedInject constructor( // Schedule a check of the bootstrap when the init sync will be finished checkBootstrap = true } - is InitialSyncProgressService.Status.Idle -> { + is InitialSyncProgressService.Status.Idle -> { if (checkBootstrap) { checkBootstrap = false maybeBootstrapCrossSigning() @@ -152,11 +158,19 @@ class HomeActivityViewModel @AssistedInject constructor( // We do not use the viewModel context because we do not want to cancel this action Timber.d("Initialize cross signing") session.cryptoService().crossSigningService().initializeCrossSigning( - authParams = UserPasswordAuth( - session = null, - user = session.myUserId, - password = password - ), + object : UserInteractiveAuthInterceptor { + override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) { + promise.resume( + UserPasswordAuth( + session = flow.session, + user = session.myUserId, + password = password + ) + ) + } else promise.resumeWith(Result.failure(UnsupportedOperationException())) + } + }, callback = NoOpMatrixCallback() ) } @@ -236,11 +250,17 @@ class HomeActivityViewModel @AssistedInject constructor( // We do not use the viewModel context because we do not want to cancel this action Timber.d("Initialize cross signing") session.cryptoService().crossSigningService().initializeCrossSigning( - authParams = UserPasswordAuth( - session = null, - user = session.myUserId, - password = password - ), + object : UserInteractiveAuthInterceptor { + override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) { + UserPasswordAuth( + session = flow.session, + user = session.myUserId, + password = password + ) + } else null + } + }, callback = NoOpMatrixCallback() ) } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 727a6f765e..c12df073ee 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -311,10 +311,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } mCrossSigningStatePreference.isVisible = true - if (!vectorPreferences.developerMode()) { - // When not in developer mode, intercept click on this preference - mCrossSigningStatePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { true } - } } private val saveMegolmStartForActivityResult = registerStartForActivityResult { diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt index af6ca9f4b7..735c456ff9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt @@ -18,4 +18,9 @@ package im.vector.app.features.settings.crosssigning import im.vector.app.core.platform.VectorViewModelAction -sealed class CrossSigningSettingsAction : VectorViewModelAction +sealed class CrossSigningSettingsAction : VectorViewModelAction { + object InitializeCrossSigning: CrossSigningSettingsAction() + object SsoAuthDone: CrossSigningSettingsAction() + data class PasswordAuthDone(val password: String): CrossSigningSettingsAction() + object ReAuthCancelled: CrossSigningSettingsAction() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt index 82279a3906..6425256929 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt @@ -19,8 +19,11 @@ import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericItemWithValue +import im.vector.app.core.ui.list.genericPositiveButtonItem +import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.core.utils.DimensionConverter import me.gujun.android.span.span import javax.inject.Inject @@ -31,7 +34,9 @@ class CrossSigningSettingsController @Inject constructor( private val dimensionConverter: DimensionConverter ) : TypedEpoxyController() { - interface InteractionListener + interface InteractionListener { + fun didTapInitializeCrossSigning() + } var interactionListener: InteractionListener? = null @@ -44,6 +49,13 @@ class CrossSigningSettingsController @Inject constructor( titleIconResourceId(R.drawable.ic_shield_trusted) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete)) } + genericButtonItem { + id("Reset") + text(stringProvider.getString(R.string.reset_cross_signing)) + buttonClickAction(DebouncedClickListener({ + interactionListener?.didTapInitializeCrossSigning() + })) + } } data.xSigningKeysAreTrusted -> { genericItem { @@ -51,6 +63,13 @@ class CrossSigningSettingsController @Inject constructor( titleIconResourceId(R.drawable.ic_shield_custom) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted)) } + genericButtonItem { + id("Reset") + text(stringProvider.getString(R.string.reset_cross_signing)) + buttonClickAction(DebouncedClickListener({ + interactionListener?.didTapInitializeCrossSigning() + })) + } } data.xSigningIsEnableInAccount -> { genericItem { @@ -58,12 +77,27 @@ class CrossSigningSettingsController @Inject constructor( titleIconResourceId(R.drawable.ic_shield_black) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted)) } + genericButtonItem { + id("Reset") + text(stringProvider.getString(R.string.reset_cross_signing)) + buttonClickAction(DebouncedClickListener({ + interactionListener?.didTapInitializeCrossSigning() + })) + } } else -> { genericItem { id("not") title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled)) } + + genericPositiveButtonItem { + id("Initialize") + text(stringProvider.getString(R.string.initialize_cross_signing)) + buttonClickAction(DebouncedClickListener({ + interactionListener?.didTapInitializeCrossSigning() + })) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt index 63611efae5..4625c21cdd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt @@ -15,20 +15,26 @@ */ package im.vector.app.features.settings.crosssigning +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding +import im.vector.app.features.auth.ReAuthActivity +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import javax.inject.Inject @@ -47,19 +53,52 @@ class CrossSigningSettingsFragment @Inject constructor( private val viewModel: CrossSigningSettingsViewModel by fragmentViewModel() + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(CrossSigningSettingsAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(CrossSigningSettingsAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled) + } + } +// activityResult.data?.extras?.getString(ReAuthActivity.RESULT_TOKEN)?.let { token -> +// } + } else { + viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupRecyclerView() - viewModel.observeViewEvents { - when (it) { + viewModel.observeViewEvents { event -> + when (event) { is CrossSigningSettingsViewEvents.Failure -> { AlertDialog.Builder(requireContext()) .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(it.throwable)) + .setMessage(errorFormatter.toHumanReadable(event.throwable)) .setPositiveButton(R.string.ok, null) .show() Unit } + is CrossSigningSettingsViewEvents.RequestReAuth -> { + ReAuthActivity.newIntent(requireContext(), event.registrationFlowResponse, getString(R.string.initialize_cross_signing)).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } + is CrossSigningSettingsViewEvents.ShowModalWaitingView -> { + views.waitingView.waitingView.isVisible = true + views.waitingView.waitingStatusText.setTextOrHide(event.status) + } + CrossSigningSettingsViewEvents.HideModalWaitingView -> { + views.waitingView.waitingView.isVisible = false + } }.exhaustive } } @@ -83,4 +122,8 @@ class CrossSigningSettingsFragment @Inject constructor( controller.interactionListener = null super.onDestroyView() } + + override fun didTapInitializeCrossSigning() { + viewModel.handle(CrossSigningSettingsAction.InitializeCrossSigning) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt index b81a321f3f..8da5da4c7b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt @@ -17,10 +17,14 @@ package im.vector.app.features.settings.crosssigning import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse /** * Transient events for cross signing settings screen */ sealed class CrossSigningSettingsViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents() + data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse) : CrossSigningSettingsViewEvents() + data class ShowModalWaitingView(val status: String?) : CrossSigningSettingsViewEvents() + object HideModalWaitingView : CrossSigningSettingsViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index fdf5d611fa..06fa930ef1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -15,25 +15,45 @@ */ package im.vector.app.features.settings.crosssigning +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext 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.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.login.ReAuthHelper import io.reactivex.Observable import io.reactivex.functions.BiFunction +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.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.crypto.crosssigning.isVerified +import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +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 org.matrix.android.sdk.rx.rx +import timber.log.Timber +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume -class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState, - private val session: Session) - : VectorViewModel(initialState) { +class CrossSigningSettingsViewModel @AssistedInject constructor( + @Assisted private val initialState: CrossSigningSettingsViewState, + private val session: Session, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { init { Observable.combineLatest, Optional, Pair, Optional>>( @@ -58,15 +78,82 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat } } + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null + @AssistedFactory interface Factory { fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel } - override fun handle(action: CrossSigningSettingsAction) { - // No op for the moment - // when (action) { - // }.exhaustive + override fun handle(action: CrossSigningSettingsAction) = withState { state -> + when (action) { + CrossSigningSettingsAction.InitializeCrossSigning -> { + _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) + viewModelScope.launch(Dispatchers.IO) { + try { + awaitCallback { + session.cryptoService().crossSigningService().initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + Timber.d("## UIA : initializeCrossSigning UIA") + if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true && reAuthHelper.data != null) { + UserPasswordAuth( + session = null, + user = session.myUserId, + password = reAuthHelper.data + ).let { promise.resume(it) } + } else { + Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity") + _viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flow)) + pendingAuth = DefaultBaseAuth(session = flow.session) + uiaContinuation = promise + } + } + }, it) + } + } catch (failure: Throwable) { + handleInitializeXSigningError(failure) + } finally { + _viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView) + } + } + Unit + } + is CrossSigningSettingsAction.SsoAuthDone -> { + // we should use token based auth + // _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) + // will release the interactive auth interceptor + Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation") + if (pendingAuth != null) { + uiaContinuation?.resume(pendingAuth!!) + } else { + uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + } + Unit + } + is CrossSigningSettingsAction.PasswordAuthDone -> { + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = action.password, + user = session.myUserId + ) + ) + } + CrossSigningSettingsAction.ReAuthCancelled -> { + Timber.d("## UIA - Reauth cancelled") + _viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView) + uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation = null + pendingAuth = null + } + }.exhaustive + } + + private fun handleInitializeXSigningError(failure: Throwable) { + Timber.e(failure, "## CrossSigning - Failed to initialize cross signing") + _viewEvents.post(CrossSigningSettingsViewEvents.Failure(Exception(stringProvider.getString(R.string.failed_to_initialize_cross_signing)))) } companion object : MvRxViewModelFactory { diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt index 8a371ada68..aef27c598e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt @@ -23,5 +23,6 @@ data class CrossSigningSettingsViewState( val crossSigningInfo: MXCrossSigningInfo? = null, val xSigningIsEnableInAccount: Boolean = false, val xSigningKeysAreTrusted: Boolean = false, - val xSigningKeyCanSign: Boolean = true + val xSigningKeyCanSign: Boolean = true, +// val pendingAuthSession: String? = null ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt index 2b0991ab4e..46a476c270 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { object Refresh : DevicesAction() data class Delete(val deviceId: String) : DevicesAction() - data class Password(val password: String) : DevicesAction() +// data class Password(val password: String) : DevicesAction() data class Rename(val deviceId: String, val newName: String) : DevicesAction() data class PromptRename(val deviceId: String) : DevicesAction() @@ -30,4 +30,8 @@ sealed class DevicesAction : VectorViewModelAction { data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction() object CompleteSecurity : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() + + object SsoAuthDone: DevicesAction() + data class PasswordAuthDone(val password: String): DevicesAction() + object ReAuthCancelled: DevicesAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt index 60d7491603..5644d1c63e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt @@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices import im.vector.app.core.platform.VectorViewEvents import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo @@ -27,9 +28,12 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo */ sealed class DevicesViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : DevicesViewEvents() +// object HideLoading : DevicesViewEvents() data class Failure(val throwable: Throwable) : DevicesViewEvents() - object RequestPassword : DevicesViewEvents() +// object RequestPassword : DevicesViewEvents() + + data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse) : DevicesViewEvents() data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index eb034530ef..95b1619b21 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -27,16 +27,20 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory -import im.vector.app.core.error.SsoFlowNotSupportedYet +import dagger.assisted.AssistedInject +import im.vector.app.R import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.login.ReAuthHelper import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback +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 @@ -44,13 +48,20 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationService 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.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +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 org.matrix.android.sdk.rx.rx import timber.log.Timber import java.util.concurrent.TimeUnit +import javax.net.ssl.HttpsURLConnection +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume data class DevicesViewState( val myDeviceId: String = "", @@ -70,9 +81,14 @@ data class DeviceFullInfo( class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, - private val session: Session + private val session: Session, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider ) : VectorViewModel(initialState), VerificationService.Listener { + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null + @AssistedFactory interface Factory { fun create(initialState: DevicesViewState): DevicesViewModel @@ -87,10 +103,6 @@ class DevicesViewModel @AssistedInject constructor( } } - // temp storage when we ask for the user password - private var _currentDeviceId: String? = null - private var _currentSession: String? = null - private val refreshPublisher: PublishSubject = PublishSubject.create() init { @@ -187,15 +199,44 @@ class DevicesViewModel @AssistedInject constructor( override fun handle(action: DevicesAction) { return when (action) { - is DevicesAction.Refresh -> queryRefreshDevicesList() - is DevicesAction.Delete -> handleDelete(action) - is DevicesAction.Password -> handlePassword(action) - is DevicesAction.Rename -> handleRename(action) - is DevicesAction.PromptRename -> handlePromptRename(action) - is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action) - is DevicesAction.CompleteSecurity -> handleCompleteSecurity() + is DevicesAction.Refresh -> queryRefreshDevicesList() + is DevicesAction.Delete -> handleDelete(action) + is DevicesAction.Rename -> handleRename(action) + is DevicesAction.PromptRename -> handlePromptRename(action) + is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action) + is DevicesAction.CompleteSecurity -> handleCompleteSecurity() is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action) is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action) + is DevicesAction.SsoAuthDone -> { + // we should use token based auth + // _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) + // will release the interactive auth interceptor + Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation") + if (pendingAuth != null) { + uiaContinuation?.resume(pendingAuth!!) + } else { + uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + } + Unit + } + is DevicesAction.PasswordAuthDone -> { + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = action.password, + user = session.myUserId + ) + ) + Unit + } + DevicesAction.ReAuthCancelled -> { + Timber.d("## UIA - Reauth cancelled") +// _viewEvents.post(DevicesViewEvents.Loading) + uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation = null + pendingAuth = null + Unit + } } } @@ -285,95 +326,48 @@ class DevicesViewModel @AssistedInject constructor( ) } - session.cryptoService().deleteDevice(deviceId, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - var isPasswordRequestFound = false - - if (failure is Failure.RegistrationFlowError) { - // We only support LoginFlowTypes.PASSWORD - // Check if we can provide the user password - failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow -> - isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true - } - - if (isPasswordRequestFound) { - _currentDeviceId = deviceId - _currentSession = failure.registrationFlowResponse.session - - setState { - copy( - request = Success(Unit) - ) + viewModelScope.launch(Dispatchers.IO) { + try { + awaitCallback { + session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor { + override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + Timber.d("## UIA : deleteDevice UIA") + if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true && reAuthHelper.data != null) { + UserPasswordAuth( + session = null, + user = session.myUserId, + password = reAuthHelper.data + ).let { promise.resume(it) } + } else { + Timber.d("## UIA : deleteDevice UIA > start reauth activity") + _viewEvents.post(DevicesViewEvents.RequestReAuth(flow)) + pendingAuth = DefaultBaseAuth(session = flow.session) + uiaContinuation = promise + } } - - _viewEvents.post(DevicesViewEvents.RequestPassword) - } + }, it) } - - if (!isPasswordRequestFound) { - // LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far... - setState { - copy( - request = Fail(failure) - ) - } - - _viewEvents.post(DevicesViewEvents.Failure(SsoFlowNotSupportedYet())) - } - } - - override fun onSuccess(data: Unit) { setState { copy( - request = Success(data) + request = Success(Unit) ) } // force settings update queryRefreshDevicesList() - } - }) - } - - private fun handlePassword(action: DevicesAction.Password) { - val currentDeviceId = _currentDeviceId - if (currentDeviceId.isNullOrBlank()) { - // Abort - return - } - - setState { - copy( - request = Loading() - ) - } - - session.cryptoService().deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback { - override fun onSuccess(data: Unit) { - _currentDeviceId = null - _currentSession = null - - setState { - copy( - request = Success(data) - ) - } - // force settings update - queryRefreshDevicesList() - } - - override fun onFailure(failure: Throwable) { - _currentDeviceId = null - _currentSession = null - - // Password is maybe not good + } catch (failure: Throwable) { setState { copy( request = Fail(failure) ) } - - _viewEvents.post(DevicesViewEvents.Failure(failure)) + if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { + _viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.authentication_error)))) + } else { + _viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.matrix_error)))) + } + // ... + Timber.e(failure, "failed to delete session") } - }) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt index 1bf538d458..e4bc109d58 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -29,14 +30,16 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.dialogs.ManuallyVerifyDialog -import im.vector.app.core.dialogs.PromptPasswordDialog import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.FragmentGenericRecyclerBinding +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import javax.inject.Inject @@ -52,7 +55,7 @@ class VectorSettingsDevicesFragment @Inject constructor( // used to avoid requesting to enter the password for each deletion // Note: Sonar does not like to use password for member name. - private var mAccountPass: String = "" +// private var mAccountPass: String = "" override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding { return FragmentGenericRecyclerBinding.inflate(inflater, container, false) @@ -71,7 +74,7 @@ class VectorSettingsDevicesFragment @Inject constructor( when (it) { is DevicesViewEvents.Loading -> showLoading(it.message) is DevicesViewEvents.Failure -> showFailure(it.throwable) - is DevicesViewEvents.RequestPassword -> maybeShowDeleteDeviceWithPasswordDialog() + is DevicesViewEvents.RequestReAuth -> maybeShowDeleteDeviceWithPasswordDialog(it) is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo) is DevicesViewEvents.ShowVerifyDevice -> { VerificationBottomSheet.withArgs( @@ -93,13 +96,6 @@ class VectorSettingsDevicesFragment @Inject constructor( } } - override fun showFailure(throwable: Throwable) { - super.showFailure(throwable) - - // Password is maybe not good, for safety measure, reset it here - mAccountPass = "" - } - override fun onDestroyView() { devicesController.callback = null views.genericRecyclerView.cleanup() @@ -154,17 +150,31 @@ class VectorSettingsDevicesFragment @Inject constructor( .show() } + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(DevicesAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(DevicesAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(DevicesAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(DevicesAction.ReAuthCancelled) + } + } + /** * Show a dialog to ask for user password, or use a previously entered password. */ - private fun maybeShowDeleteDeviceWithPasswordDialog() { - if (mAccountPass.isNotEmpty()) { - viewModel.handle(DevicesAction.Password(mAccountPass)) - } else { - PromptPasswordDialog().show(requireActivity()) { password -> - mAccountPass = password - viewModel.handle(DevicesAction.Password(mAccountPass)) - } + private fun maybeShowDeleteDeviceWithPasswordDialog(reAuthReq: DevicesViewEvents.RequestReAuth) { + ReAuthActivity.newIntent(requireContext(), reAuthReq.registrationFlowResponse, getString(R.string.devices_delete_dialog_title)).let { intent -> + reAuthActivityResultLauncher.launch(intent) } } diff --git a/vector/src/main/res/layout/fragment_reauth_confirm.xml b/vector/src/main/res/layout/fragment_reauth_confirm.xml new file mode 100644 index 0000000000..b8f1a57ae3 --- /dev/null +++ b/vector/src/main/res/layout/fragment_reauth_confirm.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_positive_button.xml b/vector/src/main/res/layout/item_positive_button.xml new file mode 100644 index 0000000000..cdee239e59 --- /dev/null +++ b/vector/src/main/res/layout/item_positive_button.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 1f282115e0..5f9f801887 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2792,4 +2792,8 @@ Discard changes Matrix Link + + Re-Authentication Needed + Element requires you to enter your credentials to perform this action. + Failed to authenticate From da16ec0af36aeeb36da913f7118b09117def8b51 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 1 Feb 2021 14:31:23 +0100 Subject: [PATCH 02/18] UIA fixes + better error support --- .../auth/UserInteractiveAuthInterceptor.kt | 2 +- .../android/sdk/api/failure/Extensions.kt | 10 + .../android/sdk/api/failure/MatrixError.kt | 14 +- .../sdk/internal/auth/registration/UIAExt.kt | 58 +++++ .../internal/crypto/tasks/DeleteDeviceTask.kt | 46 +--- .../tasks/InitializeCrossSigningTask.kt | 48 +--- .../im/vector/app/core/di/FragmentModule.kt | 6 +- .../app/features/auth/PromptFragment.kt | 46 +++- .../app/features/auth/ReAuthActivity.kt | 26 +- .../vector/app/features/auth/ReAuthState.kt | 6 +- .../BootstrapAccountPasswordFragment.kt | 110 --------- .../crypto/recover/BootstrapActions.kt | 6 +- .../crypto/recover/BootstrapBottomSheet.kt | 37 ++- .../recover/BootstrapCrossSigningTask.kt | 43 +--- .../crypto/recover/BootstrapReAuthFragment.kt | 86 +++++++ .../recover/BootstrapSharedViewModel.kt | 230 +++++++++--------- .../features/crypto/recover/BootstrapStep.kt | 6 +- .../crypto/recover/BootstrapViewEvents.kt | 2 + .../features/home/HomeActivityViewModel.kt | 9 +- .../features/navigation/DefaultNavigator.kt | 9 +- .../CrossSigningSettingsFragment.kt | 5 +- .../CrossSigningSettingsViewEvents.kt | 2 +- .../CrossSigningSettingsViewModel.kt | 7 +- .../CrossSigningSettingsViewState.kt | 3 +- .../settings/devices/DevicesViewEvents.kt | 2 +- .../settings/devices/DevicesViewModel.kt | 7 +- .../devices/VectorSettingsDevicesFragment.kt | 5 +- .../signout/ServerBackupStatusViewModel.kt | 6 +- .../res/layout/fragment_bootstrap_reauth.xml | 68 ++++++ .../res/layout/fragment_reauth_confirm.xml | 20 +- 30 files changed, 524 insertions(+), 401 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt delete mode 100644 vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt create mode 100644 vector/src/main/res/layout/fragment_bootstrap_reauth.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt index 11cf2d2cfb..5c076b3f4b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt @@ -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) + fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 4711f7957d..8d8df6ff82 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt index 895be0031a..3820a442aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt @@ -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? = null, + @Json(name = "flows") + val flows: List? = null, + @Json(name = "params") + val params: JsonDict? = null ) { companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt new file mode 100644 index 0000000000..23273178e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt @@ -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 { 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) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 3c1721b06b..6e7eb438ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -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 { 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 { 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 - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt index e261828e94..5856d705a8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt @@ -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 { 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 { 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 - } - } } diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 407aa2fc73..1e257c169e 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt index 6556e6ae65..917f60dacb 100644 --- a/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt +++ b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt @@ -51,19 +51,23 @@ class PromptFragment : VectorBaseFragment() { } 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() { 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 + } } } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt index 0c27911e0e..cd7a32275d 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -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)) } } } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt index f80c9acdd2..633743dbcb 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt deleted file mode 100644 index feea484f06..0000000000 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt +++ /dev/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() { - - 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) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt index 0785290d2a..ce06fe726f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt index 149bd629e1..5cc86fdf15 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt @@ -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 + 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 { 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 { + 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 diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index ebc8239765..d1a1237463 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -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 { - crossSigningService.initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, promise: Continuation) { - 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) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt new file mode 100644 index 0000000000..7080cdd9c3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt @@ -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() { + + 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 + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index 3a6f57198e..d67c9afd42 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -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? = 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) { + 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) diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt index 222a5d78c6..09f0e90d5d 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt index 10a092ccbb..f4ec9d68e4 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 5ba617b379..e19205560c 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -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) { - if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) { + override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { + 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) { - if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) { + override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { + if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD) { UserPasswordAuth( session = flow.session, user = session.myUserId, diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 0a6197e424..fded8602c4 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -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)) } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt index 4625c21cdd..80e44174ff 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt index 8da5da4c7b..89970b130a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 06fa930ef1..9a1150046d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -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 { session.cryptoService().crossSigningService().initializeCrossSigning( object : UserInteractiveAuthInterceptor { - override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { 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 } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt index aef27c598e..8a371ada68 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt index 5644d1c63e..fd50fe6f16 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index 95b1619b21..db29ced873 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -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 { session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor { - override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { 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 } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt index e4bc109d58..d1b488fadf 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt index a73facc009..1c3ad7563c 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt @@ -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()) diff --git a/vector/src/main/res/layout/fragment_bootstrap_reauth.xml b/vector/src/main/res/layout/fragment_bootstrap_reauth.xml new file mode 100644 index 0000000000..1bc6725c64 --- /dev/null +++ b/vector/src/main/res/layout/fragment_bootstrap_reauth.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_reauth_confirm.xml b/vector/src/main/res/layout/fragment_reauth_confirm.xml index b8f1a57ae3..e23c7c9249 100644 --- a/vector/src/main/res/layout/fragment_reauth_confirm.xml +++ b/vector/src/main/res/layout/fragment_reauth_confirm.xml @@ -69,14 +69,28 @@ + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/genericErrorText" /> \ No newline at end of file From 9c7df258622fdc653e20687ebbae088150cb6854 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 1 Feb 2021 14:52:11 +0100 Subject: [PATCH 03/18] Relax rule for e2e by default --- .../internal/session/room/create/CreateRoomBodyBuilder.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index fb840b4eb3..48b578a519 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -143,9 +143,11 @@ internal class CreateRoomBodyBuilder @Inject constructor( } private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean { - return (params.enableEncryptionIfInvitedUsersSupportIt - && crossSigningService.isCrossSigningVerified() - && params.invite3pids.isEmpty()) + return params.enableEncryptionIfInvitedUsersSupportIt + // Parity with web, enable if users have encryption ready devices + // for now remove checks on cross signing and 3pid invites + // && crossSigningService.isCrossSigningVerified() + // && params.invite3pids.isEmpty()) && params.invitedUserIds.isNotEmpty() && params.invitedUserIds.let { userIds -> val keys = deviceListManager.downloadKeys(userIds, forceDownload = false) From 5b8215a356c190739db01044af585dc31071732d Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 1 Feb 2021 16:36:38 +0100 Subject: [PATCH 04/18] Support SSO provider brand + UI fixes --- .../sdk/api/auth/data/SsoIdentityProvider.kt | 23 ++++++--- .../internal/auth/data/LoginFlowResponse.kt | 10 +++- .../features/login/SocialLoginButtonsView.kt | 30 +++++++----- .../main/res/drawable/ic_social_gitlab.xml | 48 +++++++++++++++++++ vector/src/main/res/layout/fragment_login.xml | 1 - ...fragment_login_signup_signin_selection.xml | 1 - vector/src/main/res/values/attrs.xml | 1 + .../main/res/values/styles_social_login.xml | 26 ++++++++-- vector/src/main/res/values/theme_dark.xml | 1 + vector/src/main/res/values/theme_light.xml | 1 + 10 files changed, 115 insertions(+), 27 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_social_gitlab.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt index 6759c59237..cfaf74ce24 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt @@ -38,15 +38,24 @@ data class SsoIdentityProvider( * If present then it must be an HTTPS URL to an image resource. * This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily. */ - @Json(name = "icon") val iconUrl: String? + @Json(name = "icon") val iconUrl: String?, + + /** + * The `brand` field is **optional**. It allows the client to style the login + * button to suit a particular brand. It should be a string matching the + * "Common namespaced identifier grammar" as defined in + * [MSC2758](https://github.com/matrix-org/matrix-doc/pull/2758). + */ + @Json(name = "brand") val brand: String? + ) : Parcelable { companion object { - // Not really defined by the spec, but we may define some ids here - const val ID_GOOGLE = "google" - const val ID_GITHUB = "github" - const val ID_APPLE = "apple" - const val ID_FACEBOOK = "facebook" - const val ID_TWITTER = "twitter" + const val BRAND_GOOGLE = "org.matrix.google" + const val BRAND_GITHUB = "org.matrix.github" + const val BRAND_APPLE = "org.matrix.apple" + const val BRAND_FACEBOOK = "org.matrix.facebook" + const val BRAND_TWITTER = "org.matrix.twitter" + const val BRAND_GITLAB = "org.matrix.gitlab" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index 2b26115f30..112f7a1078 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -43,5 +43,11 @@ internal data class LoginFlow( * See MSC #2858 */ @Json(name = "org.matrix.msc2858.identity_providers") - val ssoIdentityProvider: List? -) + val _devSsoIdentityProvider: List? = null, + + @Json(name = "identity_providers") + val _ssoIdentityProvider: List? = null, + +) { + val ssoIdentityProvider = _ssoIdentityProvider ?: _devSsoIdentityProvider +} diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt index 9290479a7a..cffe44d0f0 100644 --- a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -83,25 +83,28 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: ssoIdentityProviders?.forEach { identityProvider -> // Use some heuristic to render buttons according to branding guidelines val button: MaterialButton = cachedViews[identityProvider.id] - ?: when (identityProvider.id) { - SsoIdentityProvider.ID_GOOGLE -> { + ?: when (identityProvider.brand) { + SsoIdentityProvider.BRAND_GOOGLE -> { MaterialButton(context, null, R.attr.vctr_social_login_button_google_style) } - SsoIdentityProvider.ID_GITHUB -> { + SsoIdentityProvider.BRAND_GITHUB -> { MaterialButton(context, null, R.attr.vctr_social_login_button_github_style) } - SsoIdentityProvider.ID_APPLE -> { + SsoIdentityProvider.BRAND_APPLE -> { MaterialButton(context, null, R.attr.vctr_social_login_button_apple_style) } - SsoIdentityProvider.ID_FACEBOOK -> { + SsoIdentityProvider.BRAND_FACEBOOK -> { MaterialButton(context, null, R.attr.vctr_social_login_button_facebook_style) } - SsoIdentityProvider.ID_TWITTER -> { + SsoIdentityProvider.BRAND_TWITTER -> { MaterialButton(context, null, R.attr.vctr_social_login_button_twitter_style) } + SsoIdentityProvider.BRAND_GITLAB -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_gitlab_style) + } else -> { // TODO Use iconUrl - MaterialButton(context, null, R.attr.materialButtonStyle).apply { + MaterialButton(context, null, R.attr.materialButtonOutlinedStyle).apply { transformationMethod = null textAlignment = View.TEXT_ALIGNMENT_CENTER } @@ -131,12 +134,13 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: clipChildren = false if (isInEditMode) { ssoIdentityProviders = listOf( - SsoIdentityProvider(SsoIdentityProvider.ID_GOOGLE, "Google", null), - SsoIdentityProvider(SsoIdentityProvider.ID_FACEBOOK, "Facebook", null), - SsoIdentityProvider(SsoIdentityProvider.ID_APPLE, "Apple", null), - SsoIdentityProvider(SsoIdentityProvider.ID_GITHUB, "GitHub", null), - SsoIdentityProvider(SsoIdentityProvider.ID_TWITTER, "Twitter", null), - SsoIdentityProvider("Custom_pro", "SSO", null) + SsoIdentityProvider("Google", "Google", null, SsoIdentityProvider.BRAND_GOOGLE), + SsoIdentityProvider("Facebook", "Facebook",null, SsoIdentityProvider.BRAND_FACEBOOK), + SsoIdentityProvider("Apple", "Apple",null, SsoIdentityProvider.BRAND_APPLE), + SsoIdentityProvider("GitHub", "GitHub",null, SsoIdentityProvider.BRAND_GITHUB), + SsoIdentityProvider("Twitter", "Twitter", null, SsoIdentityProvider.BRAND_TWITTER), + SsoIdentityProvider("Gitlab", "Gitlab", null, SsoIdentityProvider.BRAND_GITLAB), + SsoIdentityProvider("Custom_pro", "SSO", null, null) ) } val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SocialLoginButtonsView, 0, 0) diff --git a/vector/src/main/res/drawable/ic_social_gitlab.xml b/vector/src/main/res/drawable/ic_social_gitlab.xml new file mode 100644 index 0000000000..9399f6448a --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_gitlab.xml @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index da41878365..1740d26b3b 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -143,7 +143,6 @@ android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical" - android:padding="8dp" android:visibility="gone" tools:visibility="visible"> diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml index 097e5c1f52..56d4e37f1e 100644 --- a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml +++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml @@ -88,7 +88,6 @@ android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical" - android:padding="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 51d140ebcf..41b8080fc0 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -46,6 +46,7 @@ + diff --git a/vector/src/main/res/values/styles_social_login.xml b/vector/src/main/res/values/styles_social_login.xml index 796965cee1..3ad7fbb989 100644 --- a/vector/src/main/res/values/styles_social_login.xml +++ b/vector/src/main/res/values/styles_social_login.xml @@ -1,17 +1,20 @@ - + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 86fbb57608..c31fc8240b 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -200,6 +200,7 @@ @style/WidgetButtonSocialLogin.Facebook.Dark @style/WidgetButtonSocialLogin.Twitter.Dark @style/WidgetButtonSocialLogin.Apple.Dark + @style/WidgetButtonSocialLogin.Gitlab.Dark @android:color/transparent diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index f174bcf758..56faaeb325 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -202,6 +202,7 @@ @style/WidgetButtonSocialLogin.Facebook.Light @style/WidgetButtonSocialLogin.Twitter.Light @style/WidgetButtonSocialLogin.Apple.Light + @style/WidgetButtonSocialLogin.Gitlab.Light @color/black_alpha From 76b425ee8a75d5b14debdd039181345967acab50 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 1 Feb 2021 16:38:20 +0100 Subject: [PATCH 05/18] Cleaning --- .../android/sdk/internal/auth/data/LoginFlowResponse.kt | 2 +- .../sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt | 4 ++-- .../im/vector/app/features/login/SocialLoginButtonsView.kt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index 112f7a1078..703858fb61 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -46,7 +46,7 @@ internal data class LoginFlow( val _devSsoIdentityProvider: List? = null, @Json(name = "identity_providers") - val _ssoIdentityProvider: List? = null, + val _ssoIdentityProvider: List? = null ) { val ssoIdentityProvider = _ssoIdentityProvider ?: _devSsoIdentityProvider diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt index 5856d705a8..ef31130f55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt @@ -125,8 +125,8 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor( try { uploadSigningKeysTask.execute(uploadSigningKeysParams) } catch (failure: Throwable) { - if (params.interactiveAuthInterceptor == null || - !handleUIA(failure, params.interactiveAuthInterceptor) { authUpdate -> + if (params.interactiveAuthInterceptor == null + || !handleUIA(failure, params.interactiveAuthInterceptor) { authUpdate -> uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate)) }) { Timber.d("## UIA: propagate failure") diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt index cffe44d0f0..4dc688ad22 100644 --- a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -135,9 +135,9 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: if (isInEditMode) { ssoIdentityProviders = listOf( SsoIdentityProvider("Google", "Google", null, SsoIdentityProvider.BRAND_GOOGLE), - SsoIdentityProvider("Facebook", "Facebook",null, SsoIdentityProvider.BRAND_FACEBOOK), - SsoIdentityProvider("Apple", "Apple",null, SsoIdentityProvider.BRAND_APPLE), - SsoIdentityProvider("GitHub", "GitHub",null, SsoIdentityProvider.BRAND_GITHUB), + SsoIdentityProvider("Facebook", "Facebook", null, SsoIdentityProvider.BRAND_FACEBOOK), + SsoIdentityProvider("Apple", "Apple", null, SsoIdentityProvider.BRAND_APPLE), + SsoIdentityProvider("GitHub", "GitHub", null, SsoIdentityProvider.BRAND_GITHUB), SsoIdentityProvider("Twitter", "Twitter", null, SsoIdentityProvider.BRAND_TWITTER), SsoIdentityProvider("Gitlab", "Gitlab", null, SsoIdentityProvider.BRAND_GITLAB), SsoIdentityProvider("Custom_pro", "SSO", null, null) From 6c9b16088fbd2066d54d69b7ddcc7ce9fc9a99e3 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Feb 2021 08:59:28 +0100 Subject: [PATCH 06/18] Secure secrets passed to intent --- .../im/vector/app/features/auth/ReAuthActivity.kt | 14 ++++++++++---- .../im/vector/app/features/auth/ReAuthEvents.kt | 2 +- .../im/vector/app/features/auth/ReAuthState.kt | 6 ++++-- .../im/vector/app/features/auth/ReAuthViewModel.kt | 13 +++++++++---- .../crypto/recover/BootstrapSharedViewModel.kt | 5 ++++- .../crosssigning/CrossSigningSettingsViewModel.kt | 5 ++++- .../features/settings/devices/DevicesViewModel.kt | 5 ++++- 7 files changed, 36 insertions(+), 14 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt index cd7a32275d..a95c2b73cc 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -49,7 +49,8 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { val flowType: String?, val title: String?, val session: String?, - val lastErrorCode: String? + val lastErrorCode: String?, + val resultKeyStoreAlias: String ) : Parcelable // For sso @@ -101,7 +102,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { is ReAuthEvents.PasswordFinishSuccess -> { setResult(RESULT_OK, Intent().apply { putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.PASSWORD) - putExtra(RESULT_VALUE, it.password) + putExtra(RESULT_VALUE, it.passwordSafeForIntent) }) finish() } @@ -197,8 +198,13 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { const val EXTRA_REASON_TITLE = "EXTRA_REASON_TITLE" const val RESULT_FLOW_TYPE = "RESULT_FLOW_TYPE" const val RESULT_VALUE = "RESULT_VALUE" + const val DEFAULT_RESULT_KEYSTORE_ALIAS = "ReAuthActivity" - fun newIntent(context: Context, fromError: RegistrationFlowResponse, lastErrorCode: String?, reasonTitle: String?): Intent { + fun newIntent(context: Context, + fromError: RegistrationFlowResponse, + lastErrorCode: String?, + reasonTitle: String?, + resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent { val authType = when (fromError.nextUncompletedStage()) { LoginFlowTypes.PASSWORD -> { LoginFlowTypes.PASSWORD @@ -212,7 +218,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { } } return Intent(context, ReAuthActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode)) + putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode, resultKeyStoreAlias)) } } } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt index 303d87a4c7..8cf9be6fb1 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt @@ -21,5 +21,5 @@ import im.vector.app.core.platform.VectorViewEvents sealed class ReAuthEvents : VectorViewEvents { data class OpenSsoURl(val url: String) : ReAuthEvents() object Dismiss : ReAuthEvents() - data class PasswordFinishSuccess(val password: String) : ReAuthEvents() + data class PasswordFinishSuccess(val passwordSafeForIntent: String) : ReAuthEvents() } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt index 633743dbcb..540a08405c 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt @@ -24,13 +24,15 @@ data class ReAuthState( val flowType: String? = null, val ssoFallbackPageWasShown: Boolean = false, val passwordVisible: Boolean = false, - val lastErrorCode: String? = null + val lastErrorCode: String? = null, + val resultKeyStoreAlias: String = "" ) : MvRxState { constructor(args: ReAuthActivity.Args) : this( args.title, args.session, args.flowType, - lastErrorCode = args.lastErrorCode + lastErrorCode = args.lastErrorCode, + resultKeyStoreAlias = args.resultKeyStoreAlias ) constructor() : this(null, null) diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt index d29bf2828d..4b477990c0 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt @@ -24,14 +24,14 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.platform.VectorViewModel -import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding +import java.io.ByteArrayOutputStream class ReAuthViewModel @AssistedInject constructor( @Assisted val initialState: ReAuthState, - private val session: Session, - private val authenticationService: AuthenticationService + private val session: Session ) : VectorViewModel(initialState) { @AssistedFactory @@ -72,7 +72,12 @@ class ReAuthViewModel @AssistedInject constructor( } } is ReAuthActions.ReAuthWithPass -> { - _viewEvents.post(ReAuthEvents.PasswordFinishSuccess(action.password)) + val safeForIntentCypher = ByteArrayOutputStream().also { + it.use { + session.securelyStoreObject(action.password, initialState.resultKeyStoreAlias, it) + } + }.toByteArray().toBase64NoPadding() + _viewEvents.post(ReAuthEvents.PasswordFinishSuccess(safeForIntentCypher)) } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index d67c9afd42..9282dc3e3e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -34,6 +34,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -44,6 +45,7 @@ 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.crosssigning.fromBase64 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 @@ -269,10 +271,11 @@ class BootstrapSharedViewModel @AssistedInject constructor( uiaContinuation?.resume(DefaultBaseAuth(session = pendingAuth?.session ?: "")) } is BootstrapActions.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) uiaContinuation?.resume( UserPasswordAuth( session = pendingAuth?.session, - password = action.password, + password = decryptedPass, user = session.myUserId ) ) diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 9a1150046d..d29ecefff1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -26,6 +26,7 @@ import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper import io.reactivex.Observable import io.reactivex.functions.BiFunction @@ -38,6 +39,7 @@ 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.fromBase64 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 @@ -134,10 +136,11 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( Unit } is CrossSigningSettingsAction.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) uiaContinuation?.resume( UserPasswordAuth( session = pendingAuth?.session, - password = action.password, + password = decryptedPass, user = session.myUserId ) ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index db29ced873..610c89706e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -32,6 +32,7 @@ import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper import io.reactivex.Observable import io.reactivex.functions.BiFunction @@ -51,6 +52,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat 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.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo @@ -221,10 +223,11 @@ class DevicesViewModel @AssistedInject constructor( Unit } is DevicesAction.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) uiaContinuation?.resume( UserPasswordAuth( session = pendingAuth?.session, - password = action.password, + password = decryptedPass, user = session.myUserId ) ) From 0bc203e0d5306beefb6e8098c2b0a46447c9e56f Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Feb 2021 09:02:10 +0100 Subject: [PATCH 07/18] Setup cross signing after initial sync if not yet done Use grace period if available, if not fail silently --- .../features/home/HomeActivityViewModel.kt | 131 +++++++----------- 1 file changed, 52 insertions(+), 79 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index e19205560c..f8e61b5e04 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -29,12 +29,12 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.session.InitialSyncProgressService import org.matrix.android.sdk.api.session.room.model.Membership @@ -46,6 +46,7 @@ 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 import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.asObservable import org.matrix.android.sdk.rx.rx import timber.log.Timber @@ -81,7 +82,6 @@ class HomeActivityViewModel @AssistedInject constructor( init { cleanupFiles() observeInitialSync() - mayBeInitializeCrossSigning() checkSessionPushIsOn() observeCrossSigningReset() } @@ -132,7 +132,7 @@ class HomeActivityViewModel @AssistedInject constructor( is InitialSyncProgressService.Status.Idle -> { if (checkBootstrap) { checkBootstrap = false - maybeBootstrapCrossSigning() + maybeBootstrapCrossSigningAfterInitialSync() } } } @@ -146,37 +146,6 @@ class HomeActivityViewModel @AssistedInject constructor( .disposeOnClear() } - private fun mayBeInitializeCrossSigning() { - if (args.accountCreation) { - val password = reAuthHelper.data ?: return Unit.also { - Timber.w("No password to init cross signing") - } - - val session = activeSessionHolder.getSafeActiveSession() ?: return Unit.also { - Timber.w("No session to init cross signing") - } - - // We do not use the viewModel context because we do not want to cancel this action - Timber.d("Initialize cross signing") - session.cryptoService().crossSigningService().initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { - if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD) { - promise.resume( - UserPasswordAuth( - session = flow.session, - user = session.myUserId, - password = password - ) - ) - } else promise.resumeWith(Result.failure(UnsupportedOperationException())) - } - }, - callback = NoOpMatrixCallback() - ) - } - } - /** * After migration from riot to element some users reported that their * push setting for the session was set to off @@ -212,62 +181,66 @@ class HomeActivityViewModel @AssistedInject constructor( } } - private fun maybeBootstrapCrossSigning() { - // In case of account creation, it is already done before - if (args.accountCreation) return + private fun maybeBootstrapCrossSigningAfterInitialSync() { + // We do not use the viewModel context because we do not want to tie this action to activity view model + GlobalScope.launch(Dispatchers.IO) { + val session = activeSessionHolder.getSafeActiveSession() ?: return@launch - val session = activeSessionHolder.getSafeActiveSession() ?: return + tryOrNull("## MaybeBootstrapCrossSigning: Failed to download keys") { + awaitCallback> { + session.cryptoService().downloadKeys(listOf(session.myUserId), true, it) + } + } - // Ensure keys of the user are downloaded - session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback> { - override fun onSuccess(data: MXUsersDevicesMap) { - // Is there already cross signing keys here? - val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys() - if (mxCrossSigningInfo != null) { - // Cross-signing is already set up for this user, is it trusted? - if (!mxCrossSigningInfo.isTrusted()) { - // New session - _viewEvents.post( - HomeActivityViewEvents.OnNewSession( - session.getUser(session.myUserId)?.toMatrixItem(), - // If it's an old unverified, we should send requests - // instead of waiting for an incoming one - reAuthHelper.data != null - ) - ) - } - } else { - // Initialize cross-signing - val password = reAuthHelper.data - - if (password == null) { - // Check this is not an SSO account - if (session.getHomeServerCapabilities().canChangePassword) { - // Ask password to the user: Upgrade security - _viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem())) - } - // Else (SSO) just ignore for the moment - } else { - // We do not use the viewModel context because we do not want to cancel this action - Timber.d("Initialize cross signing") + // From there we are up to date with server + // Is there already cross signing keys here? + val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys() + if (mxCrossSigningInfo != null) { + // Cross-signing is already set up for this user, is it trusted? + if (!mxCrossSigningInfo.isTrusted()) { + // New session + _viewEvents.post( + HomeActivityViewEvents.OnNewSession( + session.getUser(session.myUserId)?.toMatrixItem(), + // If it's an old unverified, we should send requests + // instead of waiting for an incoming one + reAuthHelper.data != null + ) + ) + } + } else { + // Try to initialize cross signing in background if possible + Timber.d("Initialize cross signing...") + awaitCallback { + try { session.cryptoService().crossSigningService().initializeCrossSigning( object : UserInteractiveAuthInterceptor { override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { - if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD) { - UserPasswordAuth( - session = flow.session, - user = session.myUserId, - password = password + // We missed server grace period or it's not setup, see if we remember locally password + if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD + && errorCode == null + && reAuthHelper.data != null) { + promise.resume( + UserPasswordAuth( + session = flow.session, + user = session.myUserId, + password = reAuthHelper.data + ) ) - } else null + } else { + promise.resumeWith(Result.failure(Exception("Cannot silently initialize cross signing, UIA missing"))) + } } }, - callback = NoOpMatrixCallback() + callback = it ) + Timber.d("Initialize cross signing SUCCESS") + } catch (failure: Throwable) { + Timber.e(failure, "Failed to initialize cross signing") } } } - }) + } } override fun handle(action: HomeActivityViewActions) { From 8129cd0cd352a4908f4e49558e596888494e7a2d Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Feb 2021 09:32:04 +0100 Subject: [PATCH 08/18] Cleaning + changelog --- CHANGES.md | 6 ++++-- .../android/sdk/api/auth/UserInteractiveAuthInterceptor.kt | 2 +- vector/src/main/AndroidManifest.xml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 537e3aef7b..3d6c82a776 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,10 +2,12 @@ Changes in Element 1.0.15 (2020-XX-XX) =================================================== Features ✨: - - + - Social Login support Improvements 🙌: - - + - SSO support for cross signing (#1062) + - Deactivate account when logged in with SSO (#1264) + - SSO UIA doesn't work (#2754) Bugfix 🐛: - Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt index 5c076b3f4b..e7f27f458c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt @@ -41,7 +41,7 @@ interface UserInteractiveAuthInterceptor { /** * When the API needs additional auth, this will be called. * Implementation should check the flows from flow response and act accordingly. - * Updated auth should be provider using promise.resume, this allow implementation to perform + * Updated auth should be provided 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, errCode: String?, promise: Continuation) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 1028f200af..43ad1d3182 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -248,7 +248,7 @@ android:exported="false"> From 2a3962265b6c1b05b1397ba598ab1725af93afa7 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Feb 2021 12:30:46 +0100 Subject: [PATCH 09/18] SSO UIA for deactivate account --- .../android/sdk/api/failure/Extensions.kt | 6 ++ .../sdk/api/session/account/AccountService.kt | 4 +- .../account/DeactivateAccountParams.kt | 14 ++-- .../session/account/DeactivateAccountTask.kt | 25 +++++-- .../session/account/DefaultAccountService.kt | 5 +- .../app/features/auth/ReAuthActivity.kt | 2 +- .../app/features/auth/ReAuthViewModel.kt | 1 + .../deactivation/DeactivateAccountAction.kt | 28 +++++++ .../deactivation/DeactivateAccountFragment.kt | 73 ++++++++++--------- .../DeactivateAccountViewEvents.kt | 5 +- .../DeactivateAccountViewModel.kt | 68 ++++++++++++----- .../CrossSigningSettingsViewModel.kt | 6 +- .../layout/fragment_deactivate_account.xml | 65 +---------------- 13 files changed, 164 insertions(+), 138 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 8d8df6ff82..a0983161af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -43,6 +43,12 @@ fun Throwable.isInvalidPassword(): Boolean { && error.message == "Invalid password" } +fun Throwable.isInvalidUIAAuth(): Boolean { + return this is Failure.ServerError + && error.code == MatrixError.M_FORBIDDEN + && error.flows != null +} + /** * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt index 8915202f35..eb327dfd56 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.account +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor + /** * This interface defines methods to manage the account. It's implemented at the session level. */ @@ -43,5 +45,5 @@ interface AccountService { * @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see * an incomplete view of conversations */ - suspend fun deactivateAccount(password: String, eraseAllData: Boolean) + suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt index 6c2e8b4a4e..a99a589ec4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt @@ -18,21 +18,21 @@ package org.matrix.android.sdk.internal.session.account import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth @JsonClass(generateAdapter = true) internal data class DeactivateAccountParams( - @Json(name = "auth") - val auth: UserPasswordAuth? = null, - // Set to true to erase all data of the account @Json(name = "erase") - val erase: Boolean + val erase: Boolean, + + @Json(name = "auth") + val auth: Map? = null ) { companion object { - fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams { + fun create(auth: UIABaseAuth?, erase: Boolean): DeactivateAccountParams { return DeactivateAccountParams( - auth = UserPasswordAuth(user = userId, password = password), + auth = auth?.asMap(), erase = erase ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt index 9fb1cbb7d7..148afa7c90 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.internal.session.account +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.internal.auth.registration.handleUIA +import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest @@ -27,8 +30,9 @@ import javax.inject.Inject internal interface DeactivateAccountTask : Task { data class Params( - val password: String, - val eraseAllData: Boolean + val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, + val eraseAllData: Boolean, + val userAuthParam: UIABaseAuth? = null ) } @@ -41,12 +45,21 @@ internal class DefaultDeactivateAccountTask @Inject constructor( ) : DeactivateAccountTask { override suspend fun execute(params: DeactivateAccountTask.Params) { - val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData) + val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData) - executeRequest(globalErrorReceiver) { - apiCall = accountAPI.deactivate(deactivateAccountParams) + try { + executeRequest(globalErrorReceiver) { + apiCall = accountAPI.deactivate(deactivateAccountParams) + } + } catch (throwable: Throwable) { + if (!handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth -> + execute(params.copy(userAuthParam = auth)) + } + ) { + Timber.d("## UIA: propagate failure") + throw throwable + } } - // Logout from identity server if any, ignoring errors runCatching { identityDisconnectTask.execute(Unit) } .onFailure { Timber.w(it, "Unable to disconnect identity server") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt index 1165d2116b..25b67159a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.account +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.account.AccountService import javax.inject.Inject @@ -26,7 +27,7 @@ internal class DefaultAccountService @Inject constructor(private val changePassw changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword)) } - override suspend fun deactivateAccount(password: String, eraseAllData: Boolean) { - deactivateAccountTask.execute(DeactivateAccountTask.Params(password, eraseAllData)) + override suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) { + deactivateAccountTask.execute(DeactivateAccountTask.Params(userInteractiveAuthInterceptor, eraseAllData)) } } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt index a95c2b73cc..b44639750e 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -154,7 +154,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras") super.onNavigationEvent(navigationEvent, extras) if (navigationEvent == NAVIGATION_FINISHED) { - sharedViewModel.handle(ReAuthActions.FallBackPageLoaded) +// sharedViewModel.handle(ReAuthActions.FallBackPageLoaded) } } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt index 4b477990c0..a946a91ced 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt @@ -54,6 +54,7 @@ class ReAuthViewModel @AssistedInject constructor( when (action) { ReAuthActions.StartSSOFallback -> { if (state.flowType == LoginFlowTypes.SSO) { + setState { copy(ssoFallbackPageWasShown = true) } val ssoURL = session.getUIASsoFallbackUrl(initialState.session ?: "") _viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL)) } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt new file mode 100644 index 0000000000..c3fa844805 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 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.settings.account.deactivation + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class DeactivateAccountAction : VectorViewModelAction { + object TogglePassword : DeactivateAccountAction() + data class DeactivateAccount(val eraseAllData: Boolean) : DeactivateAccountAction() + + object SsoAuthDone: DeactivateAccountAction() + data class PasswordAuthDone(val password: String): DeactivateAccountAction() + object ReAuthCancelled: DeactivateAccountAction() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt index 3d128eb755..2cc80bfa23 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.account.deactivation +import android.app.Activity import android.content.Context import android.os.Bundle import android.view.LayoutInflater @@ -23,16 +24,16 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.extensions.showPassword +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentDeactivateAccountBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.settings.VectorSettingsActivity +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import javax.inject.Inject @@ -46,6 +47,25 @@ class DeactivateAccountFragment @Inject constructor( return FragmentDeactivateAccountBinding.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(DeactivateAccountAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(DeactivateAccountAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(DeactivateAccountAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(DeactivateAccountAction.ReAuthCancelled) + } + } + override fun onResume() { super.onResume() (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title) @@ -66,59 +86,46 @@ class DeactivateAccountFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupUi() setupViewListeners() observeViewEvents() } - private fun setupUi() { - views.deactivateAccountPassword.textChanges() - .subscribe { - views.deactivateAccountPasswordTil.error = null - views.deactivateAccountSubmit.isEnabled = it.isNotEmpty() - } - .disposeOnDestroyView() - } - private fun setupViewListeners() { - views.deactivateAccountPasswordReveal.setOnClickListener { - viewModel.handle(DeactivateAccountAction.TogglePassword) - } - views.deactivateAccountSubmit.debouncedClicks { viewModel.handle(DeactivateAccountAction.DeactivateAccount( - views.deactivateAccountPassword.text.toString(), - views.deactivateAccountEraseCheckbox.isChecked)) + views.deactivateAccountEraseCheckbox.isChecked) + ) } } private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { - is DeactivateAccountViewEvents.Loading -> { + is DeactivateAccountViewEvents.Loading -> { settingsActivity?.ignoreInvalidTokenError = true showLoadingDialog(it.message) } - DeactivateAccountViewEvents.EmptyPassword -> { + DeactivateAccountViewEvents.InvalidAuth -> { + dismissLoadingDialog() settingsActivity?.ignoreInvalidTokenError = false - views.deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password) } - DeactivateAccountViewEvents.InvalidPassword -> { - settingsActivity?.ignoreInvalidTokenError = false - views.deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password) - } - is DeactivateAccountViewEvents.OtherFailure -> { + is DeactivateAccountViewEvents.OtherFailure -> { settingsActivity?.ignoreInvalidTokenError = false + dismissLoadingDialog() displayErrorDialog(it.throwable) } - DeactivateAccountViewEvents.Done -> + DeactivateAccountViewEvents.Done -> { MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true, isAccountDeactivated = true)) + } + is DeactivateAccountViewEvents.RequestReAuth -> { + ReAuthActivity.newIntent(requireContext(), + it.registrationFlowResponse, + it.lastErrorCode, + getString(R.string.deactivate_account_title)).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } }.exhaustive } } - - override fun invalidate() = withState(viewModel) { state -> - views.deactivateAccountPassword.showPassword(state.passwordShown) - views.deactivateAccountPasswordReveal.setImageResource(if (state.passwordShown) R.drawable.ic_eye_closed else R.drawable.ic_eye) - } } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt index 46acb4aee4..05200c3aa3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt @@ -17,14 +17,15 @@ package im.vector.app.features.settings.account.deactivation import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse /** * Transient events for deactivate account settings screen */ sealed class DeactivateAccountViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents() - object EmptyPassword : DeactivateAccountViewEvents() - object InvalidPassword : DeactivateAccountViewEvents() + object InvalidAuth : DeactivateAccountViewEvents() data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents() object Done : DeactivateAccountViewEvents() + data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DeactivateAccountViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt index 6a7084fb81..dc5415a6bb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt @@ -21,25 +21,28 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.auth.ReAuthActivity import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.failure.isInvalidPassword +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.failure.isInvalidUIAAuth import org.matrix.android.sdk.api.session.Session -import java.lang.Exception +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +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 timber.log.Timber +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume data class DeactivateAccountViewState( val passwordShown: Boolean = false ) : MvRxState -sealed class DeactivateAccountAction : VectorViewModelAction { - object TogglePassword : DeactivateAccountAction() - data class DeactivateAccount(val password: String, val eraseAllData: Boolean) : DeactivateAccountAction() -} - class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState, private val session: Session) : VectorViewModel(initialState) { @@ -49,10 +52,37 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel } + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null + override fun handle(action: DeactivateAccountAction) { when (action) { - DeactivateAccountAction.TogglePassword -> handleTogglePassword() + DeactivateAccountAction.TogglePassword -> handleTogglePassword() is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action) + DeactivateAccountAction.SsoAuthDone -> { + Timber.d("## UIA - FallBack success") + if (pendingAuth != null) { + uiaContinuation?.resume(pendingAuth!!) + } else { + uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + } + } + is DeactivateAccountAction.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = decryptedPass, + user = session.myUserId + ) + ) + } + DeactivateAccountAction.ReAuthCancelled -> { + Timber.d("## UIA - Reauth cancelled") + uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation = null + pendingAuth = null + } }.exhaustive } @@ -63,20 +93,22 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v } private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) { - if (action.password.isEmpty()) { - _viewEvents.post(DeactivateAccountViewEvents.EmptyPassword) - return - } - _viewEvents.post(DeactivateAccountViewEvents.Loading()) viewModelScope.launch { val event = try { - session.deactivateAccount(action.password, action.eraseAllData) + session.deactivateAccount( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + _viewEvents.post(DeactivateAccountViewEvents.RequestReAuth(flowResponse, errCode)) + pendingAuth = DefaultBaseAuth(session = flowResponse.session) + uiaContinuation = promise + } + }, action.eraseAllData) DeactivateAccountViewEvents.Done } catch (failure: Exception) { - if (failure.isInvalidPassword()) { - DeactivateAccountViewEvents.InvalidPassword + if (failure.isInvalidUIAAuth()) { + DeactivateAccountViewEvents.InvalidAuth } else { DeactivateAccountViewEvents.OtherFailure(failure) } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index d29ecefff1..ceb216ca42 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -124,16 +124,12 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( Unit } is CrossSigningSettingsAction.SsoAuthDone -> { - // we should use token based auth - // _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) - // will release the interactive auth interceptor - Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation") + Timber.d("## UIA - FallBack success") if (pendingAuth != null) { uiaContinuation?.resume(pendingAuth!!) } else { uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) } - Unit } is CrossSigningSettingsAction.PasswordAuthDone -> { val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) diff --git a/vector/src/main/res/layout/fragment_deactivate_account.xml b/vector/src/main/res/layout/fragment_deactivate_account.xml index db85c607e1..4bbf0a496c 100644 --- a/vector/src/main/res/layout/fragment_deactivate_account.xml +++ b/vector/src/main/res/layout/fragment_deactivate_account.xml @@ -31,75 +31,14 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" /> - - - - - - - - - - - - - - -