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.
This commit is contained in:
Benoit Marty 2020-06-30 00:16:38 +02:00
parent 6cb82421e4
commit 4bb804fbf7
26 changed files with 501 additions and 116 deletions

View File

@ -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)

View File

@ -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"))

View File

@ -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()
}

View File

@ -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()
}
}

View File

@ -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: OkHttpClient,
retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionCreator: SessionCreator,

View File

@ -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<OkHttpClient>,
private val okHttpClient: OkHttpClient,
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionCreator: SessionCreator,

View File

@ -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 <DATA : Any> executeRequest(eventBus: EventBus?,
block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute()
block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute()
internal class Request<DATA : Any>(private val eventBus: EventBus?) {
@ -48,6 +49,15 @@ internal class Request<DATA : Any>(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)

View File

@ -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<OkHttpClient>, baseUrl: String): Retrofit {
return Retrofit.Builder()

View File

@ -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<CurlLoggingInterceptor>()
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<CurlLoggingInterceptor>()
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
}

View File

@ -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<SSLSocketFactory, X509TrustManager> {
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, X509TrustManager>(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.

View File

@ -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
}
}

View File

@ -34,16 +34,19 @@ import javax.net.ssl.X509TrustManager
internal class PinnedTrustManager(private val fingerprints: List<Fingerprint>?,
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<X509Certificate>, 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<Fingerprint>?,
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, 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` ? */)
}
}

View File

@ -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<CurlLoggingInterceptor>()
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()
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<String, MutableSet<Fingerprint>> = HashMap()
private val openDialogIds: MutableSet<String> = 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<Fingerprint>? = 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<TextView>(R.id.ssl_fingerprint_title)
sslFingerprintTitle.text = stringProvider.getString(R.string.ssl_fingerprint_hash, unrecognizedFingerprint.hashType.toString())
val sslFingerprint = layout.findViewById<TextView>(R.id.ssl_fingerprint)
sslFingerprint.text = unrecognizedFingerprint.displayableHexRepr
val sslUserId = layout.findViewById<TextView>(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<TextView>(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()
}
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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())

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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) {

View File

@ -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<LoginFlowResult> {
override fun onFailure(failure: Throwable) {
_viewEvents.post(LoginViewEvents.Failure(failure))
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized
)
}
}
currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback<LoginFlowResult> {
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() {

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/ssl_cert_not_trust" />
<TextView
android:id="@+id/ssl_explanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/ssl_cert_new_account_expl" />
<TextView
android:id="@+id/ssl_user_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
tools:text="Home Server URL: http://matrix.org" />
<TextView
android:id="@+id/ssl_fingerprint_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/ssl_fingerprint_hash" />
<TextView
android:id="@+id/ssl_fingerprint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
tools:text="07 89 7A A0 30 82 99 95 E6 17 5D 1F 34 5D 8D 0C 67 82 63 1C 1F 57 20 75 42 91 F7 8B 28 03 54 A2" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/ssl_only_accept" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -298,6 +298,8 @@
<string name="login_error_unknown_host">This URL is not reachable, please check it</string>
<string name="login_error_no_homeserver_found">This is not a valid Matrix server address</string>
<string name="login_error_homeserver_not_found">Cannot reach a homeserver at this URL, please check it</string>
<string name="login_error_ssl_peer_unverified">"SSL Error: the peer's identity has not been verified."</string>
<string name="login_error_ssl_other">"SSL Error."</string>
<string name="login_error_ssl_handshake">Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect</string>
<string name="login_mobile_device">Mobile</string>