Start working on untrusted certs

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2017-10-27 23:10:06 +02:00
parent 2b25be2a54
commit 7f12da21f7
6 changed files with 342 additions and 3 deletions

View File

@ -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', {

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,10 @@
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