Merge pull request #179 from vector-im/feature/cryptoFinalization

Crypto: Delete device
This commit is contained in:
Benoit Marty 2019-06-14 16:06:23 +02:00 committed by GitHub
commit 02ef1172ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 197 additions and 31 deletions

View file

@ -31,7 +31,10 @@ import java.io.IOException
sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class Unknown(val throwable: Throwable? = null) : Failure(throwable) data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException) 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())) data class CryptoError(val error: MXCryptoError) : Failure(RuntimeException(error.toString()))
abstract class FeatureFailure : Failure() abstract class FeatureFailure : Failure()

View file

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

View file

@ -16,7 +16,11 @@
package im.vector.matrix.android.internal.auth.data package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class LoginFlowResponse(val flows: List<LoginFlow>) internal data class LoginFlowResponse(
@Json(name = "flows")
val flows: List<InteractiveAuthenticationFlow>
)

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.InteractiveAuthenticationFlow
@JsonClass(generateAdapter = true)
internal data class RegistrationFlowResponse(
/**
* The list of flows.
*/
@Json(name = "flows")
var flows: List<InteractiveAuthenticationFlow>? = 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 DefaultClaimOneTimeKeysForUsersDevice(get()) as ClaimOneTimeKeysForUsersDeviceTask
} }
scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultDeleteDeviceTask(get()) as DeleteDeviceTask DefaultDeleteDeviceTask(get(), get()) as DeleteDeviceTask
} }
scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultDownloadKeysForUsers(get()) as DownloadKeysForUsersTask DefaultDownloadKeysForUsers(get()) as DownloadKeysForUsersTask

View file

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

View file

@ -15,6 +15,7 @@
*/ */
package im.vector.matrix.android.internal.crypto.model.rest package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
/** /**
@ -22,5 +23,6 @@ import com.squareup.moshi.JsonClass
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class DeleteDeviceParams( 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 package im.vector.matrix.android.internal.crypto.tasks
import arrow.core.Try 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.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.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.network.executeRequest
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import timber.log.Timber
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> { internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
data class Params( 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 { : DeleteDeviceTask {
override suspend fun execute(params: DeleteDeviceTask.Params): Try<Unit> { override suspend fun execute(params: DeleteDeviceTask.Params): Try<Unit> {
return executeRequest { return executeRequest<Unit> {
apiCall = cryptoApi.deleteDevice(params.deviceId, apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
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?.flows?.isNotEmpty() == true) {
val stages = ArrayList<String>()
// Get all stages
registrationFlowResponse.flows?.forEach {
stages.addAll(it.stages ?: emptyList())
}
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.fix
import arrow.effects.instances.io.async.async import arrow.effects.instances.io.async.async
import arrow.integrations.retrofit.adapter.runAsync import arrow.integrations.retrofit.adapter.runAsync
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Call import retrofit2.Call
import timber.log.Timber
import java.io.IOException import java.io.IOException
internal inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute() 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) { if (response.isSuccessful) {
response.body() ?: throw IllegalStateException("The request returned a null body") response.body() ?: throw IllegalStateException("The request returned a null body")
} else { } else {
throw manageFailure(response.errorBody()) throw manageFailure(response.errorBody(), response.code())
} }
}.recoverWith { }.recoverWith {
when (it) { when (it) {
is IOException -> Failure.NetworkConnection(it) is IOException -> Failure.NetworkConnection(it)
is Failure.ServerError -> it is Failure.ServerError,
else -> Failure.Unknown(it) is Failure.OtherServerError -> it
else -> Failure.Unknown(it)
}.failure() }.failure()
} }
} }
private fun manageFailure(errorBody: ResponseBody?): Throwable { private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable {
val matrixError = errorBody?.let { if (errorBody == null) {
val matrixErrorAdapter = moshi.adapter(MatrixError::class.java) return RuntimeException("Error body should not be null")
matrixErrorAdapter.fromJson(errorBody.source()) }
} ?: return RuntimeException("Matrix error should not be null")
return Failure.ServerError(matrixError)
}
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

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