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/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt index effeae596a..08007e3397 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt @@ -89,6 +89,7 @@ interface AuthenticationService { * Perform a wellknown request, using the domain from the matrixId */ fun getWellKnownData(matrixId: String, + homeServerConnectionConfig: HomeServerConnectionConfig?, callback: MatrixCallback): Cancelable /** 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..087e00848c 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) + } } ) } @@ -209,7 +215,7 @@ internal class DefaultAuthenticationService @Inject constructor( // Create a fake userId, for the getWellknown task val fakeUserId = "@alice:$domain" - val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId)) + val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId, homeServerConnectionConfig)) return when (wellknownResult) { is WellknownResult.Prompt -> { @@ -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, @@ -321,9 +327,11 @@ internal class DefaultAuthenticationService @Inject constructor( } } - override fun getWellKnownData(matrixId: String, callback: MatrixCallback): Cancelable { + override fun getWellKnownData(matrixId: String, + homeServerConnectionConfig: HomeServerConnectionConfig?, + callback: MatrixCallback): Cancelable { return getWellknownTask - .configureWith(GetWellknownTask.Params(matrixId)) { + .configureWith(GetWellknownTask.Params(matrixId, homeServerConnectionConfig)) { this.callback = callback } .executeBy(taskExecutor) @@ -347,7 +355,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/login/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt index 90eddf2e14..22a921a094 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.auth.data.PasswordLoginParams 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.task.Task import okhttp3.OkHttpClient import javax.inject.Inject @@ -47,7 +48,8 @@ internal class DefaultDirectLoginTask @Inject constructor( ) : DirectLoginTask { override suspend fun execute(params: DirectLoginTask.Params): Session { - val authAPI = retrofitFactory.create(okHttpClient, params.homeServerConnectionConfig.homeServerUri.toString()) + val client = buildClient(params.homeServerConnectionConfig) + val authAPI = retrofitFactory.create(client, params.homeServerConnectionConfig.homeServerUri.toString()) .create(AuthAPI::class.java) val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) @@ -58,4 +60,11 @@ internal class DefaultDirectLoginTask @Inject constructor( return sessionCreator.createSession(credentials, params.homeServerConnectionConfig) } + + 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/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/di/AuthQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt index 0ceb94caa7..105d904329 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt @@ -29,3 +29,7 @@ internal annotation class AuthenticatedIdentity @Qualifier @Retention(AnnotationRetention.RUNTIME) internal annotation class Unauthenticated + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class UnauthenticatedWithCertificate diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/CertUtil.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/CertUtil.java deleted file mode 100644 index 6c48eddad8..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/CertUtil.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * 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.legacy.riot; - -import android.util.Pair; - -import androidx.annotation.NonNull; - -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import okhttp3.CipherSuite; -import okhttp3.ConnectionSpec; -import okhttp3.TlsVersion; -import timber.log.Timber; - -/* - * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose - */ - -/** - * Various utility classes for dealing with X509Certificates - */ -public class CertUtil { - /** - * Generates the SHA-256 fingerprint of the given certificate - * - * @param cert the certificate. - * @return the finger print - * @throws CertificateException the certificate exception - */ - public static byte[] generateSha256Fingerprint(X509Certificate cert) throws CertificateException { - return generateFingerprint(cert, "SHA-256"); - } - - /** - * Generates the SHA-1 fingerprint of the given certificate - * - * @param cert the certificated - * @return the SHA1 fingerprint - * @throws CertificateException the certificate exception - */ - public static byte[] generateSha1Fingerprint(X509Certificate cert) throws CertificateException { - return generateFingerprint(cert, "SHA-1"); - } - - /** - * Generate the fingerprint for a dedicated type. - * - * @param cert the certificate - * @param type the type - * @return the fingerprint - * @throws CertificateException certificate exception - */ - private static byte[] generateFingerprint(X509Certificate cert, String type) throws CertificateException { - final byte[] fingerprint; - final MessageDigest md; - try { - md = MessageDigest.getInstance(type); - } catch (Exception e) { - // This really *really* shouldn't throw, as java should always have a SHA-256 and SHA-1 impl. - throw new CertificateException(e); - } - - fingerprint = md.digest(cert.getEncoded()); - - return fingerprint; - } - - final private static char[] hexArray = "0123456789ABCDEF".toCharArray(); - - /** - * Convert the fingerprint to an hexa string. - * - * @param fingerprint the fingerprint - * @return the hexa string. - */ - public static String fingerprintToHexString(byte[] fingerprint) { - return fingerprintToHexString(fingerprint, ' '); - } - - public static String fingerprintToHexString(byte[] fingerprint, char sep) { - char[] hexChars = new char[fingerprint.length * 3]; - for (int j = 0; j < fingerprint.length; j++) { - int v = fingerprint[j] & 0xFF; - hexChars[j * 3] = hexArray[v >>> 4]; - hexChars[j * 3 + 1] = hexArray[v & 0x0F]; - hexChars[j * 3 + 2] = sep; - } - return new String(hexChars, 0, hexChars.length - 1); - } - - /** - * Recursively checks the exception to see if it was caused by an - * UnrecognizedCertificateException - * - * @param e the throwable. - * @return The UnrecognizedCertificateException if exists, else null. - */ - public static UnrecognizedCertificateException getCertificateException(Throwable e) { - int i = 0; // Just in case there is a getCause loop - while (e != null && i < 10) { - if (e instanceof UnrecognizedCertificateException) { - return (UnrecognizedCertificateException) e; - } - e = e.getCause(); - i++; - } - - return null; - } - - /** - * Create a SSLSocket factory for a HS config. - * - * @param hsConfig the HS config. - * @return SSLSocket factory - */ - public static Pair newPinnedSSLSocketFactory(HomeServerConnectionConfig hsConfig) { - X509TrustManager defaultTrustManager = null; - - // If we haven't specified that we wanted to pin the certs, fallback to standard - // X509 checks if fingerprints don't match. - if (!hsConfig.shouldPin()) { - TrustManagerFactory trustManagerFactory = null; - - // get the PKIX instance - try { - trustManagerFactory = TrustManagerFactory.getInstance("PKIX"); - } catch (NoSuchAlgorithmException e) { - Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance failed"); - } - - // it doesn't exist, use the default one. - if (trustManagerFactory == null) { - try { - trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - } catch (NoSuchAlgorithmException e) { - Timber.e(e, "## newPinnedSSLSocketFactory() : TrustManagerFactory.getInstance with default algorithm failed"); - } - } - - if (trustManagerFactory != null) { - try { - trustManagerFactory.init((KeyStore) null); - TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); - - for (int i = 0; i < trustManagers.length; i++) { - if (trustManagers[i] instanceof X509TrustManager) { - defaultTrustManager = (X509TrustManager) trustManagers[i]; - break; - } - } - } catch (KeyStoreException e) { - Timber.e(e, "## newPinnedSSLSocketFactory()"); - } - } - } - - X509TrustManager trustManager = new PinnedTrustManager(hsConfig.getAllowedFingerprints(), defaultTrustManager); - - TrustManager[] trustManagers = new TrustManager[]{ - trustManager - }; - - SSLSocketFactory sslSocketFactory; - - try { - if (hsConfig.forceUsageOfTlsVersions() && hsConfig.getAcceptedTlsVersions() != null) { - // Force usage of accepted Tls Versions for Android < 20 - sslSocketFactory = new TLSSocketFactory(trustManagers, hsConfig.getAcceptedTlsVersions()); - } else { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustManagers, new java.security.SecureRandom()); - sslSocketFactory = sslContext.getSocketFactory(); - } - } catch (Exception e) { - // This is too fatal - throw new RuntimeException(e); - } - - return new Pair<>(sslSocketFactory, trustManager); - } - - /** - * Create a Host name verifier for a hs config. - * - * @param hsConfig the hs config. - * @return a new HostnameVerifier. - */ - public static HostnameVerifier newHostnameVerifier(HomeServerConnectionConfig hsConfig) { - final HostnameVerifier defaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); - final List trusted_fingerprints = hsConfig.getAllowedFingerprints(); - - return new HostnameVerifier() { - @Override - public boolean verify(String hostname, SSLSession session) { - if (defaultVerifier.verify(hostname, session)) return true; - if (trusted_fingerprints == null || trusted_fingerprints.size() == 0) return false; - - // If remote cert matches an allowed fingerprint, just accept it. - try { - for (Certificate cert : session.getPeerCertificates()) { - for (Fingerprint allowedFingerprint : trusted_fingerprints) { - if (allowedFingerprint != null && cert instanceof X509Certificate && allowedFingerprint.matchesCert((X509Certificate) cert)) { - return true; - } - } - } - } catch (SSLPeerUnverifiedException e) { - return false; - } catch (CertificateException e) { - return false; - } - - return false; - } - }; - } - - /** - * Create a list of accepted TLS specifications for a hs config. - * - * @param hsConfig the hs config. - * @param url the url of the end point, used to check if we have to enable CLEARTEXT communication. - * @return a list of accepted TLS specifications. - */ - public static List newConnectionSpecs(@NonNull HomeServerConnectionConfig hsConfig, @NonNull String url) { - final ConnectionSpec.Builder builder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS); - - final List tlsVersions = hsConfig.getAcceptedTlsVersions(); - if (null != tlsVersions) { - builder.tlsVersions(tlsVersions.toArray(new TlsVersion[0])); - } - - final List tlsCipherSuites = hsConfig.getAcceptedTlsCipherSuites(); - if (null != tlsCipherSuites) { - builder.cipherSuites(tlsCipherSuites.toArray(new CipherSuite[0])); - } - - builder.supportsTlsExtensions(hsConfig.shouldAcceptTlsExtensions()); - - List list = new ArrayList<>(); - - list.add(builder.build()); - - if (url.startsWith("http://")) { - list.add(ConnectionSpec.CLEARTEXT); - } - - return list; - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/Fingerprint.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/Fingerprint.java index 2bfe7d6e32..bee345e42f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/Fingerprint.java +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/Fingerprint.java @@ -21,8 +21,6 @@ import android.util.Base64; import org.json.JSONException; import org.json.JSONObject; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; import java.util.Arrays; /* @@ -40,20 +38,10 @@ public class Fingerprint { private final HashType mHashType; private final byte[] mBytes; - private String mDisplayableHexRepr; public Fingerprint(HashType hashType, byte[] bytes) { mHashType = hashType; mBytes = bytes; - mDisplayableHexRepr = null; - } - - public static Fingerprint newSha256Fingerprint(X509Certificate cert) throws CertificateException { - return new Fingerprint(HashType.SHA256, CertUtil.generateSha256Fingerprint(cert)); - } - - public static Fingerprint newSha1Fingerprint(X509Certificate cert) throws CertificateException { - return new Fingerprint(HashType.SHA1, CertUtil.generateSha1Fingerprint(cert)); } public HashType getType() { @@ -64,14 +52,6 @@ public class Fingerprint { return mBytes; } - public String getBytesAsHexString() { - if (mDisplayableHexRepr == null) { - mDisplayableHexRepr = CertUtil.fingerprintToHexString(mBytes); - } - - return mDisplayableHexRepr; - } - public JSONObject toJson() throws JSONException { JSONObject obj = new JSONObject(); obj.put("bytes", Base64.encodeToString(getBytes(), Base64.DEFAULT)); @@ -95,24 +75,6 @@ public class Fingerprint { return new Fingerprint(hashType, fingerprintBytes); } - public boolean matchesCert(X509Certificate cert) throws CertificateException { - Fingerprint o = null; - switch (mHashType) { - case SHA256: - o = Fingerprint.newSha256Fingerprint(cert); - break; - case SHA1: - o = Fingerprint.newSha1Fingerprint(cert); - break; - } - - return equals(o); - } - - public String toString() { - return String.format("Fingerprint{type: '%s', fingeprint: '%s'}", mHashType.toString(), getBytesAsHexString()); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/PinnedTrustManager.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/PinnedTrustManager.java deleted file mode 100644 index e914bfb724..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/PinnedTrustManager.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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.legacy.riot; - -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.List; - -import javax.annotation.Nullable; -import javax.net.ssl.X509TrustManager; - -/* - * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose - */ - -/** - * Implements a TrustManager that checks Certificates against an explicit list of known - * fingerprints. - */ -public class PinnedTrustManager implements X509TrustManager { - private final List mFingerprints; - @Nullable - private final X509TrustManager mDefaultTrustManager; - - /** - * @param fingerprints An array of SHA256 cert fingerprints - * @param defaultTrustManager Optional trust manager to fall back on if cert does not match - * any of the fingerprints. Can be null. - */ - public PinnedTrustManager(List fingerprints, @Nullable X509TrustManager defaultTrustManager) { - mFingerprints = fingerprints; - mDefaultTrustManager = defaultTrustManager; - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String s) throws CertificateException { - try { - if (mDefaultTrustManager != null) { - mDefaultTrustManager.checkClientTrusted( - chain, s - ); - return; - } - } catch (CertificateException e) { - // If there is an exception we fall back to checking fingerprints - if (mFingerprints == null || mFingerprints.size() == 0) { - throw new UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.getCause()); - } - } - checkTrusted("client", chain); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String s) throws CertificateException { - try { - if (mDefaultTrustManager != null) { - mDefaultTrustManager.checkServerTrusted( - chain, s - ); - return; - } - } catch (CertificateException e) { - // If there is an exception we fall back to checking fingerprints - if (mFingerprints == null || mFingerprints.isEmpty()) { - throw new UnrecognizedCertificateException(chain[0], Fingerprint.newSha256Fingerprint(chain[0]), e.getCause()); - } - } - checkTrusted("server", chain); - } - - private void checkTrusted(String type, X509Certificate[] chain) throws CertificateException { - X509Certificate cert = chain[0]; - - boolean found = false; - if (mFingerprints != null) { - for (Fingerprint allowedFingerprint : mFingerprints) { - if (allowedFingerprint != null && allowedFingerprint.matchesCert(cert)) { - found = true; - break; - } - } - } - - if (!found) { - throw new UnrecognizedCertificateException(cert, Fingerprint.newSha256Fingerprint(cert), null); - } - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/TLSSocketFactory.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/TLSSocketFactory.java deleted file mode 100644 index 6a5921a82d..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/TLSSocketFactory.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.legacy.riot; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; - -import okhttp3.TlsVersion; -import timber.log.Timber; - -/* - * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose - */ - -/** - * Force the usage of Tls versions on every created socket - * Inspired from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/ - */ -/*package*/ class TLSSocketFactory extends SSLSocketFactory { - private SSLSocketFactory internalSSLSocketFactory; - - private String[] enabledProtocols; - - /** - * Constructor - * - * @param trustPinned - * @param acceptedTlsVersions - * @throws KeyManagementException - * @throws NoSuchAlgorithmException - */ - /*package*/ TLSSocketFactory(TrustManager[] trustPinned, List acceptedTlsVersions) throws KeyManagementException, NoSuchAlgorithmException { - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, trustPinned, new SecureRandom()); - internalSSLSocketFactory = context.getSocketFactory(); - - enabledProtocols = new String[acceptedTlsVersions.size()]; - int i = 0; - for (TlsVersion tlsVersion : acceptedTlsVersions) { - enabledProtocols[i] = tlsVersion.javaName(); - i++; - } - } - - @Override - public String[] getDefaultCipherSuites() { - return internalSSLSocketFactory.getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return internalSSLSocketFactory.getSupportedCipherSuites(); - } - - @Override - public Socket createSocket() throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); - } - - @Override - public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); - } - - @Override - public Socket createSocket(String host, int port) throws IOException, UnknownHostException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); - } - - @Override - public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { - return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); - } - - private Socket enableTLSOnSocket(Socket socket) { - if (socket != null && (socket instanceof SSLSocket)) { - SSLSocket sslSocket = (SSLSocket) socket; - - List supportedProtocols = Arrays.asList(sslSocket.getSupportedProtocols()); - List filteredEnabledProtocols = new ArrayList<>(); - - for (String protocol : enabledProtocols) { - if (supportedProtocols.contains(protocol)) { - filteredEnabledProtocols.add(protocol); - } - } - - if (!filteredEnabledProtocols.isEmpty()) { - try { - sslSocket.setEnabledProtocols(filteredEnabledProtocols.toArray(new String[filteredEnabledProtocols.size()])); - } catch (Exception e) { - Timber.e(e, "Exception"); - } - } - } - return socket; - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/UnrecognizedCertificateException.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/UnrecognizedCertificateException.java deleted file mode 100644 index 518989a272..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/riot/UnrecognizedCertificateException.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.legacy.riot; - -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -/* - * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose - */ - -/** - * Thrown when we are given a certificate that does match the certificate we were told to - * expect. - */ -public class UnrecognizedCertificateException extends CertificateException { - private final X509Certificate mCert; - private final Fingerprint mFingerprint; - - public UnrecognizedCertificateException(X509Certificate cert, Fingerprint fingerprint, Throwable cause) { - super("Unrecognized certificate with unknown fingerprint: " + cert.getSubjectDN(), cause); - mCert = cert; - mFingerprint = fingerprint; - } - - public X509Certificate getCertificate() { - return mCert; - } - - public Fingerprint getFingerprint() { - return mFingerprint; - } -} 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..2ba058d0db 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..de41d168fd 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_DEFAULT_TRUST_MANAGER = true + @Throws(CertificateException::class) override fun checkClientTrusted(chain: Array, s: String) { try { - if (defaultTrustManager != null) { + if (defaultTrustManager != null && USE_DEFAULT_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_DEFAULT_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/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index 0cdd39f117..107ef6a351 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -26,7 +26,7 @@ import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachmen import im.vector.matrix.android.internal.di.CacheDirectory import im.vector.matrix.android.internal.di.ExternalFilesDirectory import im.vector.matrix.android.internal.di.SessionCacheDirectory -import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @@ -49,7 +49,7 @@ internal class DefaultFileService @Inject constructor( @SessionCacheDirectory private val sessionCacheDirectory: File, private val contentUrlResolver: ContentUrlResolver, - @Unauthenticated + @UnauthenticatedWithCertificate private val okHttpClient: OkHttpClient, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val taskExecutor: TaskExecutor 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..f084bec924 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 @@ -48,17 +48,18 @@ import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionFilesDirectory import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate 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 @@ -183,29 +184,34 @@ internal abstract class SessionModule { .build() } + @JvmStatic + @Provides + @SessionScope + @UnauthenticatedWithCertificate + fun providesOkHttpClientWithCertificate(@Unauthenticated okHttpClient: OkHttpClient, + homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { + return okHttpClient + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } + @JvmStatic @Provides @SessionScope @Authenticated - fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, + fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient, @Authenticated accessTokenProvider: AccessTokenProvider, @SessionId sessionId: String, @MockHttpInterceptor testInterceptor: TestInterceptor?): OkHttpClient { - return okHttpClient.newBuilder() + return okHttpClient + .newBuilder() + .addAccessTokenInterceptor(accessTokenProvider) .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/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt index 8b2f371317..b26bbe7c5c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.homeserver import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.internal.auth.version.Versions @@ -43,6 +44,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( private val eventBus: EventBus, private val getWellknownTask: GetWellknownTask, private val configExtractor: IntegrationManagerConfigExtractor, + private val homeServerConnectionConfig: HomeServerConnectionConfig, @UserId private val userId: String ) : GetHomeServerCapabilitiesTask { @@ -78,7 +80,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( }.getOrNull() val wellknownResult = runCatching { - getWellknownTask.execute(GetWellknownTask.Params(userId)) + getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig)) }.getOrNull() insertInDb(capabilities, uploadCapabilities, versions, wellknownResult) 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 4afd045d0f..3f10bf791c 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 @@ -36,7 +36,7 @@ import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.di.AuthenticatedIdentity -import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate import im.vector.matrix.android.internal.extensions.observeNotNull import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.session.SessionLifecycleObserver @@ -68,7 +68,7 @@ internal class DefaultIdentityService @Inject constructor( private val identityPingTask: IdentityPingTask, private val identityDisconnectTask: IdentityDisconnectTask, private val identityRequestTokenForBindingTask: IdentityRequestTokenForBindingTask, - @Unauthenticated + @UnauthenticatedWithCertificate private val unauthenticatedOkHttpClient: Lazy, @AuthenticatedIdentity private val okHttpClient: Lazy, 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..9f902f79f1 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 @@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthenticatedIdentity import im.vector.matrix.android.internal.di.IdentityDatabase import im.vector.matrix.android.internal.di.SessionFilesDirectory -import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor import im.vector.matrix.android.internal.network.token.AccessTokenProvider @@ -45,9 +45,12 @@ internal abstract class IdentityModule { @Provides @SessionScope @AuthenticatedIdentity - fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, + fun providesOkHttpClient(@UnauthenticatedWithCertificate okHttpClient: OkHttpClient, @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient { - return okHttpClient.addAccessTokenInterceptor(accessTokenProvider) + return okHttpClient + .newBuilder() + .addAccessTokenInterceptor(accessTokenProvider) + .build() } @JvmStatic diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt index 9111c5d5f1..1781fcc3dc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt @@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.terms.GetTermsResponse import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest @@ -41,7 +41,7 @@ import okhttp3.OkHttpClient import javax.inject.Inject internal class DefaultTermsService @Inject constructor( - @Unauthenticated + @UnauthenticatedWithCertificate private val unauthenticatedOkHttpClient: Lazy, private val accountDataDataSource: AccountDataDataSource, private val termsAPI: TermsAPI, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt index eee7e22134..a948101638 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt @@ -21,7 +21,7 @@ import dagger.Lazy import dagger.Module import dagger.Provides import im.vector.matrix.android.api.session.terms.TermsService -import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.session.SessionScope import okhttp3.OkHttpClient @@ -34,7 +34,7 @@ internal abstract class TermsModule { @Provides @JvmStatic @SessionScope - fun providesTermsAPI(@Unauthenticated unauthenticatedOkHttpClient: Lazy, + fun providesTermsAPI(@UnauthenticatedWithCertificate unauthenticatedOkHttpClient: Lazy, retrofitFactory: RetrofitFactory): TermsAPI { val retrofit = retrofitFactory.create(unauthenticatedOkHttpClient, "https://foo.bar") return retrofit.create(TermsAPI::class.java) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt index c6f6b8752d..8ded737c64 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt @@ -19,12 +19,14 @@ package im.vector.matrix.android.internal.wellknown import android.util.MalformedJsonException import dagger.Lazy import im.vector.matrix.android.api.MatrixPatterns +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.WellKnown import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.failure.Failure 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.session.homeserver.CapabilitiesAPI import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI import im.vector.matrix.android.internal.task.Task @@ -36,7 +38,8 @@ import javax.net.ssl.HttpsURLConnection internal interface GetWellknownTask : Task { data class Params( - val matrixId: String + val matrixId: String, + val homeServerConnectionConfig: HomeServerConnectionConfig? ) } @@ -56,7 +59,19 @@ internal class DefaultGetWellknownTask @Inject constructor( val homeServerDomain = params.matrixId.substringAfter(":") - return findClientConfig(homeServerDomain) + val client = buildClient(params.homeServerConnectionConfig) + return findClientConfig(homeServerDomain, client) + } + + private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig?): OkHttpClient { + return if (homeServerConnectionConfig != null) { + okHttpClient.get() + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } else { + okHttpClient.get() + } } /** @@ -68,8 +83,8 @@ internal class DefaultGetWellknownTask @Inject constructor( * * @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") */ - private suspend fun findClientConfig(domain: String): WellknownResult { - val wellKnownAPI = retrofitFactory.create(okHttpClient, "https://dummy.org") + private suspend fun findClientConfig(domain: String, client: OkHttpClient): WellknownResult { + val wellKnownAPI = retrofitFactory.create(client, "https://dummy.org") .create(WellKnownAPI::class.java) return try { @@ -84,7 +99,7 @@ internal class DefaultGetWellknownTask @Inject constructor( } else { if (homeServerBaseUrl.isValidUrl()) { // Check that HS is a real one - validateHomeServer(homeServerBaseUrl, wellKnown) + validateHomeServer(homeServerBaseUrl, wellKnown, client) } else { WellknownResult.FailError } @@ -113,8 +128,8 @@ internal class DefaultGetWellknownTask @Inject constructor( /** * Return true if home server is valid, and (if applicable) if identity server is pingable */ - private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown): WellknownResult { - val capabilitiesAPI = retrofitFactory.create(okHttpClient, homeServerBaseUrl) + private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown, client: OkHttpClient): WellknownResult { + val capabilitiesAPI = retrofitFactory.create(client, homeServerBaseUrl) .create(CapabilitiesAPI::class.java) try { @@ -135,7 +150,7 @@ internal class DefaultGetWellknownTask @Inject constructor( WellknownResult.FailError } else { if (identityServerBaseUrl.isValidUrl()) { - if (validateIdentityServer(identityServerBaseUrl)) { + if (validateIdentityServer(identityServerBaseUrl, client)) { // All is ok WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown) } else { @@ -151,8 +166,8 @@ internal class DefaultGetWellknownTask @Inject constructor( /** * Return true if identity server is pingable */ - private suspend fun validateIdentityServer(identityServerBaseUrl: String): Boolean { - val identityPingApi = retrofitFactory.create(okHttpClient, identityServerBaseUrl) + private suspend fun validateIdentityServer(identityServerBaseUrl: String, client: OkHttpClient): Boolean { + val identityPingApi = retrofitFactory.create(client, identityServerBaseUrl) .create(IdentityAuthAPI::class.java) return try { diff --git a/tools/check/check_code_quality.sh b/tools/check/check_code_quality.sh index 72c531bf39..8f850734fc 100755 --- a/tools/check/check_code_quality.sh +++ b/tools/check/check_code_quality.sh @@ -108,14 +108,22 @@ else chmod u+x ${checkLongFilesScript} fi -echo -echo "Search for long files..." +maxLines=2500 -${checkLongFilesScript} 2500 \ +echo +echo "Search for kotlin files with more than ${maxLines} lines..." + +${checkLongFilesScript} ${maxLines} \ + ./matrix-sdk-android/src/main/java \ + ./matrix-sdk-android-rx/src/main/java \ + ./vector/src/androidTest/java \ + ./vector/src/debug/java \ + ./vector/src/fdroid/java \ + ./vector/src/gplay/java \ ./vector/src/main/java \ - ./vector/src/main/res/layout \ - ./vector/src/main/res/values \ - ./vector/src/main/res/values-v21 \ + ./vector/src/release/java \ + ./vector/src/sharedTest/java \ + ./vector/src/test/java resultLongFiles=$? 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..af2c23848f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/UnrecognizedCertificateDialog.kt @@ -0,0 +1,170 @@ +/* + * 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() + } +} 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..fc970297d1 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 @@ -486,7 +496,8 @@ class LoginViewModel @AssistedInject constructor( ) } - authenticationService.getWellKnownData(action.username, object : MatrixCallback { + // TODO Handle certificate error in this case. Direct login is deactivated now, so we will handle that later + authenticationService.getWellKnownData(action.username, null, object : MatrixCallback { override fun onSuccess(data: WellknownResult) { when (data) { is WellknownResult.Prompt -> @@ -649,67 +660,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