mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge pull request #179 from vector-im/feature/cryptoFinalization
Crypto: Delete device
This commit is contained in:
commit
02ef1172ce
10 changed files with 197 additions and 31 deletions
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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>
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue