Browse Source

Start working on untrusted certs

Signed-off-by: Mario Danic <mario@lovelyhq.com>
Mario Danic 7 years ago
parent
commit
7f12da21f7

+ 2 - 0
app/build.gradle

@@ -1,4 +1,5 @@
 apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
 apply plugin: 'eu.davidea.grabver'
 
 versioning {
@@ -118,6 +119,7 @@ dependencies {
     compile 'com.github.bumptech.glide:okhttp3-integration:4.2.0@aar'
 
     implementation 'org.webrtc:google-webrtc:1.0.+'
+    implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}"
 
     testImplementation 'junit:junit:4.12'
     androidTestImplementation ('com.android.support.test.espresso:espresso-core:3.0.1', {

+ 27 - 1
app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java

@@ -21,6 +21,7 @@
 package com.nextcloud.talk.controllers;
 
 import android.content.pm.ActivityInfo;
+import android.net.http.SslCertificate;
 import android.net.http.SslError;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
@@ -43,8 +44,12 @@ import com.nextcloud.talk.models.LoginData;
 import com.nextcloud.talk.utils.bundle.BundleBuilder;
 import com.nextcloud.talk.utils.bundle.BundleKeys;
 import com.nextcloud.talk.utils.database.user.UserUtils;
+import com.nextcloud.talk.utils.ssl.MagicTrustManager;
 
+import java.lang.reflect.Field;
 import java.net.URLDecoder;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -68,6 +73,8 @@ public class WebViewLoginController extends BaseController {
     UserUtils userUtils;
     @Inject
     ReactiveEntityStore<Persistable> dataStore;
+    @Inject
+    MagicTrustManager magicTrustManager;
 
     @BindView(R.id.webview)
     WebView webView;
@@ -149,7 +156,26 @@ public class WebViewLoginController extends BaseController {
 
             @Override
             public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
-                super.onReceivedSslError(view, handler, error);
+                try {
+                    SslCertificate sslCertificate = error.getCertificate();
+                    Field f = sslCertificate.getClass().getDeclaredField("mX509Certificate");
+                    f.setAccessible(true);
+                    X509Certificate cert = (X509Certificate)f.get(sslCertificate);
+
+                    if (cert == null) {
+                        handler.cancel();
+                    } else {
+                        try {
+                            magicTrustManager.checkServerTrusted(new X509Certificate[]{cert}, "generic");
+                            handler.proceed();
+                        } catch (CertificateException exception) {
+                            // cancel for now, as we don't have a way to accept custom certificates
+                            handler.cancel();
+                        }
+                    }
+                } catch (Exception exception) {
+                    handler.cancel();
+                }
             }
 
             public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {

+ 21 - 1
app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java

@@ -31,6 +31,8 @@ import com.nextcloud.talk.api.helpers.api.ApiHelper;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.utils.preferences.AppPreferences;
 import com.nextcloud.talk.utils.preferences.json.ProxyPrefs;
+import com.nextcloud.talk.utils.ssl.MagicTrustManager;
+import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat;
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
@@ -49,6 +51,7 @@ import okhttp3.OkHttpClient;
 import okhttp3.Request;
 import okhttp3.Response;
 import okhttp3.Route;
+import okhttp3.internal.tls.OkHostnameVerifier;
 import okhttp3.logging.HttpLoggingInterceptor;
 import retrofit2.Retrofit;
 import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
@@ -93,7 +96,21 @@ public class RestModule {
 
     @Provides
     @Singleton
-    OkHttpClient provideHttpClient(Proxy proxy, AppPreferences appPreferences) {
+    MagicTrustManager provideMagicTrustManager() {
+        return new MagicTrustManager();
+
+    }
+
+    @Provides
+    @Singleton
+    SSLSocketFactoryCompat provideSslSocketFactoryCompat(MagicTrustManager magicTrustManager) {
+        return new SSLSocketFactoryCompat(magicTrustManager);
+    }
+
+    @Provides
+    @Singleton
+    OkHttpClient provideHttpClient(Proxy proxy, AppPreferences appPreferences,
+                                   MagicTrustManager magicTrustManager, SSLSocketFactoryCompat sslSocketFactoryCompat) {
         OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
 
         int cacheSize = 128 * 1024 * 1024; // 128 MB
@@ -107,6 +124,9 @@ public class RestModule {
             httpClient.addInterceptor(loggingInterceptor);
         }
 
+        httpClient.sslSocketFactory(sslSocketFactoryCompat, magicTrustManager);
+        httpClient.hostnameVerifier(OkHostnameVerifier.INSTANCE);
+
         if (!Proxy.NO_PROXY.equals(proxy)) {
             httpClient.proxy(proxy);
 

+ 133 - 0
app/src/main/java/com/nextcloud/talk/utils/ssl/MagicTrustManager.java

@@ -0,0 +1,133 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Influenced by https://gitlab.com/bitfireAT/cert4android/blob/master/src/main/java/at/bitfire/cert4android/CustomCertService.kt
+ */
+
+package com.nextcloud.talk.utils.ssl;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.nextcloud.talk.application.NextcloudTalkApplication;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+public class MagicTrustManager implements X509TrustManager {
+    private static final String TAG = "MagicTrustManager";
+
+    private File keystoreFile;
+    private X509TrustManager systemTrustManager = null;
+    private KeyStore trustedKeyStore = null;
+
+    public MagicTrustManager() {
+        keystoreFile = new File(NextcloudTalkApplication.getSharedApplication().getDir("CertsKeystore",
+                Context.MODE_PRIVATE), "keystore.bks");
+        try {
+            trustedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+            FileInputStream fileInputStream = new FileInputStream(keystoreFile);
+            trustedKeyStore.load(fileInputStream, null);
+        } catch (Exception exception) {
+            try {
+                trustedKeyStore.load(null, null);
+            } catch (Exception e) {
+                Log.d(TAG, "Failed to create in-memory key store " + e.getLocalizedMessage());
+            }
+        }
+
+        TrustManagerFactory trustManagerFactory = null;
+        try {
+            trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.
+                    getDefaultAlgorithm());
+
+            trustManagerFactory.init((KeyStore) null);
+
+            for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
+                if (trustManager instanceof X509TrustManager) {
+                    systemTrustManager = (X509TrustManager) trustManager;
+                    break;
+                }
+            }
+
+        } catch (Exception exception) {
+            Log.d(TAG, "Failed to load default trust manager " + exception.getLocalizedMessage());
+        }
+
+    }
+
+    public boolean isCertInTrustStore(X509Certificate x509Certificate) {
+        if (systemTrustManager != null) {
+            try {
+                systemTrustManager.checkServerTrusted(new X509Certificate[]{x509Certificate}, "generic");
+                return true;
+            } catch (CertificateException e) {
+                if (trustedKeyStore != null) {
+                    try {
+                        if (trustedKeyStore.getCertificateAlias(x509Certificate) != null) {
+                            return true;
+                        }
+                    } catch (KeyStoreException exception) {
+                        return false;
+                    }
+                }
+
+            }
+        }
+        return false;
+    }
+
+    public void addCertInTrustStore(X509Certificate x509Certificate) {
+        if (trustedKeyStore != null) {
+            try {
+                trustedKeyStore.setCertificateEntry(x509Certificate.getSubjectDN().getName(), x509Certificate);
+                FileOutputStream fileOutputStream = new FileOutputStream(keystoreFile);
+                trustedKeyStore.store(fileOutputStream, null);
+            } catch (Exception exception) {
+                Log.d(TAG, "Failed to set certificate entry " + exception.getLocalizedMessage());
+            }
+        }
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
+        Log.d(TAG, "We don't validate client certificates just yet");
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
+        if (!isCertInTrustStore(x509Certificates[0])) {
+            throw new CertificateException();
+        }
+    }
+
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+        return new X509Certificate[0];
+    }
+}

+ 153 - 0
app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt

@@ -0,0 +1,153 @@
+/*
+ * Copyright © Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+
+package com.nextcloud.talk.utils.ssl
+
+import android.os.Build
+import java.io.IOException
+import java.net.InetAddress
+import java.net.Socket
+import java.security.GeneralSecurityException
+import java.util.*
+import javax.net.ssl.SSLContext
+import javax.net.ssl.SSLSocket
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
+
+class SSLSocketFactoryCompat(trustManager: X509TrustManager): SSLSocketFactory() {
+
+    private var delegate: SSLSocketFactory
+
+    companion object {
+        // Android 5.0+ (API level 21) provides reasonable default settings
+        // but it still allows SSLv3
+        // https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
+        var protocols: Array<String>? = null
+        var cipherSuites: Array<String>? = null
+        init {
+            if (Build.VERSION.SDK_INT >= 23) {
+                // Since Android 6.0 (API level 23),
+                // - TLSv1.1 and TLSv1.2 is enabled by default
+                // - SSLv3 is disabled by default
+                // - all modern ciphers are activated by default
+                protocols = null
+                cipherSuites = null
+            } else {
+                val socket = SSLSocketFactory.getDefault().createSocket() as SSLSocket?
+                try {
+                    socket?.let {
+                        /* set reasonable protocol versions */
+                        // - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
+                        // - remove all SSL versions (especially SSLv3) because they're insecure now
+                        val _protocols = LinkedList<String>()
+                        for (protocol in socket.supportedProtocols.filterNot { it.contains("SSL", true) })
+                            _protocols += protocol
+                        protocols = _protocols.toTypedArray()
+
+                        /* set up reasonable cipher suites */
+                        val knownCiphers = arrayOf<String>(
+                                // TLS 1.2
+                                "TLS_RSA_WITH_AES_256_GCM_SHA384",
+                                "TLS_RSA_WITH_AES_128_GCM_SHA256",
+                                "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
+                                "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
+                                "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
+                                "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
+                                "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+                                // maximum interoperability
+                                "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
+                                "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
+                                "TLS_RSA_WITH_AES_128_CBC_SHA",
+                                // additionally
+                                "TLS_RSA_WITH_AES_256_CBC_SHA",
+                                "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
+                                "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
+                                "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
+                                "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
+                        )
+                        val availableCiphers = socket.supportedCipherSuites
+
+                        /* For maximum security, preferredCiphers should *replace* enabled ciphers (thus
+                         * disabling ciphers which are enabled by default, but have become unsecure), but for
+                         * the security level of DAVdroid and maximum compatibility, disabling of insecure
+                         * ciphers should be a server-side task */
+
+                        // for the final set of enabled ciphers, take the ciphers enabled by default, ...
+                        val _cipherSuites = LinkedList<String>()
+                        _cipherSuites.addAll(socket.enabledCipherSuites)
+                        // ... add explicitly allowed ciphers ...
+                        _cipherSuites.addAll(knownCiphers)
+                        // ... and keep only those which are actually available
+                        _cipherSuites.retainAll(availableCiphers)
+
+                        cipherSuites = _cipherSuites.toTypedArray()
+                    }
+                } catch(e: IOException) {
+                } finally {
+                    socket?.close()     // doesn't implement Closeable on all supported Android versions
+                }
+            }
+        }
+    }
+
+
+    init {
+        try {
+            val sslContext = SSLContext.getInstance("TLS")
+            sslContext.init(null, arrayOf(trustManager), null)
+            delegate = sslContext.socketFactory
+        } catch (e: GeneralSecurityException) {
+            throw IllegalStateException()      // system has no TLS
+        }
+    }
+
+    override fun getDefaultCipherSuites(): Array<String>? = cipherSuites ?: delegate.defaultCipherSuites
+    override fun getSupportedCipherSuites(): Array<String>? = cipherSuites ?: delegate.supportedCipherSuites
+
+    override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
+        val ssl = delegate.createSocket(s, host, port, autoClose)
+        if (ssl is SSLSocket)
+            upgradeTLS(ssl)
+        return ssl
+    }
+
+    override fun createSocket(host: String, port: Int): Socket {
+        val ssl = delegate.createSocket(host, port)
+        if (ssl is SSLSocket)
+            upgradeTLS(ssl)
+        return ssl
+    }
+
+    override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
+        val ssl = delegate.createSocket(host, port, localHost, localPort)
+        if (ssl is SSLSocket)
+            upgradeTLS(ssl)
+        return ssl
+    }
+
+    override fun createSocket(host: InetAddress, port: Int): Socket {
+        val ssl = delegate.createSocket(host, port)
+        if (ssl is SSLSocket)
+            upgradeTLS(ssl)
+        return ssl
+    }
+
+    override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
+        val ssl = delegate.createSocket(address, port, localAddress, localPort)
+        if (ssl is SSLSocket)
+            upgradeTLS(ssl)
+        return ssl
+    }
+
+
+    private fun upgradeTLS(ssl: SSLSocket) {
+        protocols?.let { ssl.enabledProtocols = it }
+        cipherSuites?.let { ssl.enabledCipherSuites = it }
+    }
+
+}

+ 6 - 1
build.gradle

@@ -1,7 +1,11 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 
 buildscript {
-    
+
+    ext {
+        kotlinVersion = '1.1.51'
+    }
+
     repositories {
         google()
         jcenter()
@@ -10,6 +14,7 @@ buildscript {
     dependencies {
         classpath 'com.android.tools.build:gradle:3.0.0'
         classpath 'eu.davidea:grabver:0.6.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files