diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/GetTaskTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/GetTaskTest.java index 86e86a8d..8743cddd 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/GetTaskTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/GetTaskTest.java @@ -43,7 +43,7 @@ public class GetTaskTest extends AndroidTestCase { @MediumTest public void testGetNoParams() throws IOException, InterruptedException { - new GetTask() { + new GetTask("") { @Override protected void onPostExecute(String s) { assertEquals(RESPONSE, s); @@ -57,7 +57,7 @@ public class GetTaskTest extends AndroidTestCase { @MediumTest public void testGetParams() throws IOException, InterruptedException { - new GetTask() { + new GetTask("") { @Override protected void onPostExecute(String s) { assertEquals(RESPONSE, s); diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/PollWebGuiAvailableTaskTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/PollWebGuiAvailableTaskTest.java index 042ecbf9..9d42a95b 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/PollWebGuiAvailableTaskTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/PollWebGuiAvailableTaskTest.java @@ -33,8 +33,10 @@ public class PollWebGuiAvailableTaskTest extends AndroidTestCase { new SyncthingRunnable(new MockContext(null), getContext().getApplicationInfo().dataDir + "/" + SyncthingService.BINARY_NAME); + String httpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE; + final CountDownLatch latch = new CountDownLatch(1); - new PollWebGuiAvailableTask() { + new PollWebGuiAvailableTask(httpsCertPath) { @Override protected void onPostExecute(Void aVoid) { latch.countDown(); diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java index 22d97e92..58b79a2a 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/syncthing/RestApiTest.java @@ -23,6 +23,8 @@ public class RestApiTest extends AndroidTestCase { private RestApi mApi; + private String mHttpsCertPath; + @Override protected void setUp() throws Exception { super.setUp(); @@ -33,8 +35,10 @@ public class RestApiTest extends AndroidTestCase { mConfig = new ConfigXml(new MockContext(getContext())); mConfig.changeDefaultFolder(); + mHttpsCertPath = getContext().getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE; + final CountDownLatch latch = new CountDownLatch(2); - new PollWebGuiAvailableTask() { + new PollWebGuiAvailableTask(mHttpsCertPath) { @Override protected void onPostExecute(Void aVoid) { mApi.onWebGuiAvailable(); diff --git a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java index 94662cb7..a109d026 100644 --- a/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java +++ b/src/androidTest/java/com/nutomic/syncthingandroid/test/util/ConfigXmlTest.java @@ -36,7 +36,7 @@ public class ConfigXmlTest extends AndroidTestCase { @SmallTest public void testGetWebGuiUrl() { - assertTrue(mConfig.getWebGuiUrl().startsWith("http://127.0.0.1:")); + assertTrue(mConfig.getWebGuiUrl().startsWith("https://127.0.0.1:")); } /** diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java index 8695abd7..cb7b8a9d 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java @@ -3,13 +3,14 @@ package com.nutomic.syncthingandroid.syncthing; import android.os.AsyncTask; import android.util.Log; +import com.nutomic.syncthingandroid.util.Https; + import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HTTP; @@ -40,6 +41,12 @@ public class GetTask extends AsyncTask { public static final String URI_DEVICEID = "/rest/deviceid"; + private String mHttpsCertPath; + + public GetTask(String httpsCertPath) { + mHttpsCertPath = httpsCertPath; + } + /** * params[0] Syncthing hostname * params[1] URI to call @@ -52,7 +59,7 @@ public class GetTask extends AsyncTask { // Retry at most 10 times before failing for (int i = 0; i < 10; i++) { String fullUri = params[0] + params[1]; - HttpClient httpclient = new DefaultHttpClient(); + HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath); if (params.length == 5) { LinkedList urlParams = new LinkedList<>(); urlParams.add(new BasicNameValuePair(params[3], params[4])); @@ -77,7 +84,7 @@ public class GetTask extends AsyncTask { br.close(); return result; } - } catch (IOException e) { + } catch (IOException|IllegalArgumentException e) { Log.w(TAG, "Failed to call Rest API at " + fullUri, e); } try { diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/PollWebGuiAvailableTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/PollWebGuiAvailableTask.java index 829381e9..7f44c557 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/PollWebGuiAvailableTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/PollWebGuiAvailableTask.java @@ -4,12 +4,13 @@ package com.nutomic.syncthingandroid.syncthing; import android.os.AsyncTask; import android.util.Log; +import com.nutomic.syncthingandroid.util.Https; + import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpHead; import org.apache.http.conn.HttpHostConnectException; -import org.apache.http.impl.client.DefaultHttpClient; import java.io.IOException; @@ -27,13 +28,19 @@ public abstract class PollWebGuiAvailableTask extends AsyncTask { public static final String URI_SCAN = "/rest/scan"; + private String mHttpsCertPath; + + public PostTask(String httpsCertPath) { + mHttpsCertPath = httpsCertPath; + } + /** * params[0] Syncthing hostname * params[1] URI to call @@ -32,7 +39,7 @@ public class PostTask extends AsyncTask { @Override protected Boolean doInBackground(String... params) { String fullUri = params[0] + params[1]; - HttpClient httpclient = new DefaultHttpClient(); + HttpClient httpclient = Https.createHttpsClient(mHttpsCertPath); HttpPost post = new HttpPost(fullUri); post.addHeader(new BasicHeader(RestApi.HEADER_API_KEY, params[2])); @@ -41,7 +48,7 @@ public class PostTask extends AsyncTask { post.setEntity(new StringEntity(params[3], HTTP.UTF_8)); } httpclient.execute(post); - } catch (IOException e) { + } catch (IOException|IllegalArgumentException e) { Log.w(TAG, "Failed to call Rest API at " + fullUri, e); return false; } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java index b173f775..a8bb70dd 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java @@ -151,7 +151,9 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, private final String mUrl; - private String mApiKey; + private final String mApiKey; + + private final String mHttpsCertPath; private JSONObject mConfig; @@ -180,6 +182,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, mContext = context; mUrl = url; mApiKey = apiKey; + mHttpsCertPath = mContext.getFilesDir() + "/" + SyncthingService.HTTPS_CERT_FILE; mOnApiAvailableListener = listener; } @@ -197,7 +200,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, public void onApiAvailable(); } - private OnApiAvailableListener mOnApiAvailableListener; + private final OnApiAvailableListener mOnApiAvailableListener; /** * Gets local device id, syncthing version and config, then calls all OnApiAvailableListeners. @@ -205,7 +208,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, @Override public void onWebGuiAvailable() { mAvailableCount.set(0); - new GetTask() { + new GetTask(mHttpsCertPath) { @Override protected void onPostExecute(String s) { try { @@ -219,7 +222,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, } } }.execute(mUrl, GetTask.URI_VERSION, mApiKey); - new GetTask() { + new GetTask(mHttpsCertPath) { @Override protected void onPostExecute(String config) { try { @@ -340,7 +343,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, */ @TargetApi(11) public void requireRestart(Activity activity) { - new PostTask().execute(mUrl, PostTask.URI_CONFIG, mApiKey, mConfig.toString()); + new PostTask(mHttpsCertPath).execute(mUrl, PostTask.URI_CONFIG, mApiKey, mConfig.toString()); if (mRestartPostponed) return; @@ -438,7 +441,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, * @param listener Callback invoked when the result is received. */ public void getSystemInfo(final OnReceiveSystemInfoListener listener) { - new GetTask() { + new GetTask(mHttpsCertPath) { @Override protected void onPostExecute(String s) { if (s == null) @@ -557,7 +560,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, * Use the key {@link #LOCAL_DEVICE_CONNECTIONS} to get connection info for the local device. */ public void getConnections(final OnReceiveConnectionsListener listener) { - new GetTask() { + new GetTask(mHttpsCertPath) { @Override protected void onPostExecute(String s) { if (s == null) @@ -650,7 +653,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, * Returns status information about the folder with the given ID. */ public void getModel(final String folderId, final OnReceiveModelListener listener) { - new GetTask() { + new GetTask(mHttpsCertPath) { @Override protected void onPostExecute(String s) { if (s == null) @@ -877,7 +880,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, * Normalizes a given device ID. */ public void normalizeDeviceId(String id, final OnDeviceIdNormalizedListener listener) { - new GetTask() { + new GetTask(mHttpsCertPath) { @Override protected void onPostExecute(String s) { super.onPostExecute(s); @@ -934,7 +937,7 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener, */ @Override public void onFolderFileChange(String folderId, String relativePath) { - new PostTask().execute(mUrl, PostTask.URI_SCAN, mApiKey, "folder", folderId, "sub", + new PostTask(mHttpsCertPath).execute(mUrl, PostTask.URI_SCAN, mApiKey, "folder", folderId, "sub", relativePath); } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java index 74e58f4f..4d9724f6 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java @@ -65,6 +65,11 @@ public class SyncthingService extends Service { */ public static final String PRIVATE_KEY_FILE = "key.pem"; + /** + * Name of the public HTTPS CA file in the data directory. + */ + public static final String HTTPS_CERT_FILE = "https-cert.pem"; + /** * Directory where config is exported to and imported from. */ @@ -195,7 +200,7 @@ public class SyncthingService extends Service { mConfig = new ConfigXml(SyncthingService.this); mCurrentState = State.STARTING; registerOnWebGuiAvailableListener(mApi); - new PollWebGuiAvailableTaskImpl().execute(mConfig.getWebGuiUrl()); + new PollWebGuiAvailableTaskImpl(getFilesDir() + "/" + HTTPS_CERT_FILE).execute(mConfig.getWebGuiUrl()); new Thread(new SyncthingRunnable( this, getApplicationInfo().dataDir + "/" + BINARY_NAME)).start(); Notification n = new NotificationCompat.Builder(this) @@ -357,6 +362,11 @@ public class SyncthingService extends Service { } private class PollWebGuiAvailableTaskImpl extends PollWebGuiAvailableTask { + + public PollWebGuiAvailableTaskImpl(String httpsCertPath) { + super(httpsCertPath); + } + @Override protected void onPostExecute(Void aVoid) { if (mStopScheduled) { diff --git a/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java b/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java index 9154d8d7..719792d7 100644 --- a/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java +++ b/src/main/java/com/nutomic/syncthingandroid/util/ConfigXml.java @@ -88,7 +88,7 @@ public class ConfigXml { } public String getWebGuiUrl() { - return "http://" + getGuiElement().getElementsByTagName("address").item(0).getTextContent(); + return "https://" + getGuiElement().getElementsByTagName("address").item(0).getTextContent(); } public String getApiKey() { diff --git a/src/main/java/com/nutomic/syncthingandroid/util/CustomX509TrustManager.java b/src/main/java/com/nutomic/syncthingandroid/util/CustomX509TrustManager.java new file mode 100644 index 00000000..2266b4de --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/util/CustomX509TrustManager.java @@ -0,0 +1,105 @@ +package com.nutomic.syncthingandroid.util; + +import android.util.Log; + +import org.apache.http.conn.ssl.SSLSocketFactory; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.security.InvalidKeyException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; + + +/* + * TrustManager allowing the Syncthing https.pem CA + * + * Based on http://stackoverflow.com/questions/16719959#16759793 + * + */ +public class CustomX509TrustManager implements X509TrustManager { + + private static final String TAG = "CustomX509TrustManager"; + + /** + * Taken from: http://janis.peisenieks.lv/en/76/english-making-an-ssl-connection-via-android/ + * + */ + public static class CustomSSLSocketFactory extends SSLSocketFactory { + SSLContext sslContext = SSLContext.getInstance("TLS"); + + public CustomSSLSocketFactory(SSLContext context) + throws KeyManagementException, NoSuchAlgorithmException, + KeyStoreException, UnrecoverableKeyException { + super(null); + sslContext = context; + } + + @Override + public Socket createSocket(Socket socket, String host, int port, + boolean autoClose) throws IOException { + return sslContext.getSocketFactory().createSocket(socket, host, port, + autoClose); + } + + @Override + public Socket createSocket() throws IOException { + return sslContext.getSocketFactory().createSocket(); + } + } + + private String mHttpsCertPath; + + public CustomX509TrustManager(String httpsCertPath) { + mHttpsCertPath = httpsCertPath; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + /** + * Verifies certs against public key of the local syncthing instance + */ + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] certs, + String authType) throws CertificateException { + InputStream inStream = null; + try { + inStream = new FileInputStream(mHttpsCertPath); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate ca = (X509Certificate) + cf.generateCertificate(inStream); + for (X509Certificate cert : certs) { + cert.verify(ca.getPublicKey()); + } + } catch (FileNotFoundException |NoSuchAlgorithmException|InvalidKeyException| + NoSuchProviderException |SignatureException e) { + throw new CertificateException("Untrusted Certificate!", e); + } finally { + try { + if (inStream != null) + inStream.close(); + } catch (IOException e) { + Log.w(TAG, e); + } + } + } + public X509Certificate[] getAcceptedIssuers() { + return null; + } +} diff --git a/src/main/java/com/nutomic/syncthingandroid/util/Https.java b/src/main/java/com/nutomic/syncthingandroid/util/Https.java new file mode 100644 index 00000000..17ec2d8c --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/util/Https.java @@ -0,0 +1,53 @@ +package com.nutomic.syncthingandroid.util; + +import android.util.Log; + +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; + +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +/* + * Wrapper for HTTPS Clients allowing the Syncthing https.pem CA + * + */ +public class Https { + + private static final String TAG = "HTTPS"; + + /** + * Create a HTTPClient that verifies a custom PEM certificate + * + * @param httpsCertPath refers to the filepath of a SSL/TLS PEM certificate. + */ + public static HttpClient createHttpsClient(String httpsCertPath) { + try { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, new TrustManager[] { new CustomX509TrustManager(httpsCertPath) }, + new SecureRandom()); + HttpClient client = new DefaultHttpClient(); + SSLSocketFactory ssf = new CustomX509TrustManager.CustomSSLSocketFactory(ctx); + ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + ClientConnectionManager ccm = client.getConnectionManager(); + SchemeRegistry sr = ccm.getSchemeRegistry(); + sr.register(new Scheme("https", ssf, 443)); + return new DefaultHttpClient(ccm, + client.getParams()); + } catch (NoSuchAlgorithmException|KeyManagementException|KeyStoreException| + UnrecoverableKeyException e) { + Log.w(TAG, e); + } + return null; + } +}