SSO UIA
This commit is contained in:
parent
98054815a4
commit
1244d00b31
@ -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<UIABaseAuth>)
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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<Unit>)
|
||||
|
||||
fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>)
|
||||
fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
|
@ -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<Unit>)
|
||||
|
||||
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null
|
||||
|
@ -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<Unit>) {
|
||||
override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
|
||||
deleteDeviceTask
|
||||
.configureWith(DeleteDeviceTask.Params(deviceId)) {
|
||||
.configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
|
||||
this.executionThread = TaskThread.CRYPTO
|
||||
this.callback = callback
|
||||
}
|
||||
|
@ -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<Unit>) {
|
||||
override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback<Unit>) {
|
||||
Timber.d("## CrossSigning initializeCrossSigning")
|
||||
|
||||
val params = InitializeCrossSigningTask.Params(
|
||||
authParams = authParams
|
||||
interactiveAuthInterceptor = uiaInterceptor
|
||||
)
|
||||
initializeCrossSigningTask.configureWith(params) {
|
||||
this.callbackThread = TaskThread.CRYPTO
|
||||
|
@ -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<String, *> = mapOf("session" to session)
|
||||
}
|
@ -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<String, *>? = null
|
||||
)
|
||||
|
@ -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<String, *> = mapOf(
|
||||
"session" to session,
|
||||
"token" to token,
|
||||
"transactionId" to transactionId,
|
||||
"type" to type
|
||||
)
|
||||
}
|
@ -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<String, *>
|
||||
}
|
@ -30,5 +30,5 @@ internal data class UploadSigningKeysBody(
|
||||
val userSigningKey: RestKeyInfo? = null,
|
||||
|
||||
@Json(name = "auth")
|
||||
val auth: UserPasswordAuth? = null
|
||||
val auth: Map<String, *>? = null
|
||||
)
|
||||
|
@ -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<String, *> = mapOf(
|
||||
"session" to session,
|
||||
"user" to user,
|
||||
"password" to password,
|
||||
"type" to type
|
||||
)
|
||||
}
|
||||
|
@ -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<DeleteDeviceTask.Params, Unit> {
|
||||
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<Unit>(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<UIABaseAuth> { continuation ->
|
||||
params.userInteractiveAuthInterceptor!!.performStage(flowResponse, continuation)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.w(failure, "## UIA: failed to participate")
|
||||
return false
|
||||
}
|
||||
|
||||
Timber.d("## UIA: delete device updated auth $authUpdate")
|
||||
return try {
|
||||
execute(params.copy(userAuthParam = authUpdate))
|
||||
true
|
||||
} catch (failure: Throwable) {
|
||||
handleUIA(failure, params)
|
||||
}
|
||||
} else {
|
||||
Timber.d("## UIA: not a UIA error")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
|
||||
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
|
||||
)
|
||||
|
||||
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<UIABaseAuth> { continuation ->
|
||||
params.interactiveAuthInterceptor!!.performStage(flowResponse, continuation)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.w(failure, "## UIA: failed to participate")
|
||||
return false
|
||||
}
|
||||
|
||||
Timber.d("## UIA: initialize xsigning updated auth $authUpdate")
|
||||
try {
|
||||
uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate))
|
||||
return true
|
||||
} catch (failure: Throwable) {
|
||||
return handleUIA(failure, params, uploadSigningKeysParams)
|
||||
}
|
||||
} else {
|
||||
Timber.d("## UIA: not a UIA error")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<UploadSigningKeysTask.Params, Un
|
||||
// the SSK
|
||||
val selfSignedKey: CryptoCrossSigningKey,
|
||||
/**
|
||||
* - If null:
|
||||
* - no retry will be performed
|
||||
* - If not null, it may or may not contain a sessionId:
|
||||
* - If sessionId is null:
|
||||
* - password should not be null: the task will perform a first request to get a sessionId, and then a second one
|
||||
* - If sessionId is not null:
|
||||
* - password should not be null as well, and no retry will be performed
|
||||
* Authorisation info (User Interactive flow)
|
||||
*/
|
||||
val userPasswordAuth: UserPasswordAuth?
|
||||
val userAuthParam: UIABaseAuth?
|
||||
)
|
||||
}
|
||||
|
||||
@ -59,31 +51,13 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
|
||||
) : UploadSigningKeysTask {
|
||||
|
||||
override suspend fun execute(params: UploadSigningKeysTask.Params) {
|
||||
val paramsHaveSessionId = params.userPasswordAuth?.session != null
|
||||
|
||||
val uploadQuery = UploadSigningKeysBody(
|
||||
masterKey = params.masterKey.toRest(),
|
||||
userSigningKey = params.userKey.toRest(),
|
||||
selfSigningKey = params.selfSignedKey.toRest(),
|
||||
// If sessionId is provided, use the userPasswordAuth
|
||||
auth = params.userPasswordAuth.takeIf { paramsHaveSessionId }
|
||||
auth = params.userAuthParam?.asMap()
|
||||
)
|
||||
try {
|
||||
doRequest(uploadQuery)
|
||||
} catch (throwable: Throwable) {
|
||||
val registrationFlowResponse = throwable.toRegistrationFlowResponse()
|
||||
if (registrationFlowResponse != null
|
||||
&& registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }
|
||||
&& params.userPasswordAuth?.password != null
|
||||
&& !paramsHaveSessionId
|
||||
) {
|
||||
// Retry with authentication
|
||||
doRequest(uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session)))
|
||||
} else {
|
||||
// Other error
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {
|
||||
|
@ -273,6 +273,19 @@ internal class DefaultSession @Inject constructor(
|
||||
return "$myUserId - ${sessionParams.deviceId}"
|
||||
}
|
||||
|
||||
override fun getUIASsoFallbackUrl(authenticationSessionId: String): String {
|
||||
val hsBas = sessionParams.homeServerConnectionConfig
|
||||
.homeServerUri
|
||||
.toString()
|
||||
.trim { it == '/' }
|
||||
return buildString {
|
||||
append(hsBas)
|
||||
append("/_matrix/client/r0/auth/m.login.sso/fallback/web")
|
||||
append("?session=")
|
||||
append(authenticationSessionId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun logDbUsageInfo() {
|
||||
RealmDebugTools(realmConfiguration).logInfo("Session")
|
||||
}
|
||||
|
@ -305,4 +305,5 @@
|
||||
|
||||
<string name="key_verification_request_fallback_message">%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.</string>
|
||||
|
||||
<string name="failed_to_initialize_cross_signing">Failed to set up Cross Signing</string>
|
||||
</resources>
|
||||
|
@ -242,6 +242,27 @@
|
||||
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
||||
<activity android:name=".features.usercode.UserCodeActivity" />
|
||||
|
||||
<!-- Single instance is very important for the custom scheme callback-->
|
||||
<activity android:name=".features.auth.ReAuthActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:exported="false">
|
||||
|
||||
<!-- XXX: UIA SSO has only web fallback, i.e no url redirect, so for now we comment this out
|
||||
hopefully, we would use it when finalyy available
|
||||
-->
|
||||
<!-- Add intent filter to handle redirection URL after SSO login in external browser -->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.VIEW" />-->
|
||||
|
||||
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||
|
||||
<!-- <data-->
|
||||
<!-- android:host="reauth"-->
|
||||
<!-- android:scheme="element" />-->
|
||||
<!-- </intent-filter>-->
|
||||
</activity>
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
<service
|
||||
|
@ -25,6 +25,7 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.preference.UserAvatarPreference
|
||||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.features.auth.ReAuthActivity
|
||||
import im.vector.app.features.call.CallControlsBottomSheet
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.conference.VectorJitsiActivity
|
||||
@ -145,6 +146,7 @@ interface ScreenComponent {
|
||||
fun inject(activity: VectorJitsiActivity)
|
||||
fun inject(activity: SearchActivity)
|
||||
fun inject(activity: UserCodeActivity)
|
||||
fun inject(activity: ReAuthActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.core.ui.list
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
|
||||
/**
|
||||
* A generic button list item.
|
||||
*/
|
||||
@EpoxyModelClass(layout = R.layout.item_positive_button)
|
||||
abstract class GenericPositiveButtonItem : VectorEpoxyModel<GenericPositiveButtonItem.Holder>() {
|
||||
|
||||
@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<MaterialButton>(R.id.itemGenericItemButton)
|
||||
}
|
||||
}
|
@ -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<FragmentReauthConfirmBinding>() {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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<Args>(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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
@ -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<ReAuthState, ReAuthActions, ReAuthEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(initialState: ReAuthState): ReAuthViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<ReAuthViewModel, ReAuthState> {
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Unit> {
|
||||
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
|
||||
crossSigningService.initializeCrossSigning(object : UserInteractiveAuthInterceptor {
|
||||
override fun performStage(flowResponse: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
|
||||
if (flowResponse.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) {
|
||||
val updatedAuth = params.userPasswordAuth?.copy(session = flowResponse.session)
|
||||
if (updatedAuth == null) {
|
||||
promise.resumeWith(Result.failure(UnsupportedOperationException()))
|
||||
} else {
|
||||
promise.resume(updatedAuth)
|
||||
}
|
||||
} else {
|
||||
promise.resumeWith(Result.failure(UnsupportedOperationException()))
|
||||
}
|
||||
}
|
||||
},
|
||||
it)
|
||||
}
|
||||
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
|
||||
return BootstrapResult.SuccessCrossSigningOnly
|
||||
|
@ -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,
|
||||
@ -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,
|
||||
object : UserInteractiveAuthInterceptor {
|
||||
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
|
||||
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,
|
||||
object : UserInteractiveAuthInterceptor {
|
||||
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
|
||||
if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) {
|
||||
UserPasswordAuth(
|
||||
session = flow.session,
|
||||
user = session.myUserId,
|
||||
password = password
|
||||
),
|
||||
)
|
||||
} else null
|
||||
}
|
||||
},
|
||||
callback = NoOpMatrixCallback()
|
||||
)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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<CrossSigningSettingsViewState>() {
|
||||
|
||||
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()
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
||||
class CrossSigningSettingsViewModel @AssistedInject constructor(
|
||||
@Assisted private val initialState: CrossSigningSettingsViewState,
|
||||
private val session: Session,
|
||||
private val reAuthHelper: ReAuthHelper,
|
||||
private val stringProvider: StringProvider
|
||||
) : VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
||||
|
||||
init {
|
||||
Observable.combineLatest<List<DeviceInfo>, Optional<MXCrossSigningInfo>, Pair<List<DeviceInfo>, Optional<MXCrossSigningInfo>>>(
|
||||
@ -58,15 +78,82 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
|
||||
}
|
||||
}
|
||||
|
||||
var uiaContinuation: Continuation<UIABaseAuth>? = 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<Unit> {
|
||||
session.cryptoService().crossSigningService().initializeCrossSigning(
|
||||
object : UserInteractiveAuthInterceptor {
|
||||
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
|
||||
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<CrossSigningSettingsViewModel, CrossSigningSettingsViewState> {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
||||
|
||||
var uiaContinuation: Continuation<UIABaseAuth>? = 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<Unit> = PublishSubject.create()
|
||||
|
||||
init {
|
||||
@ -189,13 +201,42 @@ class DevicesViewModel @AssistedInject constructor(
|
||||
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.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<Unit> {
|
||||
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
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
awaitCallback<Unit> {
|
||||
session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor {
|
||||
override fun performStage(flow: RegistrationFlowResponse, promise: Continuation<UIABaseAuth>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}, it)
|
||||
}
|
||||
|
||||
if (isPasswordRequestFound) {
|
||||
_currentDeviceId = deviceId
|
||||
_currentSession = failure.registrationFlowResponse.session
|
||||
|
||||
setState {
|
||||
copy(
|
||||
request = Success(Unit)
|
||||
)
|
||||
}
|
||||
|
||||
_viewEvents.post(DevicesViewEvents.RequestPassword)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPasswordRequestFound) {
|
||||
// LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far...
|
||||
// force settings update
|
||||
queryRefreshDevicesList()
|
||||
} catch (failure: Throwable) {
|
||||
setState {
|
||||
copy(
|
||||
request = Fail(failure)
|
||||
)
|
||||
}
|
||||
|
||||
_viewEvents.post(DevicesViewEvents.Failure(SsoFlowNotSupportedYet()))
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
setState {
|
||||
copy(
|
||||
request = Success(data)
|
||||
)
|
||||
}
|
||||
// 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<Unit> {
|
||||
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
|
||||
setState {
|
||||
copy(
|
||||
request = Fail(failure)
|
||||
)
|
||||
}
|
||||
|
||||
_viewEvents.post(DevicesViewEvents.Failure(failure))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
99
vector/src/main/res/layout/fragment_reauth_confirm.xml
Normal file
99
vector/src/main/res/layout/fragment_reauth_confirm.xml
Normal file
@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reAuthConfirmText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/re_authentication_default_confirm_text"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/reAuthConfirmButton"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/passwordContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/loginPasswordNotice"
|
||||
app:layout_constraintTop_toBottomOf="@id/reAuthConfirmText">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordFieldTil"
|
||||
style="@style/VectorTextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_signup_password_hint"
|
||||
app:errorEnabled="true"
|
||||
app:errorIconDrawable="@null">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/passwordField"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:paddingEnd="48dp"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/passwordReveal"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
android:layout_height="@dimen/layout_touch_size"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_eye"
|
||||
app:tint="?attr/colorAccent"
|
||||
tools:contentDescription="@string/a11y_show_password"
|
||||
tools:ignore="MissingPrefix" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loginPasswordNotice"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start"
|
||||
android:text="@string/login_signin_matrix_id_password_notice"
|
||||
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/passwordContainer"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/reAuthConfirmButton"
|
||||
style="@style/VectorButtonStylePositive"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:text="@string/_continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/loginPasswordNotice" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
19
vector/src/main/res/layout/item_positive_button.xml
Normal file
19
vector/src/main/res/layout/item_positive_button.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/itemGenericItemButton"
|
||||
style="@style/VectorButtonStylePositive"
|
||||
android:textAllCaps="false"
|
||||
app:iconGravity="textStart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Action Name" />
|
||||
|
||||
</LinearLayout>
|
@ -2792,4 +2792,8 @@
|
||||
<string name="warning_unsaved_change_discard">Discard changes</string>
|
||||
|
||||
<string name="matrix_to_card_title">Matrix Link</string>
|
||||
|
||||
<string name="re_authentication_activity_title">Re-Authentication Needed</string>
|
||||
<string name="re_authentication_default_confirm_text">Element requires you to enter your credentials to perform this action.</string>
|
||||
<string name="authentication_error">Failed to authenticate</string>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user