From 8f2fe9955a862f7ae763f4a8237cd144ff9f4940 Mon Sep 17 00:00:00 2001 From: Lode Hoste Date: Thu, 9 Apr 2015 20:03:50 +0200 Subject: [PATCH] Enable TLS for the WebGUI --- proguard-android.txt | 5 ++ .../activities/WebGuiActivity.java | 84 ++++++++++++++++++- .../syncthingandroid/syncthing/RestApi.java | 4 + 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/proguard-android.txt b/proguard-android.txt index fb4c375e..5629c4d0 100644 --- a/proguard-android.txt +++ b/proguard-android.txt @@ -1,3 +1,8 @@ # Fix appcompat-v7 v21.0.0 causing crash on Samsung devices with Android v4.2.2 (https://code.google.com/p/android/issues/detail?id=78377) -keep class !android.support.v7.internal.view.menu.MenuBuilder, !android.support.v7.internal.view.menu.SubMenuBuilder, android.support.v7.** { *; } -keep interface android.support.v7.** { *; } + +# Enable reflective access to mX509Certificate +-keepclassmembers class android.net.http.SslCertificate { + private final X509Certificate mX509Certificate; +} diff --git a/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java b/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java index 636c72b1..0f5e4b81 100644 --- a/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java +++ b/src/main/java/com/nutomic/syncthingandroid/activities/WebGuiActivity.java @@ -2,30 +2,82 @@ package com.nutomic.syncthingandroid.activities; import android.annotation.SuppressLint; import android.content.ComponentName; +import android.graphics.Bitmap; +import android.net.http.SslCertificate; +import android.net.http.SslError; import android.os.Bundle; import android.os.IBinder; +import android.util.Log; import android.view.View; +import android.webkit.SslErrorHandler; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ProgressBar; import com.nutomic.syncthingandroid.R; +import com.nutomic.syncthingandroid.syncthing.RestApi; import com.nutomic.syncthingandroid.syncthing.SyncthingService; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + /** * Holds a WebView that shows the web ui of the local syncthing instance. */ -public class WebGuiActivity extends SyncthingActivity implements SyncthingService.OnWebGuiAvailableListener { +public class WebGuiActivity extends SyncthingActivity + implements SyncthingService.OnWebGuiAvailableListener { + + private static final String TAG = "WebGuiActivity"; private WebView mWebView; private View mLoadingView; + private X509Certificate mCaCert; + /** * Hides the loading screen and shows the WebView once it is fully loaded. */ private final WebViewClient mWebViewClient = new WebViewClient() { + /** + * Catch (self-signed) SSL errors and test if they correspond to Syncthing's certificate. + */ + @Override + public void onReceivedSslError (WebView view, SslErrorHandler handler, SslError error) { + try { + // Uses reflection to access the private mX509Certificate field of SslCertificate + SslCertificate sslCert = error.getCertificate(); + Field f = sslCert.getClass().getDeclaredField("mX509Certificate"); + f.setAccessible(true); + X509Certificate cert = (X509Certificate)f.get(sslCert); + if (cert == null) { + Log.w(TAG, "X509Certificate reference invalid"); + handler.cancel(); + return; + } + cert.verify(mCaCert.getPublicKey()); + handler.proceed(); + } catch (NoSuchFieldException|IllegalAccessException|CertificateException| + NoSuchAlgorithmException|InvalidKeyException|NoSuchProviderException| + SignatureException e) { + Log.w(TAG, e); + handler.cancel(); + } + } + @Override public void onPageFinished(WebView view, String url) { mWebView.setVisibility(View.VISIBLE); @@ -50,6 +102,8 @@ public class WebGuiActivity extends SyncthingActivity implements SyncthingServic ProgressBar pb = (ProgressBar) mLoadingView.findViewById(R.id.progress); pb.setIndeterminate(true); + loadCaCert(); + mWebView = (WebView) findViewById(R.id.webview); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.setWebViewClient(mWebViewClient); @@ -63,10 +117,36 @@ public class WebGuiActivity extends SyncthingActivity implements SyncthingServic /** * Loads and shows WebView, hides loading view. + * + * Sets the X-API-Key (HEADER_API_KEY) header for authorization */ @Override public void onWebGuiAvailable() { - mWebView.loadUrl(getService().getWebGuiUrl()); + Map extraHeaders = new HashMap<>(); + extraHeaders.put(RestApi.HEADER_API_KEY, getService().getApi().getApiKey()); + mWebView.loadUrl(getService().getWebGuiUrl(), extraHeaders); } + /** + * Reads the SyncthingService.HTTPS_CERT_FILE Ca Cert key and loads it in memory + */ + private void loadCaCert() { + InputStream inStream = null; + try { + String httpsCertPath = getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE; + inStream = new FileInputStream(httpsCertPath); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + mCaCert = (X509Certificate) + cf.generateCertificate(inStream); + } catch (FileNotFoundException|CertificateException e) { + throw new IllegalArgumentException("Untrusted Certificate"); + } finally { + try { + if (inStream != null) + inStream.close(); + } catch (IOException e) { + Log.w(TAG, e); + } + } + } } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java index a8bb70dd..75094c57 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java @@ -953,4 +953,8 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, return new Device(); } + public String getApiKey() { + return mApiKey; + } + }