This commit is contained in:
Valere 2021-01-27 09:52:18 +01:00
parent 98054815a4
commit 1244d00b31
43 changed files with 1381 additions and 200 deletions

View File

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

View File

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

View File

@ -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>)

View File

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

View File

@ -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
}

View File

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

View File

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

View File

@ -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
)

View File

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

View File

@ -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, *>
}

View File

@ -30,5 +30,5 @@ internal data class UploadSigningKeysBody(
val userSigningKey: RestKeyInfo? = null,
@Json(name = "auth")
val auth: UserPasswordAuth? = null
val auth: Map<String, *>? = null
)

View File

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

View File

@ -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
}
}
}

View File

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

View File

@ -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
}
}
}

View File

@ -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) {

View File

@ -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")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -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> {

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

View 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>

View File

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