From 6e43e9b51c36dcce14323bc598ae9492e2a242f7 Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoitm@matrix.org>
Date: Thu, 7 May 2020 22:51:18 +0200
Subject: [PATCH] Identity: refresh pepper, logout feature and other
 improvements

---
 .../matrix/android/api/failure/MatrixError.kt |  8 ++-
 .../session/identity/BulkLookupTask.kt        | 72 ++++++++++++++-----
 .../identity/DefaultIdentityService.kt        | 22 +++---
 .../identity/IdentityDisconnectTask.kt        | 37 ++++++++++
 .../session/identity/IdentityModule.kt        |  3 +
 .../settings/VectorSettingsGeneralFragment.kt |  4 ++
 .../features/terms/ReviewTermsFragment.kt     |  6 ++
 7 files changed, 127 insertions(+), 25 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt

diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
index 2dcead7477..7c9ace5d82 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
@@ -39,7 +39,10 @@ data class MatrixError(
         // For M_LIMIT_EXCEEDED
         @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
         // For M_UNKNOWN_TOKEN
-        @Json(name = "soft_logout") val isSoftLogout: Boolean = false
+        @Json(name = "soft_logout") val isSoftLogout: Boolean = false,
+        // For M_INVALID_PEPPER
+        // {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"}
+        @Json(name = "lookup_pepper") val newLookupPepper: String? = null
 ) {
 
     companion object {
@@ -131,6 +134,9 @@ data class MatrixError(
 
         const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED"
 
+        // For identity service
+        const val M_INVALID_PEPPER = "M_INVALID_PEPPER"
+
         // Possible value for "limit_type"
         const val LIMIT_TYPE_MAU = "monthly_active_user"
     }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt
index 0524e704f3..e780e33478 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/BulkLookupTask.kt
@@ -16,6 +16,8 @@
 
 package im.vector.matrix.android.internal.session.identity
 
+import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.api.failure.MatrixError
 import im.vector.matrix.android.api.session.identity.FoundThreePid
 import im.vector.matrix.android.api.session.identity.IdentityServiceError
 import im.vector.matrix.android.api.session.identity.ThreePid
@@ -26,6 +28,7 @@ import im.vector.matrix.android.internal.session.identity.db.IdentityServiceStor
 import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse
 import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Params
 import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpV2Response
+import im.vector.matrix.android.internal.session.profile.ThirdPartyIdentifier
 import im.vector.matrix.android.internal.task.Task
 import java.util.Locale
 import javax.inject.Inject
@@ -47,16 +50,14 @@ internal class DefaultBulkLookupTask @Inject constructor(
         val pepper = entity.hashLookupPepper
         val hashDetailResponse = if (pepper == null) {
             // We need to fetch the hash details first
-            executeRequest<IdentityHashDetailResponse>(null) {
-                apiCall = identityAPI.hashDetails()
-            }
-                    .also { identityServiceStore.setHashDetails(it) }
+            fetchAndStoreHashDetails(identityAPI)
         } else {
             IdentityHashDetailResponse(pepper, entity.hashLookupAlgorithm.toList())
         }
 
         if (hashDetailResponse.algorithms.contains("sha256").not()) {
-            // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment do not do it
+            // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it
+            // Also, what we have in cache could be outdated, the identity server maybe now supports sha256
             throw IdentityServiceError.BulkLookupSha256NotSupported
         }
 
@@ -69,20 +70,59 @@ internal class DefaultBulkLookupTask @Inject constructor(
             }
         }
 
-        val identityLookUpV2Response = executeRequest<IdentityLookUpV2Response>(null) {
-            apiCall = identityAPI.bulkLookupV2(IdentityLookUpV2Params(
-                    hashedAddresses,
-                    "sha256",
-                    hashDetailResponse.pepper
-            ))
-        }
-
-        // TODO Catch invalid hash pepper and retry
+        val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true)
 
         // Convert back to List<FoundThreePid>
         return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response)
     }
 
+    private suspend fun lookUpInternal(identityAPI: IdentityAPI,
+                                       hashedAddresses: List<String>,
+                                       hashDetailResponse: IdentityHashDetailResponse,
+                                       canRetry: Boolean): IdentityLookUpV2Response {
+        return try {
+            executeRequest(null) {
+                apiCall = identityAPI.bulkLookupV2(IdentityLookUpV2Params(
+                        hashedAddresses,
+                        "sha256",
+                        hashDetailResponse.pepper
+                ))
+            }
+        } catch (failure: Throwable) {
+            // Catch invalid hash pepper and retry
+            if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) {
+                // This is not documented, by the error can contain the new pepper!
+                if (!failure.error.newLookupPepper.isNullOrEmpty()) {
+                    // Store it and use it right now
+                    hashDetailResponse.copy(pepper = failure.error.newLookupPepper)
+                            .also { identityServiceStore.setHashDetails(it) }
+                            .let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) }
+                } else {
+                    // Retrieve the new hash details
+                    val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI)
+
+                    if (hashDetailResponse.algorithms.contains("sha256").not()) {
+                        // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it
+                        // Also, what we have in cache is maybe outdated, the identity server maybe now support sha256
+                        throw IdentityServiceError.BulkLookupSha256NotSupported
+                    }
+
+                    lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */)
+                }
+            } else {
+                // Other error
+                throw failure
+            }
+        }
+    }
+
+    private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse {
+        return executeRequest<IdentityHashDetailResponse>(null) {
+            apiCall = identityAPI.hashDetails()
+        }
+                .also { identityServiceStore.setHashDetails(it) }
+    }
+
     private fun handleSuccess(threePids: List<ThreePid>, hashedAddresses: List<String>, identityLookUpV2Response: IdentityLookUpV2Response): List<FoundThreePid> {
         return identityLookUpV2Response.mappings.keys.map { hashedAddress ->
             FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpV2Response.mappings[hashedAddress] ?: error(""))
@@ -91,8 +131,8 @@ internal class DefaultBulkLookupTask @Inject constructor(
 
     private fun ThreePid.toMedium(): String {
         return when (this) {
-            is ThreePid.Email  -> "email"
-            is ThreePid.Msisdn -> "msisdn"
+            is ThreePid.Email  -> ThirdPartyIdentifier.MEDIUM_EMAIL
+            is ThreePid.Msisdn -> ThirdPartyIdentifier.MEDIUM_MSISDN
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt
index ab264dd8b9..fa7facae73 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt
@@ -42,7 +42,6 @@ import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
 import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent
 import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
 import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
-import im.vector.matrix.android.internal.task.TaskExecutor
 import im.vector.matrix.android.internal.task.launchToCallback
 import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
 import kotlinx.coroutines.GlobalScope
@@ -57,7 +56,7 @@ internal class DefaultIdentityService @Inject constructor(
         private val getOpenIdTokenTask: GetOpenIdTokenTask,
         private val bulkLookupTask: BulkLookupTask,
         private val identityRegisterTask: IdentityRegisterTask,
-        private val taskExecutor: TaskExecutor,
+        private val identityDisconnectTask: IdentityDisconnectTask,
         @Unauthenticated
         private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
         @AuthenticatedIdentity
@@ -147,12 +146,19 @@ internal class DefaultIdentityService @Inject constructor(
                     // Nothing to do
                     Timber.d("Same URL, nothing to do")
                 null    -> {
-                    // TODO Disconnect previous one if any
+                    // Disconnect previous one if any
+                    identityServiceStore.get()?.let {
+                        if (it.identityServerUrl != null && it.token != null) {
+                            // Disconnect, ignoring any error
+                            runCatching {
+                                identityDisconnectTask.execute(Unit)
+                            }
+                        }
+                    }
                     identityServiceStore.setUrl(null)
                     updateAccountData(null)
                 }
                 else    -> {
-                    // TODO: check first that it is a valid identity server
                     // Try to get a token
                     getIdentityServerToken(urlCandidate)
 
@@ -179,7 +185,7 @@ internal class DefaultIdentityService @Inject constructor(
         }
     }
 
-    private suspend fun lookUpInternal(firstTime: Boolean, threePids: List<ThreePid>): List<FoundThreePid> {
+    private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> {
         ensureToken()
 
         return try {
@@ -187,12 +193,12 @@ internal class DefaultIdentityService @Inject constructor(
         } catch (throwable: Throwable) {
             // Refresh token?
             when {
-                throwable.isInvalidToken() && firstTime -> {
+                throwable.isInvalidToken() && canRetry -> {
                     identityServiceStore.setToken(null)
                     lookUpInternal(false, threePids)
                 }
-                throwable.isTermsNotSigned()            -> throw IdentityServiceError.TermsNotSignedException
-                else                                    -> throw throwable
+                throwable.isTermsNotSigned()           -> throw IdentityServiceError.TermsNotSignedException
+                else                                   -> throw throwable
             }
         }
     }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt
new file mode 100644
index 0000000000..0d0c24a4b0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.matrix.android.internal.session.identity
+
+import im.vector.matrix.android.api.session.identity.IdentityServiceError
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.task.Task
+import javax.inject.Inject
+
+internal interface IdentityDisconnectTask : Task<Unit, Unit>
+
+internal class DefaultIdentityDisconnectTask @Inject constructor(
+        private val identityApiProvider: IdentityApiProvider
+) : IdentityDisconnectTask {
+
+    override suspend fun execute(params: Unit) {
+        val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured
+
+        executeRequest<Unit>(null) {
+            apiCall = identityAPI.logout()
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt
index b320bac28d..d735e8a785 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt
@@ -70,4 +70,7 @@ internal abstract class IdentityModule {
 
     @Binds
     abstract fun bindBulkLookupTask(task: DefaultBulkLookupTask): BulkLookupTask
+
+    @Binds
+    abstract fun bindIdentityDisconnectTask(task: DefaultIdentityDisconnectTask): IdentityDisconnectTask
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
index b11ffbcd84..9bc524781c 100644
--- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt
@@ -130,6 +130,10 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
 
             // Unfortunately, this is not supported in lib v7
             // it.editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+            it.setOnPreferenceClickListener {
+                notImplemented()
+                true
+            }
 
             it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
                 notImplemented()
diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt
index 70d8ab8658..7e73a7c1b8 100644
--- a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt
@@ -28,6 +28,7 @@ import im.vector.riotx.core.epoxy.onClick
 import im.vector.riotx.core.extensions.cleanup
 import im.vector.riotx.core.extensions.configureWith
 import im.vector.riotx.core.extensions.exhaustive
+import im.vector.riotx.core.platform.VectorBaseActivity
 import im.vector.riotx.core.platform.VectorBaseFragment
 import im.vector.riotx.core.utils.openUrlInExternalBrowser
 import kotlinx.android.synthetic.main.fragment_review_terms.*
@@ -76,6 +77,11 @@ class ReviewTermsFragment @Inject constructor(
         super.onDestroyView()
     }
 
+    override fun onResume() {
+        super.onResume()
+        (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.terms_of_service)
+    }
+
     override fun invalidate() = withState(reviewTermsViewModel) { state ->
         when (state.termsList) {
             is Loading -> {