From 7f12da21f7bb3437c9eba85bb803a9afa6d31f28 Mon Sep 17 00:00:00 2001 From: Mario Danic Date: Fri, 27 Oct 2017 23:10:06 +0200 Subject: [PATCH] Start working on untrusted certs Signed-off-by: Mario Danic --- app/build.gradle | 2 + .../controllers/WebViewLoginController.java | 28 +++- .../talk/dagger/modules/RestModule.java | 22 ++- .../talk/utils/ssl/MagicTrustManager.java | 133 +++++++++++++++ .../talk/utils/ssl/SSLSocketFactoryCompat.kt | 153 ++++++++++++++++++ build.gradle | 7 +- 6 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ssl/MagicTrustManager.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt diff --git a/app/build.gradle b/app/build.gradle index 496bafba6..a3b74dd9d 100644 --- a/app/build.gradle +++ b/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', { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java b/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java index 9e279d062..9694f6163 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java +++ b/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 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) { diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java index f7e0de9a8..237abfbf2 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java +++ b/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); diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/MagicTrustManager.java b/app/src/main/java/com/nextcloud/talk/utils/ssl/MagicTrustManager.java new file mode 100644 index 000000000..d034b322c --- /dev/null +++ b/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 + * + * 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 . + * + * 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]; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt b/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt new file mode 100644 index 000000000..282bba40d --- /dev/null +++ b/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? = null + var cipherSuites: Array? = 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() + for (protocol in socket.supportedProtocols.filterNot { it.contains("SSL", true) }) + _protocols += protocol + protocols = _protocols.toTypedArray() + + /* set up reasonable cipher suites */ + val knownCiphers = arrayOf( + // 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() + _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? = cipherSuites ?: delegate.defaultCipherSuites + override fun getSupportedCipherSuites(): Array? = 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 } + } + +} diff --git a/build.gradle b/build.gradle index 0bb59a3a1..c57b4175b 100644 --- a/build.gradle +++ b/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