From 4bb804fbf7664adf593147d2c5ddc34200417ab3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 Jun 2020 00:16:38 +0200 Subject: [PATCH] Allow self-signed certificate (#1564) Accepted fingerprint before the migration to RiotX should still work after the migration. The dialog to trust the certificate is displayed during the login flow. For the moment, it is not displayed if the certificate change on the server once the user is logged in. This use case will be handled later. --- CHANGES.md | 1 + .../matrix/android/api/failure/Failure.kt | 3 + .../matrix/android/api/failure/GlobalError.kt | 3 + .../auth/DefaultAuthenticationService.kt | 21 ++- .../internal/auth/login/DefaultLoginWizard.kt | 3 +- .../registration/DefaultRegistrationWizard.kt | 3 +- .../android/internal/network/Request.kt | 12 +- .../internal/network/RetrofitFactory.kt | 14 +- .../network/httpclient/OkHttpClientUtil.kt | 40 ++-- .../android/internal/network/ssl/CertUtil.kt | 28 ++- .../internal/network/ssl/Fingerprint.kt | 6 +- .../network/ssl/PinnedTrustManager.kt | 11 +- .../android/internal/session/SessionModule.kt | 16 +- .../session/identity/IdentityModule.kt | 11 +- .../vector/riotx/core/di/ScreenComponent.kt | 2 + .../vector/riotx/core/di/VectorComponent.kt | 3 + .../dialogs/UnrecognizedCertificateDialog.kt | 171 ++++++++++++++++++ .../vector/riotx/core/error/ErrorFormatter.kt | 12 +- .../riotx/core/platform/VectorBaseActivity.kt | 28 ++- .../riotx/core/platform/VectorBaseFragment.kt | 3 + .../features/login/AbstractLoginFragment.kt | 30 ++- .../riotx/features/login/LoginAction.kt | 3 + .../riotx/features/login/LoginActivity.kt | 11 +- .../riotx/features/login/LoginViewModel.kt | 121 +++++++------ .../res/layout/dialog_ssl_fingerprint.xml | 59 ++++++ vector/src/main/res/values/strings.xml | 2 + 26 files changed, 501 insertions(+), 116 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/dialogs/UnrecognizedCertificateDialog.kt create mode 100644 vector/src/main/res/layout/dialog_ssl_fingerprint.xml diff --git a/CHANGES.md b/CHANGES.md index 06c1424404..831ef4f081 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ Improvements 🙌: - Prioritising Recovery key over Recovery passphrase (#1463) - Room Settings: Name, Topic, Photo, Aliases, History Visibility (#1455) - Update user avatar (#1054) + - Allow self-signed certificate (#1564) Bugfix 🐛: - Fix dark theme issue on login screen (#1097) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt index 4d44e3346b..f519819d0d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.failure import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse +import im.vector.matrix.android.internal.network.ssl.Fingerprint import java.io.IOException /** @@ -32,9 +33,11 @@ import java.io.IOException sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { data class Unknown(val throwable: Throwable? = null) : Failure(throwable) data class Cancelled(val throwable: Throwable? = null) : Failure(throwable) + data class UnrecognizedCertificateFailure(val url: String, val fingerprint: Fingerprint) : Failure() data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException) data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false"))) + // When server send an error, but it cannot be interpreted as a MatrixError data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody")) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/GlobalError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/GlobalError.kt index b2bc585258..ae268b9da2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/GlobalError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/GlobalError.kt @@ -16,8 +16,11 @@ package im.vector.matrix.android.api.failure +import im.vector.matrix.android.internal.network.ssl.Fingerprint + // This class will be sent to the bus sealed class GlobalError { data class InvalidToken(val softLogout: Boolean) : GlobalError() data class ConsentNotGivenError(val consentUri: String) : GlobalError() + data class CertificateError(val fingerprint: Fingerprint) : GlobalError() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index 2453bc0d05..473e77b95a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -43,6 +43,8 @@ import im.vector.matrix.android.internal.auth.version.isSupportedBySdk import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.network.httpclient.addSocketFactory +import im.vector.matrix.android.internal.network.ssl.UnrecognizedCertificateException import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.launchToCallback @@ -121,7 +123,11 @@ internal class DefaultAuthenticationService @Inject constructor( callback.onSuccess(it) }, { - callback.onFailure(it) + if (it is UnrecognizedCertificateException) { + callback.onFailure(Failure.UnrecognizedCertificateFailure(homeServerConnectionConfig.homeServerUri.toString(), it.fingerprint)) + } else { + callback.onFailure(it) + } } ) } @@ -248,7 +254,7 @@ internal class DefaultAuthenticationService @Inject constructor( ?: let { pendingSessionData?.homeServerConnectionConfig?.let { DefaultRegistrationWizard( - okHttpClient, + buildClient(it), retrofitFactory, coroutineDispatchers, sessionCreator, @@ -269,7 +275,7 @@ internal class DefaultAuthenticationService @Inject constructor( ?: let { pendingSessionData?.homeServerConnectionConfig?.let { DefaultLoginWizard( - okHttpClient, + buildClient(it), retrofitFactory, coroutineDispatchers, sessionCreator, @@ -347,7 +353,14 @@ internal class DefaultAuthenticationService @Inject constructor( } private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { - val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) + val retrofit = retrofitFactory.create(buildClient(homeServerConnectionConfig), homeServerConnectionConfig.homeServerUri.toString()) return retrofit.create(AuthAPI::class.java) } + + private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { + return okHttpClient.get() + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt index 2ce9372903..522f571cf7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.auth.login import android.util.Patterns -import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.login.LoginWizard @@ -44,7 +43,7 @@ import kotlinx.coroutines.withContext import okhttp3.OkHttpClient internal class DefaultLoginWizard( - okHttpClient: Lazy, + okHttpClient: OkHttpClient, retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionCreator: SessionCreator, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index 750d806b6f..dafb024b7e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.auth.registration -import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.LoginFlowTypes import im.vector.matrix.android.api.auth.registration.RegisterThreePid @@ -41,7 +40,7 @@ import okhttp3.OkHttpClient * This class execute the registration request and is responsible to keep the session of interactive authentication */ internal class DefaultRegistrationWizard( - private val okHttpClient: Lazy, + private val okHttpClient: OkHttpClient, private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionCreator: SessionCreator, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 44096fca71..5aaad6f7c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.network import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.shouldBeRetried +import im.vector.matrix.android.internal.network.ssl.CertUtil import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import org.greenrobot.eventbus.EventBus @@ -26,7 +27,7 @@ import retrofit2.awaitResponse import java.io.IOException internal suspend inline fun executeRequest(eventBus: EventBus?, - block: Request.() -> Unit) = Request(eventBus).apply(block).execute() + block: Request.() -> Unit) = Request(eventBus).apply(block).execute() internal class Request(private val eventBus: EventBus?) { @@ -48,6 +49,15 @@ internal class Request(private val eventBus: EventBus?) { throw response.toFailure(eventBus) } } catch (exception: Throwable) { + // Check if this is a certificateException + CertUtil.getCertificateException(exception) + // TODO Support certificate error once logged + //?.also { unrecognizedCertificateException -> + // // Send the error to the bus, for a global management + // eventBus?.post(GlobalError.CertificateError(unrecognizedCertificateException)) + //} + ?.also { unrecognizedCertificateException -> throw unrecognizedCertificateException } + if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) { delay(currentDelay) currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt index ad26171793..0b087d7a1a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitFactory.kt @@ -26,7 +26,19 @@ import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import javax.inject.Inject -class RetrofitFactory @Inject constructor(private val moshi: Moshi) { +internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) { + + /** + * Use only for authentication service + */ + fun create(okHttpClient: OkHttpClient, baseUrl: String): Retrofit { + return Retrofit.Builder() + .baseUrl(baseUrl.ensureTrailingSlash()) + .client(okHttpClient) + .addConverterFactory(UnitConverterFactory) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + } fun create(okHttpClient: Lazy, baseUrl: String): Retrofit { return Retrofit.Builder() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt index 8ffa0553e9..605d6b4a8c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt @@ -16,24 +16,38 @@ package im.vector.matrix.android.internal.network.httpclient +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor +import im.vector.matrix.android.internal.network.ssl.CertUtil import im.vector.matrix.android.internal.network.token.AccessTokenProvider import okhttp3.OkHttpClient +import timber.log.Timber -internal fun OkHttpClient.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient { - return newBuilder() - .apply { - // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor - val existingCurlInterceptors = interceptors().filterIsInstance() - interceptors().removeAll(existingCurlInterceptors) +internal fun OkHttpClient.Builder.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient.Builder { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance() + interceptors().removeAll(existingCurlInterceptors) - addInterceptor(AccessTokenInterceptor(accessTokenProvider)) + addInterceptor(AccessTokenInterceptor(accessTokenProvider)) - // Re add eventually the curl logging interceptors - existingCurlInterceptors.forEach { - addInterceptor(it) - } - } - .build() + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + + return this +} + +internal fun OkHttpClient.Builder.addSocketFactory(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient.Builder { + try { + val pair = CertUtil.newPinnedSSLSocketFactory(homeServerConnectionConfig) + sslSocketFactory(pair.sslSocketFactory, pair.x509TrustManager) + hostnameVerifier(CertUtil.newHostnameVerifier(homeServerConnectionConfig)) + connectionSpecs(CertUtil.newConnectionSpecs(homeServerConnectionConfig)) + } catch (e: Exception) { + Timber.e(e, "addSocketFactory failed") + } + + return this } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt index b304791b1c..2346ff8877 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/CertUtil.kt @@ -16,29 +16,30 @@ package im.vector.matrix.android.internal.network.ssl -import android.util.Pair import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import okhttp3.ConnectionSpec +import okhttp3.internal.tls.OkHostnameVerifier import timber.log.Timber import java.security.KeyStore import java.security.MessageDigest import java.security.cert.CertificateException import java.security.cert.X509Certificate import javax.net.ssl.HostnameVerifier -import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager -import kotlin.experimental.and /** * Various utility classes for dealing with X509Certificates */ internal object CertUtil { + // Set to false to do some test + private const val USE_DEFAULT_HOSTNAME_VERIFIER = true + private val hexArray = "0123456789ABCDEF".toCharArray() /** @@ -95,11 +96,10 @@ internal object CertUtil { * @param fingerprint the fingerprint * @return the hexa string. */ - @JvmOverloads fun fingerprintToHexString(fingerprint: ByteArray, sep: Char = ' '): String { val hexChars = CharArray(fingerprint.size * 3) for (j in fingerprint.indices) { - val v = (fingerprint[j] and 0xFF.toByte()).toInt() + val v = (fingerprint[j].toInt() and 0xFF) hexChars[j * 3] = hexArray[v.ushr(4)] hexChars[j * 3 + 1] = hexArray[v and 0x0F] hexChars[j * 3 + 2] = sep @@ -128,13 +128,18 @@ internal object CertUtil { return null } + internal data class PinnedSSLSocketFactory( + val sslSocketFactory: SSLSocketFactory, + val x509TrustManager: X509TrustManager + ) + /** * Create a SSLSocket factory for a HS config. * * @param hsConfig the HS config. * @return SSLSocket factory */ - fun newPinnedSSLSocketFactory(hsConfig: HomeServerConnectionConfig): Pair { + fun newPinnedSSLSocketFactory(hsConfig: HomeServerConnectionConfig): PinnedSSLSocketFactory { try { var defaultTrustManager: X509TrustManager? = null @@ -155,7 +160,7 @@ internal object CertUtil { try { tf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) } catch (e: Exception) { - Timber.e(e, "## addRule : onBingRuleUpdateFailure failed") + Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance of default failed") } } @@ -183,7 +188,7 @@ internal object CertUtil { sslSocketFactory = sslContext.socketFactory } - return Pair(sslSocketFactory, defaultTrustManager) + return PinnedSSLSocketFactory(sslSocketFactory, defaultTrustManager!!) } catch (e: Exception) { throw RuntimeException(e) } @@ -196,11 +201,14 @@ internal object CertUtil { * @return a new HostnameVerifier. */ fun newHostnameVerifier(hsConfig: HomeServerConnectionConfig): HostnameVerifier { - val defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier() + val defaultVerifier: HostnameVerifier = OkHostnameVerifier // HttpsURLConnection.getDefaultHostnameVerifier() val trustedFingerprints = hsConfig.allowedFingerprints return HostnameVerifier { hostname, session -> - if (defaultVerifier.verify(hostname, session)) return@HostnameVerifier true + if (USE_DEFAULT_HOSTNAME_VERIFIER) { + if (defaultVerifier.verify(hostname, session)) return@HostnameVerifier true + } + // TODO How to recover from this error? if (trustedFingerprints.isEmpty()) return@HostnameVerifier false // If remote cert matches an allowed fingerprint, just accept it. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/Fingerprint.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/Fingerprint.kt index dd8e70d459..e6139f3f9d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/Fingerprint.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/Fingerprint.kt @@ -32,7 +32,7 @@ data class Fingerprint( } @Throws(CertificateException::class) - fun matchesCert(cert: X509Certificate): Boolean { + internal fun matchesCert(cert: X509Certificate): Boolean { val o: Fingerprint? = when (hashType) { HashType.SHA256 -> newSha256Fingerprint(cert) HashType.SHA1 -> newSha1Fingerprint(cert) @@ -57,7 +57,7 @@ data class Fingerprint( return result } - companion object { + internal companion object { @Throws(CertificateException::class) fun newSha256Fingerprint(cert: X509Certificate): Fingerprint { @@ -79,6 +79,6 @@ data class Fingerprint( @JsonClass(generateAdapter = false) enum class HashType { @Json(name = "sha-1") SHA1, - @Json(name = "sha-256")SHA256 + @Json(name = "sha-256") SHA256 } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManager.kt index f3f9ecc562..f1c3652d0c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/ssl/PinnedTrustManager.kt @@ -34,16 +34,19 @@ import javax.net.ssl.X509TrustManager internal class PinnedTrustManager(private val fingerprints: List?, private val defaultTrustManager: X509TrustManager?) : X509TrustManager { + // Set to false to perform some test + private val USE_DEAFULT_TRUST_MANAGER = true + @Throws(CertificateException::class) override fun checkClientTrusted(chain: Array, s: String) { try { - if (defaultTrustManager != null) { + if (defaultTrustManager != null && USE_DEAFULT_TRUST_MANAGER) { defaultTrustManager.checkClientTrusted(chain, s) return } } catch (e: CertificateException) { // If there is an exception we fall back to checking fingerprints - if (fingerprints == null || fingerprints.isEmpty()) { + if (fingerprints.isNullOrEmpty()) { throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) } } @@ -54,14 +57,14 @@ internal class PinnedTrustManager(private val fingerprints: List?, @Throws(CertificateException::class) override fun checkServerTrusted(chain: Array, s: String) { try { - if (defaultTrustManager != null) { + if (defaultTrustManager != null && USE_DEAFULT_TRUST_MANAGER) { defaultTrustManager.checkServerTrusted(chain, s) return } } catch (e: CertificateException) { // If there is an exception we fall back to checking fingerprints if (fingerprints == null || fingerprints.isEmpty()) { - throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause) + throw UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.cause /* BMA: Shouldn't be `e` ? */) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 2e9f6174f2..23280edbdb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -51,14 +51,14 @@ import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.eventbus.EventBusTimberLogger -import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy import im.vector.matrix.android.internal.network.NetworkCallbackStrategy import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy import im.vector.matrix.android.internal.network.RetrofitFactory -import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor +import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor +import im.vector.matrix.android.internal.network.httpclient.addSocketFactory import im.vector.matrix.android.internal.network.token.AccessTokenProvider import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider import im.vector.matrix.android.internal.session.call.CallEventObserver @@ -189,23 +189,17 @@ internal abstract class SessionModule { @Authenticated fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, @Authenticated accessTokenProvider: AccessTokenProvider, + homeServerConnectionConfig: HomeServerConnectionConfig, @SessionId sessionId: String, @MockHttpInterceptor testInterceptor: TestInterceptor?): OkHttpClient { return okHttpClient.newBuilder() + .addAccessTokenInterceptor(accessTokenProvider) + .addSocketFactory(homeServerConnectionConfig) .apply { - // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor - val existingCurlInterceptors = interceptors().filterIsInstance() - interceptors().removeAll(existingCurlInterceptors) - - addInterceptor(AccessTokenInterceptor(accessTokenProvider)) if (testInterceptor != null) { testInterceptor.sessionId = sessionId addInterceptor(testInterceptor) } - // Re add eventually the curl logging interceptors - existingCurlInterceptors.forEach { - addInterceptor(it) - } } .build() } 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 7a5790788b..e906cf1938 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 @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.identity import dagger.Binds import dagger.Module import dagger.Provides +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthenticatedIdentity import im.vector.matrix.android.internal.di.IdentityDatabase @@ -26,6 +27,7 @@ import im.vector.matrix.android.internal.di.SessionFilesDirectory import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor +import im.vector.matrix.android.internal.network.httpclient.addSocketFactory import im.vector.matrix.android.internal.network.token.AccessTokenProvider import im.vector.matrix.android.internal.session.SessionModule import im.vector.matrix.android.internal.session.SessionScope @@ -46,8 +48,13 @@ internal abstract class IdentityModule { @SessionScope @AuthenticatedIdentity fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, - @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient { - return okHttpClient.addAccessTokenInterceptor(accessTokenProvider) + @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider, + homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { + return okHttpClient + .newBuilder() + .addAccessTokenInterceptor(accessTokenProvider) + .addSocketFactory(homeServerConnectionConfig) + .build() } @JvmStatic diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 7bba3cb5d4..ceb276614a 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -21,6 +21,7 @@ import androidx.fragment.app.FragmentFactory import androidx.lifecycle.ViewModelProvider import dagger.BindsInstance import dagger.Component +import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.features.MainActivity @@ -100,6 +101,7 @@ interface ScreenComponent { fun navigator(): Navigator fun errorFormatter(): ErrorFormatter fun uiStateRepository(): UiStateRepository + fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog /* ========================================================================================== * Activities diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 14f3019666..36dda5aa39 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -27,6 +27,7 @@ import im.vector.riotx.ActiveSessionDataSource import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.VectorApplication +import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.core.utils.AssetReader @@ -89,6 +90,8 @@ interface VectorComponent { fun activeSessionHolder(): ActiveSessionHolder + fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog + fun emojiCompatFontProvider(): EmojiCompatFontProvider fun emojiCompatWrapper(): EmojiCompatWrapper diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/UnrecognizedCertificateDialog.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/UnrecognizedCertificateDialog.kt new file mode 100644 index 0000000000..0c7ed6e714 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/UnrecognizedCertificateDialog.kt @@ -0,0 +1,171 @@ +/* + * 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.riotx.core.dialogs + +import android.app.Activity +import android.view.View +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import im.vector.matrix.android.internal.network.ssl.Fingerprint +import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.resources.StringProvider +import timber.log.Timber +import java.util.HashMap +import java.util.HashSet +import javax.inject.Inject + +/** + * This class displays the unknown certificate dialog + */ +class UnrecognizedCertificateDialog @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val stringProvider: StringProvider +) { + private val ignoredFingerprints: MutableMap> = HashMap() + private val openDialogIds: MutableSet = HashSet() + + /** + * Display a certificate dialog box, asking the user about an unknown certificate + * To use when user is currently logged in + * + * @param unrecognizedFingerprint the fingerprint for the unknown certificate + * @param callback callback to fire when the user makes a decision + */ + fun show(activity: Activity, + unrecognizedFingerprint: Fingerprint, + callback: Callback) { + val userId = activeSessionHolder.getSafeActiveSession()?.myUserId + val hsConfig = activeSessionHolder.getSafeActiveSession()?.sessionParams?.homeServerConnectionConfig ?: return + + internalShow(activity, unrecognizedFingerprint, true, callback, userId, hsConfig.homeServerUri.toString(), hsConfig.allowedFingerprints.isNotEmpty()) + } + + /** + * To use during login flow + */ + fun show(activity: Activity, + unrecognizedFingerprint: Fingerprint, + homeServerUrl: String, + callback: Callback) { + internalShow(activity, unrecognizedFingerprint, false, callback, null, homeServerUrl, false) + } + + /** + * Display a certificate dialog box, asking the user about an unknown certificate + * + * @param unrecognizedFingerprint the fingerprint for the unknown certificate + * @param existing the current session already exist, so it mean that something has changed server side + * @param callback callback to fire when the user makes a decision + */ + private fun internalShow(activity: Activity, + unrecognizedFingerprint: Fingerprint, + existing: Boolean, + callback: Callback, + userId: String?, + homeServerUrl: String, + homeServerConnectionConfigHasFingerprints: Boolean) { + val dialogId = userId ?: homeServerUrl + unrecognizedFingerprint.displayableHexRepr + + if (openDialogIds.contains(dialogId)) { + Timber.i("Not opening dialog $dialogId as one is already open.") + return + } + + if (userId != null) { + val f: Set? = ignoredFingerprints[userId] + if (f != null && f.contains(unrecognizedFingerprint)) { + callback.onIgnore() + return + } + } + + val builder = AlertDialog.Builder(activity) + val inflater = activity.layoutInflater + val layout: View = inflater.inflate(R.layout.dialog_ssl_fingerprint, null) + val sslFingerprintTitle = layout.findViewById(R.id.ssl_fingerprint_title) + sslFingerprintTitle.text = stringProvider.getString(R.string.ssl_fingerprint_hash, unrecognizedFingerprint.hashType.toString()) + val sslFingerprint = layout.findViewById(R.id.ssl_fingerprint) + sslFingerprint.text = unrecognizedFingerprint.displayableHexRepr + val sslUserId = layout.findViewById(R.id.ssl_user_id) + if (userId != null) { + sslUserId.text = stringProvider.getString(R.string.generic_label_and_value, + stringProvider.getString(R.string.username), + userId) + } else { + sslUserId.text = stringProvider.getString(R.string.generic_label_and_value, + stringProvider.getString(R.string.hs_url), + homeServerUrl) + } + val sslExpl = layout.findViewById(R.id.ssl_explanation) + if (existing) { + if (homeServerConnectionConfigHasFingerprints) { + sslExpl.text = stringProvider.getString(R.string.ssl_expected_existing_expl) + } else { + sslExpl.text = stringProvider.getString(R.string.ssl_unexpected_existing_expl) + } + } else { + sslExpl.text = stringProvider.getString(R.string.ssl_cert_new_account_expl) + } + builder.setView(layout) + builder.setTitle(R.string.ssl_could_not_verify) + builder.setPositiveButton(R.string.ssl_trust) { _, _ -> + callback.onAccept() + } + if (existing) { + builder.setNegativeButton(R.string.ssl_remain_offline) { _, _ -> + if (userId != null) { + var f = ignoredFingerprints[userId] + if (f == null) { + f = HashSet() + ignoredFingerprints[userId] = f + } + f.add(unrecognizedFingerprint) + } + callback.onIgnore() + } + builder.setNeutralButton(R.string.ssl_logout_account) { _, _ -> callback.onReject() } + } else { + builder.setNegativeButton(R.string.cancel) { _, _ -> callback.onReject() } + } + + builder.setOnDismissListener { + Timber.d("Dismissed!") + openDialogIds.remove(dialogId) + + } + + builder.show() + openDialogIds.add(dialogId) + } + + interface Callback { + /** + * The certificate was explicitly accepted + */ + fun onAccept() + + /** + * The warning was ignored by the user + */ + fun onIgnore() + + /** + * The unknown certificate was explicitly rejected + */ + fun onReject() + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index b2e02d564c..907107c90b 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -26,6 +26,8 @@ import java.net.HttpURLConnection import java.net.SocketTimeoutException import java.net.UnknownHostException import javax.inject.Inject +import javax.net.ssl.SSLException +import javax.net.ssl.SSLPeerUnverifiedException interface ErrorFormatter { fun toHumanReadable(throwable: Throwable?): String @@ -41,13 +43,17 @@ class DefaultErrorFormatter @Inject constructor( is IdentityServiceError -> identityServerError(throwable) is Failure.NetworkConnection -> { when (throwable.ioException) { - is SocketTimeoutException -> + is SocketTimeoutException -> stringProvider.getString(R.string.error_network_timeout) - is UnknownHostException -> + is UnknownHostException -> // Invalid homeserver? // TODO Check network state, airplane mode, etc. stringProvider.getString(R.string.login_error_unknown_host) - else -> + is SSLPeerUnverifiedException -> + stringProvider.getString(R.string.login_error_ssl_peer_unverified) + is SSLException -> + stringProvider.getString(R.string.login_error_ssl_other) + else -> stringProvider.getString(R.string.error_no_network) } } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index ba9e7320d2..bdd873d0cd 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -55,7 +55,10 @@ import im.vector.riotx.core.di.HasVectorInjector import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.VectorComponent import im.vector.riotx.core.dialogs.DialogLocker +import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.core.utils.toast import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivityArgs @@ -231,7 +234,30 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { is GlobalError.ConsentNotGivenError -> consentNotGivenHelper.displayDialog(globalError.consentUri, activeSessionHolder.getActiveSession().sessionParams.homeServerHost ?: "") - } + is GlobalError.CertificateError -> + handleCertificateError(globalError) + }.exhaustive + } + + private fun handleCertificateError(certificateError: GlobalError.CertificateError) { + vectorComponent() + .unrecognizedCertificateDialog() + .show(this, + certificateError.fingerprint, + object : UnrecognizedCertificateDialog.Callback { + override fun onAccept() { + // TODO Support certificate error once logged + } + + override fun onIgnore() { + // TODO Support certificate error once logged + } + + override fun onReject() { + // TODO Support certificate error once logged + } + } + ) } protected open fun handleInvalidToken(globalError: GlobalError.InvalidToken) { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index c4dcb0d996..c0b1b54c09 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -44,6 +44,7 @@ import im.vector.riotx.R import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.features.navigation.Navigator import io.reactivex.android.schedulers.AndroidSchedulers @@ -69,6 +70,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { protected lateinit var navigator: Navigator protected lateinit var errorFormatter: ErrorFormatter + protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog private var progress: ProgressDialog? = null @@ -92,6 +94,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) navigator = screenComponent.navigator() errorFormatter = screenComponent.errorFormatter() + unrecognizedCertificateDialog = screenComponent.unrecognizedCertificateDialog() viewModelFactory = screenComponent.viewModelFactory() childFragmentManager.fragmentFactory = screenComponent.fragmentFactory() injectWith(injector()) diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 8fceaad07f..5927e5b117 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -27,6 +27,7 @@ import com.airbnb.mvrx.withState import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.riotx.R +import im.vector.riotx.core.dialogs.UnrecognizedCertificateDialog import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.VectorBaseFragment @@ -72,7 +73,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { override fun showFailure(throwable: Throwable) { when (throwable) { - is Failure.ServerError -> { + is Failure.ServerError -> if (throwable.error.code == MatrixError.M_FORBIDDEN && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { AlertDialog.Builder(requireActivity()) @@ -83,11 +84,34 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { } else { onError(throwable) } - } - else -> onError(throwable) + is Failure.UnrecognizedCertificateFailure -> + showUnrecognizedCertificateFailure(throwable) + else -> + onError(throwable) } } + private fun showUnrecognizedCertificateFailure(failure: Failure.UnrecognizedCertificateFailure) { + // Ask the user to accept the certificate + unrecognizedCertificateDialog.show(requireActivity(), + failure.fingerprint, + failure.url, + object : UnrecognizedCertificateDialog.Callback { + override fun onAccept() { + // User accept the certificate + loginViewModel.handle(LoginAction.UserAcceptCertificate(failure.fingerprint)) + } + + override fun onIgnore() { + // Cannot happen in this case + } + + override fun onReject() { + // Nothing to do in this case + } + }) + } + open fun onError(throwable: Throwable) { super.showFailure(throwable) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index afd27f04a7..d083f6e9f7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.login import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.registration.RegisterThreePid +import im.vector.matrix.android.internal.network.ssl.Fingerprint import im.vector.riotx.core.platform.VectorViewModelAction sealed class LoginAction : VectorViewModelAction { @@ -61,4 +62,6 @@ sealed class LoginAction : VectorViewModelAction { data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction() data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction() + + data class UserAcceptCertificate(val fingerprint: Fingerprint) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 13bde075a6..86be00702c 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -133,15 +133,14 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { } } } - is LoginViewEvents.OutdatedHomeserver -> + is LoginViewEvents.OutdatedHomeserver -> { AlertDialog.Builder(this) .setTitle(R.string.login_error_outdated_homeserver_title) .setMessage(R.string.login_error_outdated_homeserver_content) .setPositiveButton(R.string.ok, null) .show() - is LoginViewEvents.Failure -> - // This is handled by the Fragments Unit + } is LoginViewEvents.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java, @@ -195,7 +194,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - } + is LoginViewEvents.Failure, + is LoginViewEvents.Loading -> + // This is handled by the Fragments + Unit + }.exhaustive } private fun updateWithState(loginViewState: LoginViewState) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 98cff4f6d9..1036a1fd0a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -87,6 +87,8 @@ class LoginViewModel @AssistedInject constructor( } } + private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null + val currentThreePid: String? get() = registrationWizard?.currentThreePid @@ -118,10 +120,18 @@ class LoginViewModel @AssistedInject constructor( is LoginAction.RegisterAction -> handleRegisterAction(action) is LoginAction.ResetAction -> handleResetAction(action) is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) + is LoginAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent) }.exhaustive } + private fun handleUserAcceptCertificate(action: LoginAction.UserAcceptCertificate) { + // It happen when we get the login flow, so alter the homeserver config and retrieve again the login flow + currentHomeServerConnectionConfig + ?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) } + ?.let { getLoginFlow(it) } + } + private fun handleLoginWithToken(action: LoginAction.LoginWithToken) { val safeLoginWizard = loginWizard @@ -649,67 +659,74 @@ class LoginViewModel @AssistedInject constructor( // This is invalid _viewEvents.post(LoginViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) } else { - currentTask?.cancel() - currentTask = null - authenticationService.cancelPendingLoginOrRegistration() + getLoginFlow(homeServerConnectionConfig) + } + } - setState { - copy( - asyncHomeServerLoginFlowRequest = Loading() - ) + private fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig) { + currentHomeServerConnectionConfig = homeServerConnectionConfig + + currentTask?.cancel() + currentTask = null + authenticationService.cancelPendingLoginOrRegistration() + + setState { + copy( + asyncHomeServerLoginFlowRequest = Loading() + ) + } + + currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + _viewEvents.post(LoginViewEvents.Failure(failure)) + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized + ) + } } - currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - _viewEvents.post(LoginViewEvents.Failure(failure)) - setState { - copy( - asyncHomeServerLoginFlowRequest = Uninitialized - ) - } - } - - override fun onSuccess(data: LoginFlowResult) { - when (data) { - is LoginFlowResult.Success -> { - val loginMode = when { - // SSO login is taken first - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported - } - - if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { - notSupported() - } else { - setState { - copy( - asyncHomeServerLoginFlowRequest = Uninitialized, - homeServerUrl = data.homeServerUrl, - loginMode = loginMode, - loginModeSupportedTypes = data.supportedLoginTypes.toList() - ) - } - } + override fun onSuccess(data: LoginFlowResult) { + when (data) { + is LoginFlowResult.Success -> { + val loginMode = when { + // SSO login is taken first + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } - is LoginFlowResult.OutdatedHomeserver -> { + + if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { notSupported() + } else { + // FIXME We should post a view event here normally? + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized, + homeServerUrl = data.homeServerUrl, + loginMode = loginMode, + loginModeSupportedTypes = data.supportedLoginTypes.toList() + ) + } } } - } - - private fun notSupported() { - // Notify the UI - _viewEvents.post(LoginViewEvents.OutdatedHomeserver) - - setState { - copy( - asyncHomeServerLoginFlowRequest = Uninitialized - ) + is LoginFlowResult.OutdatedHomeserver -> { + notSupported() } } - }) - } + } + + private fun notSupported() { + // Notify the UI + _viewEvents.post(LoginViewEvents.OutdatedHomeserver) + + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized + ) + } + } + }) } override fun onCleared() { diff --git a/vector/src/main/res/layout/dialog_ssl_fingerprint.xml b/vector/src/main/res/layout/dialog_ssl_fingerprint.xml new file mode 100644 index 0000000000..3c1abc11a2 --- /dev/null +++ b/vector/src/main/res/layout/dialog_ssl_fingerprint.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 83e0d847ac..05aa7d27bb 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -298,6 +298,8 @@ This URL is not reachable, please check it This is not a valid Matrix server address Cannot reach a homeserver at this URL, please check it + "SSL Error: the peer's identity has not been verified." + "SSL Error." Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect Mobile