Crypto: Delete device

This commit is contained in:
Benoit Marty 2019-06-13 16:39:35 +02:00
parent 567c1fd7a5
commit d353e9314b
9 changed files with 192 additions and 30 deletions

View file

@ -31,7 +31,10 @@ import java.io.IOException
sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
data class ServerError(val error: MatrixError) : Failure(RuntimeException(error.toString()))
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
// When server send an error, but it cannot be interpreted as a MatrixError
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody))
data class CryptoError(val error: MXCryptoError) : Failure(RuntimeException(error.toString()))
abstract class FeatureFailure : Failure()

View file

@ -16,8 +16,18 @@
package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* A Login flow.
*/
@JsonClass(generateAdapter = true)
internal data class LoginFlow(val type: String,
val stages: List<String>)
internal data class LoginFlow(
@Json(name = "type")
val type: String? = null,
@Json(name = "stages")
val stages: List<String>
)

View file

@ -0,0 +1,53 @@
/*
* Copyright 2019 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.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.auth.data.LoginFlow
@JsonClass(generateAdapter = true)
internal data class RegistrationFlowResponse(
/**
* The list of stages the client has completed successfully.
*/
@Json(name = "flows")
var loginFlows: List<LoginFlow>? = null,
/**
* The list of stages the client has completed successfully.
*/
@Json(name = "completed")
var completedStages: List<String>? = null,
/**
* The session identifier that the client must pass back to the home server, if one is provided,
* in subsequent attempts to authenticate in the same API call.
*/
@Json(name = "session")
var session: String? = null,
/**
* The information that the client will need to know in order to use a given type of authentication.
* For each login stage type presented, that type may be present as a key in this dictionary.
* For example, the public key of reCAPTCHA stage could be given here.
*/
@Json(name = "params")
var params: JsonDict? = null
)

View file

@ -227,7 +227,7 @@ internal class CryptoModule {
DefaultClaimOneTimeKeysForUsersDevice(get()) as ClaimOneTimeKeysForUsersDeviceTask
}
scope(DefaultSession.SCOPE) {
DefaultDeleteDeviceTask(get()) as DeleteDeviceTask
DefaultDeleteDeviceTask(get(), get()) as DeleteDeviceTask
}
scope(DefaultSession.SCOPE) {
DefaultDownloadKeysForUsers(get()) as DownloadKeysForUsersTask

View file

@ -19,7 +19,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* This class provides the
* This class provides the authentication data to delete a device
*/
@JsonClass(generateAdapter = true)
data class DeleteDeviceAuth(
@ -32,6 +32,9 @@ data class DeleteDeviceAuth(
@Json(name = "type")
var type: String? = null,
@Json(name = "user")
var user: String? = null,
@Json(name = "password")
var password: String? = null
)

View file

@ -15,6 +15,7 @@
*/
package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
@ -22,5 +23,6 @@ import com.squareup.moshi.JsonClass
*/
@JsonClass(generateAdapter = true)
data class DeleteDeviceParams(
var auth: DeleteDeviceAuth? = null
@Json(name = "auth")
var deleteDeviceAuth: DeleteDeviceAuth? = null
)

View file

@ -17,10 +17,19 @@
package im.vector.matrix.android.internal.crypto.tasks
import arrow.core.Try
import arrow.core.failure
import arrow.core.recoverWith
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceAuth
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import timber.log.Timber
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
data class Params(
@ -29,15 +38,84 @@ internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
)
}
internal class DefaultDeleteDeviceTask(private val cryptoApi: CryptoApi)
internal class DefaultDeleteDeviceTask(private val cryptoApi: CryptoApi,
private val credentials: Credentials)
: DeleteDeviceTask {
override suspend fun execute(params: DeleteDeviceTask.Params): Try<Unit> {
return executeRequest {
apiCall = cryptoApi.deleteDevice(params.deviceId,
DeleteDeviceParams())
}
return executeRequest<Unit> {
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
}.recoverWith { throwable ->
if (throwable is Failure.OtherServerError && throwable.httpCode == 401) {
// Replay the request with passing the credentials
// TODO Recover error, see legacy code MXSession.deleteDevice()
// Parse to get a RegistrationFlowResponse
val registrationFlowResponseAdapter = MoshiProvider.providesMoshi().adapter(RegistrationFlowResponse::class.java)
val registrationFlowResponse = try {
registrationFlowResponseAdapter.fromJson(throwable.errorBody)
} catch (e: Exception) {
null
}
// check if the server response can be casted
if (registrationFlowResponse?.loginFlows?.isNotEmpty() == true) {
val stages = ArrayList<String>()
// Get all stages
registrationFlowResponse.loginFlows?.forEach {
stages.addAll(it.stages)
}
Timber.v("## deleteDevice() : supported stages $stages")
deleteDeviceRecursive(registrationFlowResponse.session, params, stages)
} else {
throwable.failure()
}
} else {
// Other error
throwable.failure()
}
}
}
private fun deleteDeviceRecursive(authSession: String?,
params: DeleteDeviceTask.Params,
remainingStages: MutableList<String>): Try<Unit> {
// Pick the first stage
val stage = remainingStages.first()
val newParams = DeleteDeviceParams()
.apply {
deleteDeviceAuth = DeleteDeviceAuth()
.apply {
type = stage
session = authSession
user = credentials.userId
password = params.accountPassword
}
}
return executeRequest<Unit> {
apiCall = cryptoApi.deleteDevice(params.deviceId, newParams)
}.recoverWith { throwable ->
if (throwable is Failure.ServerError
&& throwable.httpCode == 401
&& (throwable.error.code == MatrixError.FORBIDDEN || throwable.error.code == MatrixError.UNKNOWN)) {
if (remainingStages.size > 1) {
// Try next stage
val otherStages = remainingStages.subList(1, remainingStages.size)
deleteDeviceRecursive(authSession, params, otherStages)
} else {
// No more stage remaining
throwable.failure()
}
} else {
// Other error
throwable.failure()
}
}
}
}

View file

@ -23,12 +23,14 @@ import arrow.effects.IO
import arrow.effects.fix
import arrow.effects.instances.io.async.async
import arrow.integrations.retrofit.adapter.runAsync
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider
import okhttp3.ResponseBody
import retrofit2.Call
import timber.log.Timber
import java.io.IOException
internal inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute()
@ -44,23 +46,38 @@ internal class Request<DATA> {
if (response.isSuccessful) {
response.body() ?: throw IllegalStateException("The request returned a null body")
} else {
throw manageFailure(response.errorBody())
throw manageFailure(response.errorBody(), response.code())
}
}.recoverWith {
when (it) {
is IOException -> Failure.NetworkConnection(it)
is Failure.ServerError -> it
else -> Failure.Unknown(it)
is IOException -> Failure.NetworkConnection(it)
is Failure.ServerError,
is Failure.OtherServerError -> it
else -> Failure.Unknown(it)
}.failure()
}
}
private fun manageFailure(errorBody: ResponseBody?): Throwable {
val matrixError = errorBody?.let {
val matrixErrorAdapter = moshi.adapter(MatrixError::class.java)
matrixErrorAdapter.fromJson(errorBody.source())
} ?: return RuntimeException("Matrix error should not be null")
return Failure.ServerError(matrixError)
}
private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable {
if (errorBody == null) {
return RuntimeException("Error body should not be null")
}
val errorBodyStr = errorBody.string()
val matrixErrorAdapter = moshi.adapter(MatrixError::class.java)
try {
val matrixError = matrixErrorAdapter.fromJson(errorBodyStr)
if (matrixError != null) {
return Failure.ServerError(matrixError, httpCode)
}
} catch (ex: JsonDataException) {
// This is not a MatrixError
Timber.w("The error returned by the server is not a MatrixError")
}
return Failure.OtherServerError(errorBodyStr, httpCode)
}
}

View file

@ -2493,15 +2493,12 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
* @param deviceId the device id
*/
private fun deleteDevice(deviceId: String) {
notImplemented()
// We have to manage registration flow first, to handle what is necessary to delete a devive
/*
displayLoadingView()
session.deleteDevice(deviceId, mAccountPassword, object : MatrixCallback<Unit> {
mSession.deleteDevice(deviceId, mAccountPassword, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
hideLoadingView()
refreshDevicesList() // force settings update
// force settings update
refreshDevicesList()
}
override fun onFailure(failure: Throwable) {
@ -2509,7 +2506,6 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
onCommonDone(failure.localizedMessage)
}
})
*/
}
/**