From 51b93b6e1424aa1f5f4a61eddd63355d2a2d2734 Mon Sep 17 00:00:00 2001 From: msmannan00 Date: Wed, 21 Apr 2021 17:48:24 +0500 Subject: [PATCH] Bug Fixes Bug Fixes --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 10 - .../homeController/homeController.java | 4 + .../helperManager/localFileDownloader.java | 129 ++- .../netcipher/NetCipher.java | 358 ++++++++ .../client/SocksAwareClientConnOperator.java | 255 ++++++ .../client/SocksAwareProxyRoutePlanner.java | 71 ++ .../netcipher/client/StrongBuilder.java | 159 ++++ .../netcipher/client/StrongBuilderBase.java | 282 ++++++ .../client/StrongConnectionBuilder.java | 165 ++++ .../netcipher/client/StrongConstants.java | 44 + .../netcipher/client/StrongHttpsClient.java | 165 ++++ .../client/StrongSSLSocketFactory.java | 202 +++++ .../client/TlsOnlySocketFactory.java | 544 ++++++++++++ .../netcipher/proxy/OrbotHelper.java | 701 +++++++++++++++ .../netcipher/proxy/ProxyHelper.java | 74 ++ .../netcipher/proxy/ProxySelector.java | 59 ++ .../netcipher/proxy/PsiphonHelper.java | 177 ++++ .../netcipher/proxy/SetFromMap.java | 88 ++ .../netcipher/proxy/SignatureUtils.java | 476 ++++++++++ .../netcipher/proxy/StatusCallback.java | 64 ++ .../netcipher/proxy/TorServiceUtils.java | 246 ++++++ .../netcipher/web/WebkitProxy.java | 832 +++++++++++++++++ app/src/main/res/localization.xml | 2 +- app/src/main/res/raw/debiancacerts.bks | Bin 0 -> 174398 bytes app/src/main/res/values-ca/strings.xml | 2 +- app/src/main/res/values-ch/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ja-rJP/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-th/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-ur/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- libnetcipher/.classpath | 9 + libnetcipher/.gitignore | 1 + libnetcipher/.project | 33 + libnetcipher/AndroidManifest.xml | 8 + libnetcipher/build.gradle | 62 ++ libnetcipher/custom_rules.xml | 104 +++ .../libs/httpclientandroidlib-1.2.1.jar | 0 libnetcipher/netcipher.pom | 33 + libnetcipher/proguard-project.txt | 20 + libnetcipher/project.properties | 15 + libnetcipher/res/raw/debiancacerts.bks | 0 libnetcipher/settings.gradle | 1 + .../guardianproject/netcipher/NetCipher.java | 357 ++++++++ .../client/SocksAwareClientConnOperator.java | 255 ++++++ .../client/SocksAwareProxyRoutePlanner.java | 71 ++ .../netcipher/client/StrongBuilder.java | 159 ++++ .../netcipher/client/StrongBuilderBase.java | 287 ++++++ .../client/StrongConnectionBuilder.java | 165 ++++ .../netcipher/client/StrongConstants.java | 44 + .../netcipher/client/StrongHttpsClient.java | 164 ++++ .../client/StrongSSLSocketFactory.java | 202 +++++ .../client/TlsOnlySocketFactory.java | 544 ++++++++++++ .../netcipher/proxy/OrbotHelper.java | 701 +++++++++++++++ .../netcipher/proxy/ProxyHelper.java | 74 ++ .../netcipher/proxy/ProxySelector.java | 59 ++ .../netcipher/proxy/PsiphonHelper.java | 177 ++++ .../netcipher/proxy/SetFromMap.java | 88 ++ .../netcipher/proxy/SignatureUtils.java | 476 ++++++++++ .../netcipher/proxy/StatusCallback.java | 64 ++ .../netcipher/proxy/TorServiceUtils.java | 246 ++++++ .../netcipher/web/WebkitProxy.java | 833 ++++++++++++++++++ 74 files changed, 10327 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/NetCipher.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/SocksAwareClientConnOperator.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/SocksAwareProxyRoutePlanner.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongBuilder.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongBuilderBase.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongConnectionBuilder.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongConstants.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongHttpsClient.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongSSLSocketFactory.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/TlsOnlySocketFactory.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/OrbotHelper.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/ProxyHelper.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/ProxySelector.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/PsiphonHelper.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/SetFromMap.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/SignatureUtils.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/StatusCallback.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/TorServiceUtils.java create mode 100644 app/src/main/java/com/darkweb/genesissearchengine/netcipher/web/WebkitProxy.java create mode 100644 app/src/main/res/raw/debiancacerts.bks create mode 100644 libnetcipher/.classpath create mode 100644 libnetcipher/.gitignore create mode 100644 libnetcipher/.project create mode 100644 libnetcipher/AndroidManifest.xml create mode 100644 libnetcipher/build.gradle create mode 100644 libnetcipher/custom_rules.xml create mode 100644 libnetcipher/libs/httpclientandroidlib-1.2.1.jar create mode 100644 libnetcipher/netcipher.pom create mode 100644 libnetcipher/proguard-project.txt create mode 100644 libnetcipher/project.properties create mode 100644 libnetcipher/res/raw/debiancacerts.bks create mode 100644 libnetcipher/settings.gradle create mode 100644 libnetcipher/src/info/guardianproject/netcipher/NetCipher.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/client/SocksAwareClientConnOperator.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/client/SocksAwareProxyRoutePlanner.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/client/StrongBuilder.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/client/StrongBuilderBase.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/client/StrongConnectionBuilder.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/client/StrongConstants.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/client/StrongHttpsClient.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/client/StrongSSLSocketFactory.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/client/TlsOnlySocketFactory.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/proxy/OrbotHelper.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/proxy/ProxyHelper.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/proxy/ProxySelector.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/proxy/PsiphonHelper.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/proxy/SetFromMap.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/proxy/SignatureUtils.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/proxy/StatusCallback.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/proxy/TorServiceUtils.java create mode 100644 libnetcipher/src/info/guardianproject/netcipher/web/WebkitProxy.java diff --git a/app/build.gradle b/app/build.gradle index fd015c5e..9f38f8dd 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,4 +143,5 @@ dependencies { implementation 'com.github.instacart.truetime-android:library-extension-rx:3.3' implementation files('libs/httpclientandroidlib-1.2.1.jar') implementation 'net.zetetic:android-database-sqlcipher:4.4.3@aar' + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 690a16ab..7e8ded7e 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -221,16 +221,6 @@ - - - - { private String PROXY_ADDRESS = "localhost"; private int PROXY_PORT = 9050; - private int mID = 123; - private String mFileName=""; + private int mID; + private String mFileName; private float mTotalByte; private float mDownloadByte; private String mURL; @@ -119,51 +120,99 @@ public class localFileDownloader extends AsyncTask { @Override protected String doInBackground(String... f_url) { int count; - try { - URL url = new URL(f_url[0]); - Proxy proxy = new Proxy(Proxy.Type.SOCKS, InetSocketAddress.createUnresolved(PROXY_ADDRESS, PROXY_PORT)); - URLConnection conection; + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + URL url = new URL(f_url[0]); + Proxy proxy = new Proxy(Proxy.Type.SOCKS, InetSocketAddress.createUnresolved(PROXY_ADDRESS, PROXY_PORT)); + URLConnection conection; - conection = url.openConnection(proxy); - //conection = (HttpsURLConnection)ProxySelector.openConnectionWithProxy(new URI(f_url[0])); + conection = url.openConnection(proxy); + //conection = (HttpsURLConnection)ProxySelector.openConnectionWithProxy(new URI(f_url[0])); - conection.connect(); - int lenghtOfFile = conection.getContentLength(); + conection.connect(); + int lenghtOfFile = conection.getContentLength(); - mStream = conection.getInputStream(); - // Output stream - output = new FileOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()+"/"+mFileName)); - byte[] data = new byte[100000]; + mStream = conection.getInputStream(); + // Output stream + output = new FileOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()+"/"+mFileName)); + byte[] data = new byte[100000]; - long total = 0; + long total = 0; + + mTotalByte = lenghtOfFile; + while ((count = mStream.read(data)) != -1) { + total += count; + int cur = (int) ((total * 100) / lenghtOfFile); + mDownloadByte = cur; + publishProgress(Math.min(cur, 100)); + if (Math.min(cur, 100) > 98) { + sleep(500); + } + Log.i("currentProgress", "currentProgress: " + Math.min(cur, 100) + "\n " + cur); + + output.write(data, 0, count); - mTotalByte = lenghtOfFile; - while ((count = mStream.read(data)) != -1) { - total += count; - int cur = (int) ((total * 100) / lenghtOfFile); - mDownloadByte = cur; - publishProgress(Math.min(cur, 100)); - if (Math.min(cur, 100) > 98) { - sleep(500); } - Log.i("currentProgress", "currentProgress: " + Math.min(cur, 100) + "\n " + cur); - output.write(data, 0, count); + build.setContentText("saving file"); + build.setSmallIcon(android.R.drawable.stat_sys_download); + mNotifyManager.notify(mID, build.build()); + output.flush(); + output.close(); + mStream.close(); + + } catch (Exception ex) { + onCancel(); } + }else { + try { + String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%"; + String urlEncoded = Uri.encode(f_url[0], ALLOWED_URI_CHARS); - build.setContentText("saving file"); - build.setSmallIcon(android.R.drawable.stat_sys_download); - mNotifyManager.notify(mID, build.build()); + StrongHttpsClient httpclient = new StrongHttpsClient(context); - output.flush(); - output.close(); - mStream.close(); + httpclient.useProxy(true, "SOCKS", "127.0.0.1", 9050); - } catch (Exception ex) { - onCancel(); + HttpGet httpget = new HttpGet(urlEncoded); + HttpResponse response = httpclient.execute(httpget); + + StringBuffer sb = new StringBuffer(); + sb.append(response.getStatusLine()).append("\n\n"); + + InputStream mStream = response.getEntity().getContent(); + + output = new FileOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()+"/"+mFileName)); + byte[] data = new byte[100000]; + + long total = 0; + + mTotalByte = response.getEntity().getContentLength(); + int read; + while ((read = mStream.read(data)) != -1) { + total += read; + int cur = (int) ((total * 100) / response.getEntity().getContentLength()); + mDownloadByte = cur; + publishProgress(Math.min(cur, 100)); + if (Math.min(cur, 100) > 98) { + sleep(500); + } + + Log.i("currentProgress", "currentProgress: " + Math.min(cur, 100) + "\n " + cur); + output.write(data, 0, read); + } + + build.setContentText("saving file"); + build.setSmallIcon(android.R.drawable.stat_sys_download); + mNotifyManager.notify(mID, build.build()); + + output.flush(); + output.close(); + mStream.close(); + }catch (Exception ex){ + Log.d("sda", "dsa"); + } } - return null; } diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/NetCipher.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/NetCipher.java new file mode 100644 index 00000000..dbc37985 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/NetCipher.java @@ -0,0 +1,358 @@ +/* + * Copyright 2014-2016 Hans-Christoph Steiner + * Copyright 2012-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher; + +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +import com.darkweb.genesissearchengine.netcipher.client.TlsOnlySocketFactory; +import com.darkweb.genesissearchengine.netcipher.proxy.OrbotHelper; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + + +public class NetCipher { + private static final String TAG = "NetCipher"; + + private NetCipher() { + // this is a utility class with only static methods + } + + public final static Proxy ORBOT_HTTP_PROXY = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress("127.0.0.1", 8118)); + + private static Proxy proxy; + + /** + * Set the global HTTP proxy for all new {@link HttpURLConnection}s and + * {@link HttpsURLConnection}s that are created after this is called. + *

+ * {@link #useTor()} will override this setting. Traffic must be directed + * to Tor using the proxy settings, and Orbot has its own proxy settings + * for connections that need proxies to work. So if "use Tor" is enabled, + * as tested by looking for the static instance of Proxy, then no other + * proxy settings are allowed to override the current Tor proxy. + * + * @param host the IP address for the HTTP proxy to use globally + * @param port the port number for the HTTP proxy to use globally + */ + public static void setProxy(String host, int port) { + if (!TextUtils.isEmpty(host) && port > 0) { + InetSocketAddress isa = new InetSocketAddress(host, port); + setProxy(new Proxy(Proxy.Type.HTTP, isa)); + } else if (NetCipher.proxy != ORBOT_HTTP_PROXY) { + setProxy(null); + } + } + + /** + * Set the global HTTP proxy for all new {@link HttpURLConnection}s and + * {@link HttpsURLConnection}s that are created after this is called. + *

+ * {@link #useTor()} will override this setting. Traffic must be directed + * to Tor using the proxy settings, and Orbot has its own proxy settings + * for connections that need proxies to work. So if "use Tor" is enabled, + * as tested by looking for the static instance of Proxy, then no other + * proxy settings are allowed to override the current Tor proxy. + * + * @param proxy the HTTP proxy to use globally + */ + public static void setProxy(Proxy proxy) { + if (proxy != null && NetCipher.proxy == ORBOT_HTTP_PROXY) { + Log.w(TAG, "useTor is enabled, ignoring new proxy settings!"); + } else { + NetCipher.proxy = proxy; + } + } + + /** + * Get the currently active global HTTP {@link Proxy}. + * + * @return the active HTTP {@link Proxy} + */ + public static Proxy getProxy() { + return proxy; + } + + /** + * Clear the global HTTP proxy for all new {@link HttpURLConnection}s and + * {@link HttpsURLConnection}s that are created after this is called. This + * returns things to the default, proxy-less state. + */ + public static void clearProxy() { + setProxy(null); + } + + /** + * Set Orbot as the global HTTP proxy for all new {@link HttpURLConnection} + * s and {@link HttpsURLConnection}s that are created after this is called. + * This overrides all future calls to {@link #setProxy(Proxy)}, except to + * clear the proxy, e.g. {@code #setProxy(null)} or {@link #clearProxy()}. + *

+ * Traffic must be directed to Tor using the proxy settings, and Orbot has its + * own proxy settings for connections that need proxies to work. So if "use + * Tor" is enabled, as tested by looking for the static instance of Proxy, + * then no other proxy settings are allowed to override the current Tor proxy. + */ + public static void useTor() { + setProxy(ORBOT_HTTP_PROXY); + } + + /** + * Get a {@link TlsOnlySocketFactory} from NetCipher. + * + * @see HttpsURLConnection#setDefaultSSLSocketFactory(SSLSocketFactory) + */ + public static TlsOnlySocketFactory getTlsOnlySocketFactory() { + return getTlsOnlySocketFactory(false); + } + + /** + * Get a {@link TlsOnlySocketFactory} from NetCipher, and specify whether + * it should use a more compatible, but less strong, suite of ciphers. + * + * @see HttpsURLConnection#setDefaultSSLSocketFactory(SSLSocketFactory) + */ + public static TlsOnlySocketFactory getTlsOnlySocketFactory(boolean compatible) { + SSLContext sslcontext; + try { + sslcontext = SSLContext.getInstance("TLSv1"); + sslcontext.init(null, null, null); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } catch (KeyManagementException e) { + throw new IllegalArgumentException(e); + } + return new TlsOnlySocketFactory(sslcontext.getSocketFactory(), compatible); + } + + /** + * Get a {@link HttpURLConnection} from a {@link URL}, and specify whether + * it should use a more compatible, but less strong, suite of ciphers. + * + * @param url + * @param compatible + * @return the {@code url} in an instance of {@link HttpURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getHttpURLConnection(URL url, boolean compatible) + throws IOException { + // .onion addresses only work via Tor, so force Tor for all of them + Proxy proxy = NetCipher.proxy; + if (OrbotHelper.isOnionAddress(url)) + proxy = ORBOT_HTTP_PROXY; + + HttpURLConnection connection; + if (proxy != null) { + connection = (HttpURLConnection) url.openConnection(proxy); + } else { + connection = (HttpURLConnection) url.openConnection(); + } + + if (connection instanceof HttpsURLConnection) { + HttpsURLConnection httpsConnection = ((HttpsURLConnection) connection); + SSLSocketFactory tlsOnly = getTlsOnlySocketFactory(compatible); + httpsConnection.setSSLSocketFactory(tlsOnly); + if (Build.VERSION.SDK_INT < 16) { + httpsConnection.setHostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.STRICT_HOSTNAME_VERIFIER); + } + } + return connection; + } + + /** + * Get a {@link HttpsURLConnection} from a URL {@link String} using the best + * TLS configuration available on the device. + * + * @param urlString + * @return the URL in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getHttpsURLConnection(String urlString) throws IOException { + URL url = new URL(urlString.replaceFirst("^[Hh][Tt][Tt][Pp]:", "https:")); + return getHttpsURLConnection(url, false); + } + + /** + * Get a {@link HttpsURLConnection} from a {@link Uri} using the best TLS + * configuration available on the device. + * + * @param uri + * @return the {@code uri} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getHttpsURLConnection(Uri uri) throws IOException { + return getHttpsURLConnection(uri.toString()); + } + + /** + * Get a {@link HttpsURLConnection} from a {@link URI} using the best TLS + * configuration available on the device. + * + * @param uri + * @return the {@code uri} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getHttpsURLConnection(URI uri) throws IOException { + if (TextUtils.equals(uri.getScheme(), "https")) + return getHttpsURLConnection(uri.toURL(), false); + else + // otherwise force scheme to https + return getHttpsURLConnection(uri.toString()); + } + + /** + * Get a {@link HttpsURLConnection} from a {@link URL} using the best TLS + * configuration available on the device. + * + * @param url + * @return the {@code url} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getHttpsURLConnection(URL url) throws IOException { + return getHttpsURLConnection(url, false); + } + + /** + * Get a {@link HttpsURLConnection} from a {@link URL} using a more + * compatible, but less strong, suite of ciphers. + * + * @param url + * @return the {@code url} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getCompatibleHttpsURLConnection(URL url) throws IOException { + return getHttpsURLConnection(url, true); + } + + /** + * Get a {@link HttpsURLConnection} from a {@link URL}, and specify whether + * it should use a more compatible, but less strong, suite of ciphers. + * + * @param url + * @param compatible + * @return the {@code url} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getHttpsURLConnection(URL url, boolean compatible) + throws IOException { + // use default method, but enforce a HttpsURLConnection + HttpURLConnection connection = getHttpURLConnection(url, compatible); + if (connection instanceof HttpsURLConnection) { + return (HttpsURLConnection) connection; + } else { + throw new IllegalArgumentException("not an HTTPS connection!"); + } + } + + /** + * Get a {@link HttpURLConnection} from a {@link URL}. If the connection is + * {@code https://}, it will use a more compatible, but less strong, TLS + * configuration. + * + * @param url + * @return the {@code url} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getCompatibleHttpURLConnection(URL url) throws IOException { + return getHttpURLConnection(url, true); + } + + /** + * Get a {@link HttpURLConnection} from a URL {@link String}. If it is an + * {@code https://} link, then this will use the best TLS configuration + * available on the device. + * + * @param urlString + * @return the URL in an instance of {@link HttpURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getHttpURLConnection(String urlString) throws IOException { + return getHttpURLConnection(new URL(urlString)); + } + + /** + * Get a {@link HttpURLConnection} from a {@link Uri}. If it is an + * {@code https://} link, then this will use the best TLS configuration + * available on the device. + * + * @param uri + * @return the {@code uri} in an instance of {@link HttpURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getHttpURLConnection(Uri uri) throws IOException { + return getHttpURLConnection(uri.toString()); + } + + /** + * Get a {@link HttpURLConnection} from a {@link URI}. If it is an + * {@code https://} link, then this will use the best TLS configuration + * available on the device. + * + * @param uri + * @return the {@code uri} in an instance of {@link HttpURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getHttpURLConnection(URI uri) throws IOException { + return getHttpURLConnection(uri.toURL()); + } + + /** + * Get a {@link HttpURLConnection} from a {@link URL}. If it is an + * {@code https://} link, then this will use the best TLS configuration + * available on the device. + * + * @param url + * @return the {@code url} in an instance of {@link HttpURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getHttpURLConnection(URL url) throws IOException { + return (HttpURLConnection) getHttpURLConnection(url, false); + } +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/SocksAwareClientConnOperator.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/SocksAwareClientConnOperator.java new file mode 100644 index 00000000..c6e17dd9 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/SocksAwareClientConnOperator.java @@ -0,0 +1,255 @@ +/* + * Copyright 2015 str4d + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.client; + +import android.util.Log; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import ch.boye.httpclientandroidlib.HttpHost; +import ch.boye.httpclientandroidlib.conn.HttpHostConnectException; +import ch.boye.httpclientandroidlib.conn.OperatedClientConnection; +import ch.boye.httpclientandroidlib.conn.scheme.Scheme; +import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry; +import ch.boye.httpclientandroidlib.conn.scheme.SchemeSocketFactory; +import ch.boye.httpclientandroidlib.conn.scheme.SocketFactory; +import ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory; +import ch.boye.httpclientandroidlib.impl.conn.DefaultClientConnectionOperator; +import ch.boye.httpclientandroidlib.params.HttpParams; +import ch.boye.httpclientandroidlib.protocol.HttpContext; + +public class SocksAwareClientConnOperator extends DefaultClientConnectionOperator { + + private static final int CONNECT_TIMEOUT_MILLISECONDS = 60000; + private static final int READ_TIMEOUT_MILLISECONDS = 60000; + + private HttpHost mProxyHost; + private String mProxyType; + private SocksAwareProxyRoutePlanner mRoutePlanner; + + public SocksAwareClientConnOperator(SchemeRegistry registry, + HttpHost proxyHost, + String proxyType, + SocksAwareProxyRoutePlanner proxyRoutePlanner) { + super(registry); + + mProxyHost = proxyHost; + mProxyType = proxyType; + mRoutePlanner = proxyRoutePlanner; + } + + @Override + public void openConnection( + final OperatedClientConnection conn, + final HttpHost target, + final InetAddress local, + final HttpContext context, + final HttpParams params) throws IOException { + if (mProxyHost != null) { + if (mProxyType != null && mProxyType.equalsIgnoreCase("socks")) { + Log.d("StrongHTTPS", "proxying using SOCKS"); + openSocksConnection(mProxyHost, conn, target, local, context, params); + } else { + Log.d("StrongHTTPS", "proxying with: " + mProxyType); + openNonSocksConnection(conn, target, local, context, params); + } + } else if (mRoutePlanner != null) { + if (mRoutePlanner.isProxy(target)) { + // HTTP proxy, already handled by the route planner system + Log.d("StrongHTTPS", "proxying using non-SOCKS"); + openNonSocksConnection(conn, target, local, context, params); + } else { + // Either SOCKS or direct + HttpHost proxy = mRoutePlanner.determineRequiredProxy(target, null, context); + if (proxy == null) { + Log.d("StrongHTTPS", "not proxying"); + openNonSocksConnection(conn, target, local, context, params); + } else if (mRoutePlanner.isSocksProxy(proxy)) { + Log.d("StrongHTTPS", "proxying using SOCKS"); + openSocksConnection(proxy, conn, target, local, context, params); + } else { + throw new IllegalStateException("Non-SOCKS proxy returned"); + } + } + } else { + Log.d("StrongHTTPS", "not proxying"); + openNonSocksConnection(conn, target, local, context, params); + } + } + + private void openNonSocksConnection( + final OperatedClientConnection conn, + final HttpHost target, + final InetAddress local, + final HttpContext context, + final HttpParams params) throws IOException { + if (conn == null) { + throw new IllegalArgumentException("Connection must not be null."); + } + if (target == null) { + throw new IllegalArgumentException("Target host must not be null."); + } + // local address may be null + // @@@ is context allowed to be null? + if (params == null) { + throw new IllegalArgumentException("Parameters must not be null."); + } + if (conn.isOpen()) { + throw new IllegalArgumentException("Connection must not be open."); + } + + final Scheme schm = schemeRegistry.getScheme(target.getSchemeName()); + final SocketFactory sf = schm.getSocketFactory(); + + Socket sock = sf.createSocket(); + conn.opening(sock, target); + + try { + Socket connsock = sf.connectSocket(sock, target.getHostName(), + schm.resolvePort(target.getPort()), + local, 0, params); + + if (sock != connsock) { + sock = connsock; + conn.opening(sock, target); + } + } catch (ConnectException ex) { + throw new HttpHostConnectException(target, ex); + } + prepareSocket(sock, context, params); + conn.openCompleted(sf.isSecure(sock), params); + } + + // Derived from the original DefaultClientConnectionOperator.java in Apache HttpClient 4.2 + private void openSocksConnection( + final HttpHost proxy, + final OperatedClientConnection conn, + final HttpHost target, + final InetAddress local, + final HttpContext context, + final HttpParams params) throws IOException { + Socket socket = null; + Socket sslSocket = null; + try { + if (conn == null || target == null || params == null) { + throw new IllegalArgumentException("Required argument may not be null"); + } + if (conn.isOpen()) { + throw new IllegalStateException("Connection must not be open"); + } + + Scheme scheme = schemeRegistry.getScheme(target.getSchemeName()); + SchemeSocketFactory schemeSocketFactory = scheme.getSchemeSocketFactory(); + + int port = scheme.resolvePort(target.getPort()); + String host = target.getHostName(); + + // Perform explicit SOCKS4a connection request. SOCKS4a supports remote host name resolution + // (i.e., Tor resolves the hostname, which may be an onion address). + // The Android (Apache Harmony) Socket class appears to support only SOCKS4 and throws an + // exception on an address created using INetAddress.createUnresolved() -- so the typical + // technique for using Java SOCKS4a/5 doesn't appear to work on Android: + // https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/java/net/PlainSocketImpl.java + // See also: http://www.mit.edu/~foley/TinFoil/src/tinfoil/TorLib.java, for a similar implementation + + // From http://en.wikipedia.org/wiki/SOCKS#SOCKS4a: + // + // field 1: SOCKS version number, 1 byte, must be 0x04 for this version + // field 2: command code, 1 byte: + // 0x01 = establish a TCP/IP stream connection + // 0x02 = establish a TCP/IP port binding + // field 3: network byte order port number, 2 bytes + // field 4: deliberate invalid IP address, 4 bytes, first three must be 0x00 and the last one must not be 0x00 + // field 5: the user ID string, variable length, terminated with a null (0x00) + // field 6: the domain name of the host we want to contact, variable length, terminated with a null (0x00) + + + socket = new Socket(); + conn.opening(socket, target); + socket.setSoTimeout(READ_TIMEOUT_MILLISECONDS); + socket.connect(new InetSocketAddress(proxy.getHostName(), proxy.getPort()), CONNECT_TIMEOUT_MILLISECONDS); + + DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream()); + outputStream.write((byte) 0x04); + outputStream.write((byte) 0x01); + outputStream.writeShort((short) port); + outputStream.writeInt(0x01); + outputStream.write((byte) 0x00); + outputStream.write(host.getBytes()); + outputStream.write((byte) 0x00); + + DataInputStream inputStream = new DataInputStream(socket.getInputStream()); + if (inputStream.readByte() != (byte) 0x00 || inputStream.readByte() != (byte) 0x5a) { + throw new IOException("SOCKS4a connect failed"); + } + inputStream.readShort(); + inputStream.readInt(); + + if (schemeSocketFactory instanceof SSLSocketFactory) { + sslSocket = ((SSLSocketFactory) schemeSocketFactory).createLayeredSocket(socket, host, port, params); + conn.opening(sslSocket, target); + sslSocket.setSoTimeout(READ_TIMEOUT_MILLISECONDS); + prepareSocket(sslSocket, context, params); + conn.openCompleted(schemeSocketFactory.isSecure(sslSocket), params); + } else { + conn.opening(socket, target); + socket.setSoTimeout(READ_TIMEOUT_MILLISECONDS); + prepareSocket(socket, context, params); + conn.openCompleted(schemeSocketFactory.isSecure(socket), params); + } + // TODO: clarify which connection throws java.net.SocketTimeoutException? + } catch (IOException e) { + try { + if (sslSocket != null) { + sslSocket.close(); + } + if (socket != null) { + socket.close(); + } + } catch (IOException ioe) { + } + throw e; + } + } + + @Override + public void updateSecureConnection( + final OperatedClientConnection conn, + final HttpHost target, + final HttpContext context, + final HttpParams params) throws IOException { + if (mProxyHost != null && mProxyType.equalsIgnoreCase("socks")) + throw new RuntimeException("operation not supported"); + else + super.updateSecureConnection(conn, target, context, params); + } + + @Override + protected InetAddress[] resolveHostname(final String host) throws UnknownHostException { + if (mProxyHost != null && mProxyType.equalsIgnoreCase("socks")) + throw new RuntimeException("operation not supported"); + else + return super.resolveHostname(host); + } +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/SocksAwareProxyRoutePlanner.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/SocksAwareProxyRoutePlanner.java new file mode 100644 index 00000000..561bc80d --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/SocksAwareProxyRoutePlanner.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 str4d + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.darkweb.genesissearchengine.netcipher.client; + +import ch.boye.httpclientandroidlib.HttpException; +import ch.boye.httpclientandroidlib.HttpHost; +import ch.boye.httpclientandroidlib.HttpRequest; +import ch.boye.httpclientandroidlib.conn.SchemePortResolver; +import ch.boye.httpclientandroidlib.impl.conn.DefaultRoutePlanner; +import ch.boye.httpclientandroidlib.protocol.HttpContext; + +public abstract class SocksAwareProxyRoutePlanner extends DefaultRoutePlanner { + public SocksAwareProxyRoutePlanner(SchemePortResolver schemePortResolver) { + super(schemePortResolver); + } + + @Override + protected HttpHost determineProxy( + HttpHost target, + HttpRequest request, + HttpContext context) throws HttpException { + HttpHost proxy = determineRequiredProxy(target, request, context); + if (isSocksProxy(proxy)) + proxy = null; + return proxy; + } + + /** + * Determine the proxy required for the provided target. + * + * @param target see {@link #determineProxy(HttpHost, HttpRequest, HttpContext) determineProxy()} + * @param request see {@link #determineProxy(HttpHost, HttpRequest, HttpContext) determineProxy()}. + * Will be null when called from {@link SocksAwareClientConnOperator} to + * determine if target requires a SOCKS proxy, so don't rely on it in this case. + * @param context see {@link #determineProxy(HttpHost, HttpRequest, HttpContext) determineProxy()} + * @return the proxy required for this target, or null if should connect directly. + */ + protected abstract HttpHost determineRequiredProxy( + HttpHost target, + HttpRequest request, + HttpContext context); + + /** + * Checks if the provided target is a proxy we define. + * + * @param target to check + * @return true if this is a proxy, false otherwise + */ + protected abstract boolean isProxy(HttpHost target); + + /** + * Checks if the provided target is a SOCKS proxy we define. + * + * @param target to check + * @return true if this target is a SOCKS proxy, false otherwise. + */ + protected abstract boolean isSocksProxy(HttpHost target); +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongBuilder.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongBuilder.java new file mode 100644 index 00000000..9f5aee0e --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongBuilder.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.client; + +import android.content.Intent; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import javax.net.ssl.TrustManager; + +public interface StrongBuilder { + /** + * Callback to get a connection handed to you for use, + * already set up for NetCipher. + * + * @param the type of connection created by this builder + */ + interface Callback { + /** + * Called when the NetCipher-enhanced connection is ready + * for use. + * + * @param connection the connection + */ + void onConnected(C connection); + + /** + * Called if we tried to connect through to Orbot but failed + * for some reason + * + * @param e the reason + */ + void onConnectionException(Exception e); + + /** + * Called if our attempt to get a status from Orbot failed + * after a defined period of time. See statusTimeout() on + * OrbotInitializer. + */ + void onTimeout(); + + /** + * Called if you requested validation that we are connecting + * through Tor, and while we were able to connect to Orbot, that + * validation failed. + */ + void onInvalid(); + } + + /** + * Call this to configure the Tor proxy from the results + * returned by Orbot, using the best available proxy + * (SOCKS if possible, else HTTP) + * + * @return the builder + */ + T withBestProxy(); + + /** + * @return true if this builder supports HTTP proxies, false + * otherwise + */ + boolean supportsHttpProxy(); + + /** + * Call this to configure the Tor proxy from the results + * returned by Orbot, using the HTTP proxy. + * + * @return the builder + */ + T withHttpProxy(); + + /** + * @return true if this builder supports SOCKS proxies, false + * otherwise + */ + boolean supportsSocksProxy(); + + /** + * Call this to configure the Tor proxy from the results + * returned by Orbot, using the SOCKS proxy. + * + * @return the builder + */ + T withSocksProxy(); + + /** + * Applies your own custom TrustManagers, such as for + * replacing the stock keystore support with a custom + * keystore. + * + * @param trustManagers the TrustManagers to use + * @return the builder + */ + T withTrustManagers(TrustManager[] trustManagers) + throws NoSuchAlgorithmException, KeyManagementException; + + /** + * Call this if you want a weaker set of supported ciphers, + * because you are running into compatibility problems with + * some server due to a cipher mismatch. The better solution + * is to fix the server. + * + * @return the builder + */ + T withWeakCiphers(); + + /** + * Call this if you want the builder to confirm that we are + * communicating over Tor, by reaching out to a Tor test + * server and confirming our connection status. By default, + * this is skipped. Adding this check adds security, but it + * has the chance of false negatives (e.g., we cannot reach + * that Tor server for some reason). + * + * @return the builder + */ + T withTorValidation(); + + /** + * Builds a connection, applying the configuration already + * specified in the builder. + * + * @param status status Intent from OrbotInitializer + * @return the connection + * @throws IOException + */ + C build(Intent status) throws Exception; + + /** + * Asynchronous version of build(), one that uses OrbotInitializer + * internally to get the status and checks the validity of the Tor + * connection (if requested). Note that your callback methods may + * be invoked on any thread; do not assume that they will be called + * on any particular thread. + * + * @param callback Callback to get a connection handed to you + * for use, already set up for NetCipher + */ + void build(Callback callback); +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongBuilderBase.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongBuilderBase.java new file mode 100644 index 00000000..66d15fb5 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongBuilderBase.java @@ -0,0 +1,282 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * Copyright 2015 str4d + * Portions Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.client; + +import android.content.Context; +import android.content.Intent; + +import com.darkweb.genesissearchengine.netcipher.proxy.OrbotHelper; + +import org.json.JSONObject; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +/** + * Builds an HttpUrlConnection that connects via Tor through + * Orbot. + */ +abstract public class + StrongBuilderBase + implements StrongBuilder { + /** + * Performs an HTTP GET request using the supplied connection + * to a supplied URL, returning the String response or + * throws an Exception (e.g., cannot reach the server). + * This is used as part of validating the Tor connection. + * + * @param status the status Intent we got back from Orbot + * @param connection a connection of the type for the builder + * @param url an public Web page + * @return the String response from the GET request + */ + abstract protected String get(Intent status, C connection, String url) + throws Exception; + + final static String TOR_CHECK_URL="https://check.torproject.org/api/ip"; + private final static String PROXY_HOST="127.0.0.1"; + protected final Context ctxt; + protected Proxy.Type proxyType; + protected SSLContext sslContext=null; + protected boolean useWeakCiphers=false; + protected boolean validateTor=false; + + /** + * Standard constructor. + * + * @param ctxt any Context will do; the StrongBuilderBase + * will hold onto the Application singleton + */ + public StrongBuilderBase(Context ctxt) { + this.ctxt=ctxt.getApplicationContext(); + } + + /** + * Copy constructor. + * + * @param original builder to clone + */ + public StrongBuilderBase(StrongBuilderBase original) { + this.ctxt=original.ctxt; + this.proxyType=original.proxyType; + this.sslContext=original.sslContext; + this.useWeakCiphers=original.useWeakCiphers; + } + + /** + * {@inheritDoc} + */ + @Override + public T withBestProxy() { + if (supportsSocksProxy()) { + return(withSocksProxy()); + } + else { + return(withHttpProxy()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportsHttpProxy() { + return(true); + } + + /** + * {@inheritDoc} + */ + @Override + public T withHttpProxy() { + proxyType=Proxy.Type.HTTP; + + return((T)this); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportsSocksProxy() { + return(false); + } + + /** + * {@inheritDoc} + */ + @Override + public T withSocksProxy() { + proxyType=Proxy.Type.SOCKS; + + return((T)this); + } + + /** + * {@inheritDoc} + */ + @Override + public T withTrustManagers(TrustManager[] trustManagers) + throws NoSuchAlgorithmException, KeyManagementException { + + sslContext=SSLContext.getInstance("TLSv1"); + sslContext.init(null, trustManagers, null); + + return((T)this); + } + + /** + * {@inheritDoc} + */ + @Override + public T withWeakCiphers() { + useWeakCiphers=true; + + return((T)this); + } + + /** + * {@inheritDoc} + */ + @Override + public T withTorValidation() { + validateTor=true; + + return((T)this); + } + + public SSLContext getSSLContext() { + return(sslContext); + } + + public int getSocksPort(Intent status) { + if (status.getStringExtra(OrbotHelper.EXTRA_STATUS) + .equals(OrbotHelper.STATUS_ON)) { + return(status.getIntExtra(OrbotHelper.EXTRA_PROXY_PORT_SOCKS, + 9050)); + } + + return(-1); + } + + public int getHttpPort(Intent status) { + if (status.getStringExtra(OrbotHelper.EXTRA_STATUS) + .equals(OrbotHelper.STATUS_ON)) { + return(status.getIntExtra(OrbotHelper.EXTRA_PROXY_PORT_HTTP, + 8118)); + } + + return(-1); + } + + protected SSLSocketFactory buildSocketFactory() { + if (sslContext==null) { + return(null); + } + + SSLSocketFactory result= + new TlsOnlySocketFactory(sslContext.getSocketFactory(), + useWeakCiphers); + + return(result); + } + + public Proxy buildProxy(Intent status) { + Proxy result=null; + + if (status.getStringExtra(OrbotHelper.EXTRA_STATUS) + .equals(OrbotHelper.STATUS_ON)) { + if (proxyType==Proxy.Type.SOCKS) { + result=new Proxy(Proxy.Type.SOCKS, + new InetSocketAddress(PROXY_HOST, getSocksPort(status))); + } + else if (proxyType==Proxy.Type.HTTP) { + result=new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(PROXY_HOST, getHttpPort(status))); + } + } + + return(result); + } + + @Override + public void build(final Callback callback) { + OrbotHelper.get(ctxt).addStatusCallback( + new OrbotHelper.SimpleStatusCallback() { + @Override + public void onEnabled(Intent statusIntent) { + OrbotHelper.get(ctxt).removeStatusCallback(this); + + try { + C connection=build(statusIntent); + + if (validateTor) { + validateTor=false; + checkTor(callback, statusIntent, connection); + } + else { + callback.onConnected(connection); + } + } + catch (Exception e) { + callback.onConnectionException(e); + } + } + + @Override + public void onNotYetInstalled() { + OrbotHelper.get(ctxt).removeStatusCallback(this); + callback.onTimeout(); + } + + @Override + public void onStatusTimeout() { + OrbotHelper.get(ctxt).removeStatusCallback(this); + callback.onTimeout(); + } + }); + } + + protected void checkTor(final Callback callback, final Intent status, + final C connection) { + new Thread() { + @Override + public void run() { + try { + String result=get(status, connection, TOR_CHECK_URL); + JSONObject json=new JSONObject(result); + + if (json.optBoolean("IsTor", false)) { + callback.onConnected(connection); + } + else { + callback.onInvalid(); + } + } + catch (Exception e) { + callback.onConnectionException(e); + } + } + }.start(); + } +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongConnectionBuilder.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongConnectionBuilder.java new file mode 100644 index 00000000..673b7661 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongConnectionBuilder.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.client; + +import android.content.Context; +import android.content.Intent; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +/** + * Builds an HttpUrlConnection that connects via Tor through + * Orbot. + */ +public class StrongConnectionBuilder + extends StrongBuilderBase { + private URL url; + + /** + * Creates a StrongConnectionBuilder using the strongest set + * of options for security. Use this if the strongest set of + * options is what you want; otherwise, create a + * builder via the constructor and configure it as you see fit. + * + * @param ctxt any Context will do + * @return a configured StrongConnectionBuilder + * @throws Exception + */ + static public StrongConnectionBuilder forMaxSecurity(Context ctxt) + throws Exception { + return(new StrongConnectionBuilder(ctxt) + .withBestProxy()); + } + + /** + * Creates a builder instance. + * + * @param ctxt any Context will do; builder will hold onto + * Application context + */ + public StrongConnectionBuilder(Context ctxt) { + super(ctxt); + } + + /** + * Copy constructor. + * + * @param original builder to clone + */ + public StrongConnectionBuilder(StrongConnectionBuilder original) { + super(original); + this.url=original.url; + } +/* + public boolean supportsSocksProxy() { + return(false); + } +*/ + + /** + * Sets the URL to build a connection for. + * + * @param url the URL + * @return the builder + * @throws MalformedURLException + */ + public StrongConnectionBuilder connectTo(String url) + throws MalformedURLException { + connectTo(new URL(url)); + + return(this); + } + + /** + * Sets the URL to build a connection for. + * + * @param url the URL + * @return the builder + */ + public StrongConnectionBuilder connectTo(URL url) { + this.url=url; + + return(this); + } + + /** + * {@inheritDoc} + */ + @Override + public HttpURLConnection build(Intent status) throws IOException { + return(buildForUrl(status, url)); + } + + @Override + protected String get(Intent status, HttpURLConnection connection, + String url) throws Exception { + HttpURLConnection realConnection=buildForUrl(status, new URL(url)); + + return(slurp(realConnection.getInputStream())); + } + + private HttpURLConnection buildForUrl(Intent status, URL urlToUse) + throws IOException { + URLConnection result; + Proxy proxy=buildProxy(status); + + if (proxy==null) { + result=urlToUse.openConnection(); + } + else { + result=urlToUse.openConnection(proxy); + } + + if (result instanceof HttpsURLConnection && sslContext!=null) { + SSLSocketFactory tlsOnly=buildSocketFactory(); + HttpsURLConnection https=(HttpsURLConnection)result; + + https.setSSLSocketFactory(tlsOnly); + } + + return((HttpURLConnection)result); + } + + // based on http://stackoverflow.com/a/309718/115145 + + public static String slurp(final InputStream is) + throws IOException { + final char[] buffer = new char[128]; + final StringBuilder out = new StringBuilder(); + final Reader in = new InputStreamReader(is, "UTF-8"); + + for (;;) { + int rsz = in.read(buffer, 0, buffer.length); + if (rsz < 0) + break; + out.append(buffer, 0, rsz); + } + + in.close(); + + return out.toString(); + } +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongConstants.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongConstants.java new file mode 100644 index 00000000..94a62a15 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongConstants.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.darkweb.genesissearchengine.netcipher.client; + +public class StrongConstants { + + /** + * Ordered to prefer the stronger cipher suites as noted + * http://op-co.de/blog/posts/android_ssl_downgrade/ + */ + public static final String ENABLED_CIPHERS[] = { + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", "SSL_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_WITH_RC4_128_SHA", "SSL_RSA_WITH_RC4_128_MD5" }; + + /** + * Ordered to prefer the stronger/newer TLS versions as noted + * http://op-co.de/blog/posts/android_ssl_downgrade/ + */ + public static final String ENABLED_PROTOCOLS[] = { "TLSv1.2", "TLSv1.1", + "TLSv1" }; + +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongHttpsClient.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongHttpsClient.java new file mode 100644 index 00000000..d5ffe3b5 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongHttpsClient.java @@ -0,0 +1,165 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * Copyright 2015 str4d + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.client; + +import android.content.Context; + +import com.example.myapplication.R; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import javax.net.ssl.TrustManagerFactory; + +import ch.boye.httpclientandroidlib.HttpHost; +import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator; +import ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames; +import ch.boye.httpclientandroidlib.conn.scheme.PlainSocketFactory; +import ch.boye.httpclientandroidlib.conn.scheme.Scheme; +import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry; +import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; +import ch.boye.httpclientandroidlib.impl.conn.tsccm.ThreadSafeClientConnManager; + +public class StrongHttpsClient extends DefaultHttpClient { + + final Context context; + private HttpHost proxyHost; + private String proxyType; + private SocksAwareProxyRoutePlanner routePlanner; + + private StrongSSLSocketFactory sFactory; + private SchemeRegistry mRegistry; + + private final static String TRUSTSTORE_TYPE = "BKS"; + private final static String TRUSTSTORE_PASSWORD = "changeit"; + + public StrongHttpsClient(Context context) { + this.context = context; + + mRegistry = new SchemeRegistry(); + mRegistry.register( + new Scheme(TYPE_HTTP, 80, PlainSocketFactory.getSocketFactory())); + + + try { + KeyStore keyStore = loadKeyStore(); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + sFactory = new StrongSSLSocketFactory(context, trustManagerFactory.getTrustManagers(), keyStore, TRUSTSTORE_PASSWORD); + mRegistry.register(new Scheme("https", 443, sFactory)); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + private KeyStore loadKeyStore () throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException + { + + KeyStore trustStore = KeyStore.getInstance(TRUSTSTORE_TYPE); + // load our bundled cacerts from raw assets + InputStream in = context.getResources().openRawResource(R.raw.debiancacerts); + trustStore.load(in, TRUSTSTORE_PASSWORD.toCharArray()); + + return trustStore; + } + + public StrongHttpsClient(Context context, KeyStore keystore) { + this.context = context; + + mRegistry = new SchemeRegistry(); + mRegistry.register( + new Scheme(TYPE_HTTP, 80, PlainSocketFactory.getSocketFactory())); + + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + sFactory = new StrongSSLSocketFactory(context, trustManagerFactory.getTrustManagers(), keystore, TRUSTSTORE_PASSWORD); + mRegistry.register(new Scheme("https", 443, sFactory)); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + @Override + protected ThreadSafeClientConnManager createClientConnectionManager() { + + return new ThreadSafeClientConnManager(getParams(), mRegistry) + { + @Override + protected ClientConnectionOperator createConnectionOperator( + SchemeRegistry schreg) { + + return new SocksAwareClientConnOperator(schreg, proxyHost, proxyType, + routePlanner); + } + }; + } + + public void useProxy(boolean enableTor, String type, String host, int port) + { + if (enableTor) + { + this.proxyType = type; + + if (type.equalsIgnoreCase(TYPE_SOCKS)) + { + proxyHost = new HttpHost(host, port); + } + else + { + proxyHost = new HttpHost(host, port, type); + getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxyHost); + } + } + else + { + getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY); + proxyHost = null; + } + + } + + public void disableProxy () + { + getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY); + proxyHost = null; + } + + public void useProxyRoutePlanner(SocksAwareProxyRoutePlanner proxyRoutePlanner) + { + routePlanner = proxyRoutePlanner; + setRoutePlanner(proxyRoutePlanner); + } + + /** + * NOT ADVISED, but some sites don't yet have latest protocols and ciphers available, and some + * apps still need to support them + * https://dev.guardianproject.info/issues/5644 + */ + public void enableSSLCompatibilityMode() { + sFactory.setEnableStongerDefaultProtocalVersion(false); + sFactory.setEnableStongerDefaultSSLCipherSuite(false); + } + + public final static String TYPE_SOCKS = "socks"; + public final static String TYPE_HTTP = "http"; + +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongSSLSocketFactory.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongSSLSocketFactory.java new file mode 100644 index 00000000..28e533af --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/StrongSSLSocketFactory.java @@ -0,0 +1,202 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.darkweb.genesissearchengine.netcipher.client; + +import android.content.Context; + +import ch.boye.httpclientandroidlib.conn.scheme.LayeredSchemeSocketFactory; +import ch.boye.httpclientandroidlib.params.HttpParams; + +import java.io.IOException; +import java.net.Proxy; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +public class StrongSSLSocketFactory extends + ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory implements + LayeredSchemeSocketFactory { + + private SSLSocketFactory mFactory = null; + + private Proxy mProxy = null; + + public static final String TLS = "TLS"; + public static final String SSL = "SSL"; + public static final String SSLV2 = "SSLv2"; + + // private X509HostnameVerifier mHostnameVerifier = new + // StrictHostnameVerifier(); + // private final HostNameResolver mNameResolver = new + // StrongHostNameResolver(); + + private boolean mEnableStongerDefaultSSLCipherSuite = true; + private boolean mEnableStongerDefaultProtocalVersion = true; + + private String[] mProtocols; + private String[] mCipherSuites; + + public StrongSSLSocketFactory(Context context, + TrustManager[] trustManagers, KeyStore keyStore, String keyStorePassword) + throws KeyManagementException, UnrecoverableKeyException, + NoSuchAlgorithmException, KeyStoreException, CertificateException, + IOException { + super(keyStore); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + KeyManager[] km = createKeyManagers( + keyStore, + keyStorePassword); + sslContext.init(km, trustManagers, new SecureRandom()); + + mFactory = sslContext.getSocketFactory(); + + } + + private void readSSLParameters(SSLSocket sslSocket) { + List protocolsToEnable = new ArrayList(); + List supportedProtocols = Arrays.asList(sslSocket.getSupportedProtocols()); + for(String enabledProtocol : StrongConstants.ENABLED_PROTOCOLS) { + if(supportedProtocols.contains(enabledProtocol)) { + protocolsToEnable.add(enabledProtocol); + } + } + this.mProtocols = protocolsToEnable.toArray(new String[protocolsToEnable.size()]); + + List cipherSuitesToEnable = new ArrayList(); + List supportedCipherSuites = Arrays.asList(sslSocket.getSupportedCipherSuites()); + for(String enabledCipherSuite : StrongConstants.ENABLED_CIPHERS) { + if(supportedCipherSuites.contains(enabledCipherSuite)) { + cipherSuitesToEnable.add(enabledCipherSuite); + } + } + this.mCipherSuites = cipherSuitesToEnable.toArray(new String[cipherSuitesToEnable.size()]); + } + + private KeyManager[] createKeyManagers(final KeyStore keystore, + final String password) throws KeyStoreException, + NoSuchAlgorithmException, UnrecoverableKeyException { + if (keystore == null) { + throw new IllegalArgumentException("Keystore may not be null"); + } + KeyManagerFactory kmfactory = KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmfactory.init(keystore, password != null ? password.toCharArray() + : null); + return kmfactory.getKeyManagers(); + } + + @Override + public Socket createSocket() throws IOException { + Socket newSocket = mFactory.createSocket(); + enableStrongerDefaults(newSocket); + return newSocket; + } + + @Override + public Socket createSocket(Socket socket, String host, int port, + boolean autoClose) throws IOException, UnknownHostException { + + Socket newSocket = mFactory.createSocket(socket, host, port, autoClose); + + enableStrongerDefaults(newSocket); + + return newSocket; + } + + /** + * Defaults the SSL connection to use a strong cipher suite and TLS version + * + * @param socket + */ + private void enableStrongerDefaults(Socket socket) { + if (isSecure(socket)) { + SSLSocket sslSocket = (SSLSocket) socket; + readSSLParameters(sslSocket); + + if (mEnableStongerDefaultProtocalVersion && mProtocols != null) { + sslSocket.setEnabledProtocols(mProtocols); + } + + if (mEnableStongerDefaultSSLCipherSuite && mCipherSuites != null) { + sslSocket.setEnabledCipherSuites(mCipherSuites); + } + } + } + + @Override + public boolean isSecure(Socket sock) throws IllegalArgumentException { + return (sock instanceof SSLSocket); + } + + public void setProxy(Proxy proxy) { + mProxy = proxy; + } + + public Proxy getProxy() { + return mProxy; + } + + public boolean isEnableStongerDefaultSSLCipherSuite() { + return mEnableStongerDefaultSSLCipherSuite; + } + + public void setEnableStongerDefaultSSLCipherSuite(boolean enable) { + this.mEnableStongerDefaultSSLCipherSuite = enable; + } + + public boolean isEnableStongerDefaultProtocalVersion() { + return mEnableStongerDefaultProtocalVersion; + } + + public void setEnableStongerDefaultProtocalVersion(boolean enable) { + this.mEnableStongerDefaultProtocalVersion = enable; + } + + @Override + public Socket createSocket(HttpParams httpParams) throws IOException { + Socket newSocket = mFactory.createSocket(); + + enableStrongerDefaults(newSocket); + + return newSocket; + + } + + @Override + public Socket createLayeredSocket(Socket arg0, String arg1, int arg2, + boolean arg3) throws IOException, UnknownHostException { + return ((LayeredSchemeSocketFactory) mFactory).createLayeredSocket( + arg0, arg1, arg2, arg3); + } + +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/TlsOnlySocketFactory.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/TlsOnlySocketFactory.java new file mode 100644 index 00000000..b7c190a5 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/client/TlsOnlySocketFactory.java @@ -0,0 +1,544 @@ +/* + * Copyright 2015 Bhavit Singh Sengar + * Copyright 2015-2016 Hans-Christoph Steiner + * Copyright 2015-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * From https://stackoverflow.com/a/29946540 + */ + +package com.darkweb.genesissearchengine.netcipher.client; + +import android.net.SSLCertificateSocketFactory; +import android.os.Build; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * While making a secure connection, Android's {@link HttpsURLConnection} falls + * back to SSLv3 from TLSv1. This is a bug in android versions < 4.4. It can be + * fixed by removing the SSLv3 protocol from Enabled Protocols list. Use this as + * the {@link SSLSocketFactory} for + * {@link HttpsURLConnection#setDefaultSSLSocketFactory(SSLSocketFactory)} + * + * @author Bhavit S. Sengar + * @author Hans-Christoph Steiner + */ +public class TlsOnlySocketFactory extends SSLSocketFactory { + private static final int HANDSHAKE_TIMEOUT=0; + private static final String TAG = "TlsOnlySocketFactory"; + private final SSLSocketFactory delegate; + private final boolean compatible; + + public TlsOnlySocketFactory() { + this.delegate =SSLCertificateSocketFactory.getDefault(HANDSHAKE_TIMEOUT, null); + this.compatible = false; + } + + public TlsOnlySocketFactory(SSLSocketFactory delegate) { + this.delegate = delegate; + this.compatible = false; + } + + /** + * Make {@link SSLSocket}s that are compatible with outdated servers. + * + * @param delegate + * @param compatible + */ + public TlsOnlySocketFactory(SSLSocketFactory delegate, boolean compatible) { + this.delegate = delegate; + this.compatible = compatible; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + private Socket makeSocketSafe(Socket socket, String host) { + if (socket instanceof SSLSocket) { + TlsOnlySSLSocket tempSocket= + new TlsOnlySSLSocket((SSLSocket) socket, compatible); + + if (delegate instanceof SSLCertificateSocketFactory && + Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN_MR1) { + ((android.net.SSLCertificateSocketFactory)delegate) + .setHostname(socket, host); + } + else { + tempSocket.setHostname(host); + } + + socket = tempSocket; + } + return socket; + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + return makeSocketSafe(delegate.createSocket(s, host, port, autoClose), host); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return makeSocketSafe(delegate.createSocket(host, port), host); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException { + return makeSocketSafe(delegate.createSocket(host, port, localHost, localPort), host); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return makeSocketSafe(delegate.createSocket(host, port), host.getHostName()); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, + int localPort) throws IOException { + return makeSocketSafe(delegate.createSocket(address, port, localAddress, localPort), + address.getHostName()); + } + + private class TlsOnlySSLSocket extends DelegateSSLSocket { + + final boolean compatible; + + private TlsOnlySSLSocket(SSLSocket delegate, boolean compatible) { + super(delegate); + this.compatible = compatible; + + // badly configured servers can't handle a good config + if (compatible) { + ArrayList protocols = new ArrayList(Arrays.asList(delegate + .getEnabledProtocols())); + protocols.remove("SSLv2"); + protocols.remove("SSLv3"); + super.setEnabledProtocols(protocols.toArray(new String[protocols.size()])); + + /* + * Exclude extremely weak EXPORT ciphers. NULL ciphers should + * never even have been an option in TLS. + */ + ArrayList enabled = new ArrayList(10); + Pattern exclude = Pattern.compile(".*(EXPORT|NULL).*"); + for (String cipher : delegate.getEnabledCipherSuites()) { + if (!exclude.matcher(cipher).matches()) { + enabled.add(cipher); + } + } + super.setEnabledCipherSuites(enabled.toArray(new String[enabled.size()])); + return; + } // else + + // 16-19 support v1.1 and v1.2 but only by default starting in 20+ + // https://developer.android.com/reference/javax/net/ssl/SSLSocket.html + ArrayList protocols = new ArrayList(Arrays.asList(delegate + .getSupportedProtocols())); + protocols.remove("SSLv2"); + protocols.remove("SSLv3"); + super.setEnabledProtocols(protocols.toArray(new String[protocols.size()])); + + /* + * Exclude weak ciphers, like EXPORT, MD5, DES, and DH. NULL ciphers + * should never even have been an option in TLS. + */ + ArrayList enabledCiphers = new ArrayList(10); + Pattern exclude = Pattern.compile(".*(_DES|DH_|DSS|EXPORT|MD5|NULL|RC4).*"); + for (String cipher : delegate.getSupportedCipherSuites()) { + if (!exclude.matcher(cipher).matches()) { + enabledCiphers.add(cipher); + } + } + super.setEnabledCipherSuites(enabledCiphers.toArray(new String[enabledCiphers.size()])); + } + + /** + * This works around a bug in Android < 19 where SSLv3 is forced + */ + @Override + public void setEnabledProtocols(String[] protocols) { + if (protocols != null && protocols.length == 1 && "SSLv3".equals(protocols[0])) { + List systemProtocols; + if (this.compatible) { + systemProtocols = Arrays.asList(delegate.getEnabledProtocols()); + } else { + systemProtocols = Arrays.asList(delegate.getSupportedProtocols()); + } + List enabledProtocols = new ArrayList(systemProtocols); + if (enabledProtocols.size() > 1) { + enabledProtocols.remove("SSLv2"); + enabledProtocols.remove("SSLv3"); + } else { + Log.w(TAG, "SSL stuck with protocol available for " + + String.valueOf(enabledProtocols)); + } + protocols = enabledProtocols.toArray(new String[enabledProtocols.size()]); + } + super.setEnabledProtocols(protocols); + } + } + + public class DelegateSSLSocket extends SSLSocket { + + protected final SSLSocket delegate; + + DelegateSSLSocket(SSLSocket delegate) { + this.delegate = delegate; + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public String[] getEnabledCipherSuites() { + return delegate.getEnabledCipherSuites(); + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + delegate.setEnabledCipherSuites(suites); + } + + @Override + public String[] getSupportedProtocols() { + return delegate.getSupportedProtocols(); + } + + @Override + public String[] getEnabledProtocols() { + return delegate.getEnabledProtocols(); + } + + @Override + public void setEnabledProtocols(String[] protocols) { + delegate.setEnabledProtocols(protocols); + } + + @Override + public SSLSession getSession() { + return delegate.getSession(); + } + + @Override + public void addHandshakeCompletedListener(HandshakeCompletedListener listener) { + delegate.addHandshakeCompletedListener(listener); + } + + @Override + public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) { + delegate.removeHandshakeCompletedListener(listener); + } + + @Override + public void startHandshake() throws IOException { + delegate.startHandshake(); + } + + @Override + public void setUseClientMode(boolean mode) { + delegate.setUseClientMode(mode); + } + + @Override + public boolean getUseClientMode() { + return delegate.getUseClientMode(); + } + + @Override + public void setNeedClientAuth(boolean need) { + delegate.setNeedClientAuth(need); + } + + @Override + public void setWantClientAuth(boolean want) { + delegate.setWantClientAuth(want); + } + + @Override + public boolean getNeedClientAuth() { + return delegate.getNeedClientAuth(); + } + + @Override + public boolean getWantClientAuth() { + return delegate.getWantClientAuth(); + } + + @Override + public void setEnableSessionCreation(boolean flag) { + delegate.setEnableSessionCreation(flag); + } + + @Override + public boolean getEnableSessionCreation() { + return delegate.getEnableSessionCreation(); + } + + @Override + public void bind(SocketAddress localAddr) throws IOException { + delegate.bind(localAddr); + } + + @Override + public synchronized void close() throws IOException { + delegate.close(); + } + + @Override + public void connect(SocketAddress remoteAddr) throws IOException { + delegate.connect(remoteAddr); + } + + @Override + public void connect(SocketAddress remoteAddr, int timeout) throws IOException { + delegate.connect(remoteAddr, timeout); + } + + @Override + public SocketChannel getChannel() { + return delegate.getChannel(); + } + + @Override + public InetAddress getInetAddress() { + return delegate.getInetAddress(); + } + + @Override + public InputStream getInputStream() throws IOException { + return delegate.getInputStream(); + } + + @Override + public boolean getKeepAlive() throws SocketException { + return delegate.getKeepAlive(); + } + + @Override + public InetAddress getLocalAddress() { + return delegate.getLocalAddress(); + } + + @Override + public int getLocalPort() { + return delegate.getLocalPort(); + } + + @Override + public SocketAddress getLocalSocketAddress() { + return delegate.getLocalSocketAddress(); + } + + @Override + public boolean getOOBInline() throws SocketException { + return delegate.getOOBInline(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return delegate.getOutputStream(); + } + + @Override + public int getPort() { + return delegate.getPort(); + } + + @Override + public synchronized int getReceiveBufferSize() throws SocketException { + return delegate.getReceiveBufferSize(); + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return delegate.getRemoteSocketAddress(); + } + + @Override + public boolean getReuseAddress() throws SocketException { + return delegate.getReuseAddress(); + } + + @Override + public synchronized int getSendBufferSize() throws SocketException { + return delegate.getSendBufferSize(); + } + + @Override + public int getSoLinger() throws SocketException { + return delegate.getSoLinger(); + } + + @Override + public synchronized int getSoTimeout() throws SocketException { + return delegate.getSoTimeout(); + } + + @Override + public boolean getTcpNoDelay() throws SocketException { + return delegate.getTcpNoDelay(); + } + + @Override + public int getTrafficClass() throws SocketException { + return delegate.getTrafficClass(); + } + + @Override + public boolean isBound() { + return delegate.isBound(); + } + + @Override + public boolean isClosed() { + return delegate.isClosed(); + } + + @Override + public boolean isConnected() { + return delegate.isConnected(); + } + + @Override + public boolean isInputShutdown() { + return delegate.isInputShutdown(); + } + + @Override + public boolean isOutputShutdown() { + return delegate.isOutputShutdown(); + } + + @Override + public void sendUrgentData(int value) throws IOException { + delegate.sendUrgentData(value); + } + + @Override + public void setKeepAlive(boolean keepAlive) throws SocketException { + delegate.setKeepAlive(keepAlive); + } + + @Override + public void setOOBInline(boolean oobinline) throws SocketException { + delegate.setOOBInline(oobinline); + } + + @Override + public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { + delegate.setPerformancePreferences(connectionTime, + latency, bandwidth); + } + + @Override + public synchronized void setReceiveBufferSize(int size) throws SocketException { + delegate.setReceiveBufferSize(size); + } + + @Override + public void setReuseAddress(boolean reuse) throws SocketException { + delegate.setReuseAddress(reuse); + } + + @Override + public synchronized void setSendBufferSize(int size) throws SocketException { + delegate.setSendBufferSize(size); + } + + @Override + public void setSoLinger(boolean on, int timeout) throws SocketException { + delegate.setSoLinger(on, timeout); + } + + @Override + public synchronized void setSoTimeout(int timeout) throws SocketException { + delegate.setSoTimeout(timeout); + } + + @Override + public void setTcpNoDelay(boolean on) throws SocketException { + delegate.setTcpNoDelay(on); + } + + @Override + public void setTrafficClass(int value) throws SocketException { + delegate.setTrafficClass(value); + } + + @Override + public void shutdownInput() throws IOException { + delegate.shutdownInput(); + } + + @Override + public void shutdownOutput() throws IOException { + delegate.shutdownOutput(); + } + + // inspired by https://github.com/k9mail/k-9/commit/54f9fd36a77423a55f63fbf9b1bcea055a239768 + + public DelegateSSLSocket setHostname(String host) { + try { + delegate + .getClass() + .getMethod("setHostname", String.class) + .invoke(delegate, host); + } + catch (Exception e) { + throw new IllegalStateException("Could not enable SNI", e); + } + + return(this); + } + + @Override + public String toString() { + return delegate.toString(); + } + + @Override + public boolean equals(Object o) { + return delegate.equals(o); + } + } +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/OrbotHelper.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/OrbotHelper.java new file mode 100644 index 00000000..d5a39fd2 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/OrbotHelper.java @@ -0,0 +1,701 @@ +/* + * Copyright 2014-2016 Hans-Christoph Steiner + * Copyright 2012-2016 Nathan Freitas + * Portions Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.proxy; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Log; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Utility class to simplify setting up a proxy connection + * to Orbot. + * + * If you are using classes in the info.guardianproject.netcipher.client + * package, call OrbotHelper.get(this).init(); from onCreate() + * of a custom Application subclass, or from some other guaranteed + * entry point to your app. At that point, the + * info.guardianproject.netcipher.client classes will be ready + * for use. + */ +public class OrbotHelper implements ProxyHelper { + + private final static int REQUEST_CODE_STATUS = 100; + + public final static String ORBOT_PACKAGE_NAME = "org.torproject.android"; + public final static String ORBOT_MARKET_URI = "market://details?id=" + ORBOT_PACKAGE_NAME; + public final static String ORBOT_FDROID_URI = "https://f-droid.org/repository/browse/?fdid=" + + ORBOT_PACKAGE_NAME; + public final static String ORBOT_PLAY_URI = "https://play.google.com/store/apps/details?id=" + + ORBOT_PACKAGE_NAME; + + /** + * A request to Orbot to transparently start Tor services + */ + public final static String ACTION_START = "org.torproject.android.intent.action.START"; + + /** + * {@link Intent} send by Orbot with {@code ON/OFF/STARTING/STOPPING} status + * included as an {@link #EXTRA_STATUS} {@code String}. Your app should + * always receive {@code ACTION_STATUS Intent}s since any other app could + * start Orbot. Also, user-triggered starts and stops will also cause + * {@code ACTION_STATUS Intent}s to be broadcast. + */ + public final static String ACTION_STATUS = "org.torproject.android.intent.action.STATUS"; + + /** + * {@code String} that contains a status constant: {@link #STATUS_ON}, + * {@link #STATUS_OFF}, {@link #STATUS_STARTING}, or + * {@link #STATUS_STOPPING} + */ + public final static String EXTRA_STATUS = "org.torproject.android.intent.extra.STATUS"; + /** + * A {@link String} {@code packageName} for Orbot to direct its status reply + * to, used in {@link #ACTION_START} {@link Intent}s sent to Orbot + */ + public final static String EXTRA_PACKAGE_NAME = "org.torproject.android.intent.extra.PACKAGE_NAME"; + + public final static String EXTRA_PROXY_PORT_HTTP = "org.torproject.android.intent.extra.HTTP_PROXY_PORT"; + public final static String EXTRA_PROXY_PORT_SOCKS = "org.torproject.android.intent.extra.SOCKS_PROXY_PORT"; + + + /** + * All tor-related services and daemons are stopped + */ + public final static String STATUS_OFF = "OFF"; + /** + * All tor-related services and daemons have completed starting + */ + public final static String STATUS_ON = "ON"; + public final static String STATUS_STARTING = "STARTING"; + public final static String STATUS_STOPPING = "STOPPING"; + /** + * The user has disabled the ability for background starts triggered by + * apps. Fallback to the old Intent that brings up Orbot. + */ + public final static String STATUS_STARTS_DISABLED = "STARTS_DISABLED"; + + public final static String ACTION_START_TOR = "org.torproject.android.START_TOR"; + public final static String ACTION_REQUEST_HS = "org.torproject.android.REQUEST_HS_PORT"; + public final static int START_TOR_RESULT = 0x9234; + public final static int HS_REQUEST_CODE = 9999; + + +/* + private OrbotHelper() { + // only static utility methods, do not instantiate + } +*/ + + /** + * Test whether a {@link URL} is a Tor Hidden Service host name, also known + * as an ".onion address". + * + * @return whether the host name is a Tor .onion address + */ + public static boolean isOnionAddress(URL url) { + return url.getHost().endsWith(".onion"); + } + + /** + * Test whether a URL {@link String} is a Tor Hidden Service host name, also known + * as an ".onion address". + * + * @return whether the host name is a Tor .onion address + */ + public static boolean isOnionAddress(String urlString) { + try { + return isOnionAddress(new URL(urlString)); + } catch (MalformedURLException e) { + return false; + } + } + + /** + * Test whether a {@link Uri} is a Tor Hidden Service host name, also known + * as an ".onion address". + * + * @return whether the host name is a Tor .onion address + */ + public static boolean isOnionAddress(Uri uri) { + return uri.getHost().endsWith(".onion"); + } + + /** + * Check if the tor process is running. This method is very + * brittle, and is therefore deprecated in favor of using the + * {@link #ACTION_STATUS} {@code Intent} along with the + * {@link #requestStartTor(Context)} method. + */ + @Deprecated + public static boolean isOrbotRunning(Context context) { + int procId = TorServiceUtils.findProcessId(context); + + return (procId != -1); + } + + public static boolean isOrbotInstalled(Context context) { + return isAppInstalled(context, ORBOT_PACKAGE_NAME); + } + + private static boolean isAppInstalled(Context context, String uri) { + try { + PackageManager pm = context.getPackageManager(); + pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + public static void requestHiddenServiceOnPort(Activity activity, int port) { + Intent intent = new Intent(ACTION_REQUEST_HS); + intent.setPackage(ORBOT_PACKAGE_NAME); + intent.putExtra("hs_port", port); + + activity.startActivityForResult(intent, HS_REQUEST_CODE); + } + + /** + * First, checks whether Orbot is installed. If Orbot is installed, then a + * broadcast {@link Intent} is sent to request Orbot to start + * transparently in the background. When Orbot receives this {@code + * Intent}, it will immediately reply to the app that called this method + * with an {@link #ACTION_STATUS} {@code Intent} that is broadcast to the + * {@code packageName} of the provided {@link Context} (i.e. {@link + * Context#getPackageName()}. + *

+ * That reply {@link #ACTION_STATUS} {@code Intent} could say that the user + * has disabled background starts with the status + * {@link #STATUS_STARTS_DISABLED}. That means that Orbot ignored this + * request. To directly prompt the user to start Tor, use + * {@link #requestShowOrbotStart(Activity)}, which will bring up + * Orbot itself for the user to manually start Tor. Orbot always broadcasts + * it's status, so your app will receive those no matter how Tor gets + * started. + * + * @param context the app {@link Context} will receive the reply + * @return whether the start request was sent to Orbot + * @see #requestShowOrbotStart(Activity activity) + */ + public static boolean requestStartTor(Context context) { + if (OrbotHelper.isOrbotInstalled(context)) { + Log.i("OrbotHelper", "requestStartTor " + context.getPackageName()); + Intent intent = getOrbotStartIntent(context); + context.sendBroadcast(intent); + return true; + } + return false; + } + + /** + * Gets an {@link Intent} for starting Orbot. Orbot will reply with the + * current status to the {@code packageName} of the app in the provided + * {@link Context} (i.e. {@link Context#getPackageName()}. + */ + public static Intent getOrbotStartIntent(Context context) { + Intent intent = new Intent(ACTION_START); + intent.setPackage(ORBOT_PACKAGE_NAME); + intent.putExtra(EXTRA_PACKAGE_NAME, context.getPackageName()); + return intent; + } + + /** + * Gets a barebones {@link Intent} for starting Orbot. This is deprecated + * in favor of {@link #getOrbotStartIntent(Context)}. + */ + @Deprecated + public static Intent getOrbotStartIntent() { + Intent intent = new Intent(ACTION_START); + intent.setPackage(ORBOT_PACKAGE_NAME); + return intent; + } + + /** + * First, checks whether Orbot is installed, then checks whether Orbot is + * running. If Orbot is installed and not running, then an {@link Intent} is + * sent to request the user to start Orbot, which will show the main Orbot screen. + * The result will be returned in + * {@link Activity#onActivityResult(int requestCode, int resultCode, Intent data)} + * with a {@code requestCode} of {@code START_TOR_RESULT} + *

+ * Orbot will also always broadcast the status of starting Tor via the + * {@link #ACTION_STATUS} Intent, no matter how it is started. + * + * @param activity the {@code Activity} that gets the result of the + * {@link #START_TOR_RESULT} request + * @return whether the start request was sent to Orbot + * @see #requestStartTor(Context context) + */ + public static boolean requestShowOrbotStart(Activity activity) { + if (OrbotHelper.isOrbotInstalled(activity)) { + if (!OrbotHelper.isOrbotRunning(activity)) { + Intent intent = getShowOrbotStartIntent(); + activity.startActivityForResult(intent, START_TOR_RESULT); + return true; + } + } + return false; + } + + public static Intent getShowOrbotStartIntent() { + Intent intent = new Intent(ACTION_START_TOR); + intent.setPackage(ORBOT_PACKAGE_NAME); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } + + public static Intent getOrbotInstallIntent(Context context) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(ORBOT_MARKET_URI)); + + PackageManager pm = context.getPackageManager(); + List resInfos = pm.queryIntentActivities(intent, 0); + + String foundPackageName = null; + for (ResolveInfo r : resInfos) { + Log.i("OrbotHelper", "market: " + r.activityInfo.packageName); + if (TextUtils.equals(r.activityInfo.packageName, FDROID_PACKAGE_NAME) + || TextUtils.equals(r.activityInfo.packageName, PLAY_PACKAGE_NAME)) { + foundPackageName = r.activityInfo.packageName; + break; + } + } + + if (foundPackageName == null) { + intent.setData(Uri.parse(ORBOT_FDROID_URI)); + } else { + intent.setPackage(foundPackageName); + } + return intent; + } + + @Override + public boolean isInstalled(Context context) { + return isOrbotInstalled(context); + } + + @Override + public void requestStatus(Context context) { + isOrbotRunning(context); + } + + @Override + public boolean requestStart(Context context) { + return requestStartTor(context); + } + + @Override + public Intent getInstallIntent(Context context) { + return getOrbotInstallIntent(context); + } + + @Override + public Intent getStartIntent(Context context) { + return getOrbotStartIntent(); + } + + @Override + public String getName() { + return "Orbot"; + } + + /* MLM additions */ + + private final Context ctxt; + private final Handler handler; + private boolean isInstalled=false; + private Intent lastStatusIntent=null; + private Set statusCallbacks= + newSetFromMap(new WeakHashMap()); + private Set installCallbacks= + newSetFromMap(new WeakHashMap()); + private long statusTimeoutMs=30000L; + private long installTimeoutMs=60000L; + private boolean validateOrbot=true; + + abstract public static class SimpleStatusCallback + implements StatusCallback { + @Override + public void onEnabled(Intent statusIntent) { + // no-op; extend and override if needed + } + + @Override + public void onStarting() { + // no-op; extend and override if needed + } + + @Override + public void onStopping() { + // no-op; extend and override if needed + } + + @Override + public void onDisabled() { + // no-op; extend and override if needed + } + + @Override + public void onNotYetInstalled() { + // no-op; extend and override if needed + } + } + + /** + * Callback interface used for reporting the results of an + * attempt to install Orbot + */ + public interface InstallCallback { + void onInstalled(); + void onInstallTimeout(); + } + + private static volatile OrbotHelper INSTANCE; + + /** + * Retrieves the singleton, initializing if if needed + * + * @param ctxt any Context will do, as we will hold onto + * the Application + * @return the singleton + */ + synchronized public static OrbotHelper get(Context ctxt) { + if (INSTANCE==null) { + INSTANCE=new OrbotHelper(ctxt); + } + + return(INSTANCE); + } + + /** + * Standard constructor + * + * @param ctxt any Context will do; OrbotInitializer will hold + * onto the Application context + */ + private OrbotHelper(Context ctxt) { + this.ctxt=ctxt.getApplicationContext(); + this.handler=new Handler(Looper.getMainLooper()); + } + + /** + * Adds a StatusCallback to be called when we find out that + * Orbot is ready. If Orbot is ready for use, your callback + * will be called with onEnabled() immediately, before this + * method returns. + * + * @param cb a callback + * @return the singleton, for chaining + */ + public OrbotHelper addStatusCallback(StatusCallback cb) { + statusCallbacks.add(cb); + + if (lastStatusIntent!=null) { + String status= + lastStatusIntent.getStringExtra(OrbotHelper.EXTRA_STATUS); + + if (status.equals(OrbotHelper.STATUS_ON)) { + cb.onEnabled(lastStatusIntent); + } + } + + return(this); + } + + /** + * Removes an existing registered StatusCallback. + * + * @param cb the callback to remove + * @return the singleton, for chaining + */ + public OrbotHelper removeStatusCallback(StatusCallback cb) { + statusCallbacks.remove(cb); + + return(this); + } + + + /** + * Adds an InstallCallback to be called when we find out that + * Orbot is installed + * + * @param cb a callback + * @return the singleton, for chaining + */ + public OrbotHelper addInstallCallback(InstallCallback cb) { + installCallbacks.add(cb); + + return(this); + } + + /** + * Removes an existing registered InstallCallback. + * + * @param cb the callback to remove + * @return the singleton, for chaining + */ + public OrbotHelper removeInstallCallback(InstallCallback cb) { + installCallbacks.remove(cb); + + return(this); + } + + /** + * Sets how long of a delay, in milliseconds, after trying + * to get a status from Orbot before we give up. + * Defaults to 30000ms = 30 seconds = 0.000347222 days + * + * @param timeoutMs delay period in milliseconds + * @return the singleton, for chaining + */ + public OrbotHelper statusTimeout(long timeoutMs) { + statusTimeoutMs=timeoutMs; + + return(this); + } + + /** + * Sets how long of a delay, in milliseconds, after trying + * to install Orbot do we assume that it's not happening. + * Defaults to 60000ms = 60 seconds = 1 minute = 1.90259e-6 years + * + * @param timeoutMs delay period in milliseconds + * @return the singleton, for chaining + */ + public OrbotHelper installTimeout(long timeoutMs) { + installTimeoutMs=timeoutMs; + + return(this); + } + + /** + * By default, NetCipher ensures that the Orbot on the + * device is one of the official builds. Call this method + * to skip that validation. Mostly, this is for developers + * who have their own custom Orbot builds (e.g., for + * dedicated hardware). + * + * @return the singleton, for chaining + */ + public OrbotHelper skipOrbotValidation() { + validateOrbot=false; + + return(this); + } + + /** + * @return true if Orbot is installed (the last time we checked), + * false otherwise + */ + public boolean isInstalled() { + return(isInstalled); + } + + /** + * Initializes the connection to Orbot, revalidating that it + * is installed and requesting fresh status broadcasts. + * + * @return true if initialization is proceeding, false if + * Orbot is not installed + */ + public boolean init() { + Intent orbot=OrbotHelper.getOrbotStartIntent(ctxt); + + if (validateOrbot) { + ArrayList hashes=new ArrayList(); + + hashes.add("A4:54:B8:7A:18:47:A8:9E:D7:F5:E7:0F:BA:6B:BA:96:F3:EF:29:C2:6E:09:81:20:4F:E3:47:BF:23:1D:FD:5B"); + hashes.add("A7:02:07:92:4F:61:FF:09:37:1D:54:84:14:5C:4B:EE:77:2C:55:C1:9E:EE:23:2F:57:70:E1:82:71:F7:CB:AE"); + + orbot= + SignatureUtils.validateBroadcastIntent(ctxt, orbot, + hashes, false); + } + + if (orbot!=null) { + isInstalled=true; + handler.postDelayed(onStatusTimeout, statusTimeoutMs); + ctxt.registerReceiver(orbotStatusReceiver, + new IntentFilter(OrbotHelper.ACTION_STATUS)); + ctxt.sendBroadcast(orbot); + } + else { + isInstalled=false; + + for (StatusCallback cb : statusCallbacks) { + cb.onNotYetInstalled(); + } + } + + return(isInstalled); + } + + /** + * Given that init() returned false, calling installOrbot() + * will trigger an attempt to install Orbot from an available + * distribution channel (e.g., the Play Store). Only call this + * if the user is expecting it, such as in response to tapping + * a dialog button or an action bar item. + * + * Note that installation may take a long time, even if + * the user is proceeding with the installation, due to network + * speeds, waiting for user input, and so on. Either specify + * a long timeout, or consider the timeout to be merely advisory + * and use some other user input to cause you to try + * init() again after, presumably, Orbot has been installed + * and configured by the user. + * + * If the user does install Orbot, we will attempt init() + * again automatically. Hence, you will probably need user input + * to tell you when the user has gotten Orbot up and going. + * + * @param host the Activity that is triggering this work + */ + public void installOrbot(Activity host) { + handler.postDelayed(onInstallTimeout, installTimeoutMs); + + IntentFilter filter= + new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + + filter.addDataScheme("package"); + + ctxt.registerReceiver(orbotInstallReceiver, filter); + host.startActivity(OrbotHelper.getOrbotInstallIntent(ctxt)); + } + + private BroadcastReceiver orbotStatusReceiver=new BroadcastReceiver() { + @Override + public void onReceive(Context ctxt, Intent intent) { + if (TextUtils.equals(intent.getAction(), + OrbotHelper.ACTION_STATUS)) { + String status=intent.getStringExtra(OrbotHelper.EXTRA_STATUS); + + if (status.equals(OrbotHelper.STATUS_ON)) { + lastStatusIntent=intent; + handler.removeCallbacks(onStatusTimeout); + + for (StatusCallback cb : statusCallbacks) { + cb.onEnabled(intent); + } + } + else if (status.equals(OrbotHelper.STATUS_OFF)) { + for (StatusCallback cb : statusCallbacks) { + cb.onDisabled(); + } + } + else if (status.equals(OrbotHelper.STATUS_STARTING)) { + for (StatusCallback cb : statusCallbacks) { + cb.onStarting(); + } + } + else if (status.equals(OrbotHelper.STATUS_STOPPING)) { + for (StatusCallback cb : statusCallbacks) { + cb.onStopping(); + } + } + } + } + }; + + private Runnable onStatusTimeout=new Runnable() { + @Override + public void run() { + ctxt.unregisterReceiver(orbotStatusReceiver); + + for (StatusCallback cb : statusCallbacks) { + cb.onStatusTimeout(); + } + } + }; + + private BroadcastReceiver orbotInstallReceiver=new BroadcastReceiver() { + @Override + public void onReceive(Context ctxt, Intent intent) { + if (TextUtils.equals(intent.getAction(), + Intent.ACTION_PACKAGE_ADDED)) { + String pkgName=intent.getData().getEncodedSchemeSpecificPart(); + + if (OrbotHelper.ORBOT_PACKAGE_NAME.equals(pkgName)) { + isInstalled=true; + handler.removeCallbacks(onInstallTimeout); + ctxt.unregisterReceiver(orbotInstallReceiver); + + for (InstallCallback cb : installCallbacks) { + cb.onInstalled(); + } + + init(); + } + } + } + }; + + private Runnable onInstallTimeout=new Runnable() { + @Override + public void run() { + ctxt.unregisterReceiver(orbotInstallReceiver); + + for (InstallCallback cb : installCallbacks) { + cb.onInstallTimeout(); + } + } + }; + + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + static Set newSetFromMap(Map map) { + if (map.isEmpty()) { + return new SetFromMap(map); + } + throw new IllegalArgumentException("map not empty"); + } +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/ProxyHelper.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/ProxyHelper.java new file mode 100644 index 00000000..3fb018bb --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/ProxyHelper.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2016 Nathan Freitas + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.darkweb.genesissearchengine.netcipher.proxy; + +import android.content.Context; +import android.content.Intent; + +public interface ProxyHelper { + + public boolean isInstalled (Context context); + public void requestStatus (Context context); + public boolean requestStart (Context context); + public Intent getInstallIntent (Context context); + public Intent getStartIntent (Context context); + public String getName (); + + public final static String FDROID_PACKAGE_NAME = "org.fdroid.fdroid"; + public final static String PLAY_PACKAGE_NAME = "com.android.vending"; + + /** + * A request to Orbot to transparently start Tor services + */ + public final static String ACTION_START = "android.intent.action.PROXY_START"; + /** + * {@link Intent} send by Orbot with {@code ON/OFF/STARTING/STOPPING} status + */ + public final static String ACTION_STATUS = "android.intent.action.PROXY_STATUS"; + /** + * {@code String} that contains a status constant: {@link #STATUS_ON}, + * {@link #STATUS_OFF}, {@link #STATUS_STARTING}, or + * {@link #STATUS_STOPPING} + */ + public final static String EXTRA_STATUS = "android.intent.extra.PROXY_STATUS"; + + public final static String EXTRA_PROXY_PORT_HTTP = "android.intent.extra.PROXY_PORT_HTTP"; + public final static String EXTRA_PROXY_PORT_SOCKS = "android.intent.extra.PROXY_PORT_SOCKS"; + + /** + * A {@link String} {@code packageName} for Orbot to direct its status reply + * to, used in {@link #ACTION_START} {@link Intent}s sent to Orbot + */ + public final static String EXTRA_PACKAGE_NAME = "android.intent.extra.PROXY_PACKAGE_NAME"; + + /** + * All tor-related services and daemons are stopped + */ + public final static String STATUS_OFF = "OFF"; + /** + * All tor-related services and daemons have completed starting + */ + public final static String STATUS_ON = "ON"; + public final static String STATUS_STARTING = "STARTING"; + public final static String STATUS_STOPPING = "STOPPING"; + /** + * The user has disabled the ability for background starts triggered by + * apps. Fallback to the old Intent that brings up Orbot. + */ + public final static String STATUS_STARTS_DISABLED = "STARTS_DISABLED"; +} + diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/ProxySelector.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/ProxySelector.java new file mode 100644 index 00000000..633aeb60 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/ProxySelector.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.darkweb.genesissearchengine.netcipher.proxy; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.SocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import android.util.Log; + +public class ProxySelector extends java.net.ProxySelector { + + private ArrayList listProxies; + + public ProxySelector () + { + super (); + + listProxies = new ArrayList(); + + + } + + public void addProxy (Proxy.Type type,String host, int port) + { + Proxy proxy = new Proxy(type,new InetSocketAddress(host, port)); + listProxies.add(proxy); + } + + @Override + public void connectFailed(URI uri, SocketAddress address, + IOException failure) { + Log.w("ProxySelector","could not connect to " + address.toString() + ": " + failure.getMessage()); + } + + @Override + public List select(URI uri) { + + return listProxies; + } + +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/PsiphonHelper.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/PsiphonHelper.java new file mode 100644 index 00000000..a94f7a47 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/PsiphonHelper.java @@ -0,0 +1,177 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.darkweb.genesissearchengine.netcipher.proxy; + +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.List; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.text.TextUtils; + +public class PsiphonHelper implements ProxyHelper { + + public final static String PACKAGE_NAME = "com.psiphon3"; + public final static String COMPONENT_NAME = "com.psiphon3.StatusActivity"; + + + public final static String MARKET_URI = "market://details?id=" + PACKAGE_NAME; + public final static String FDROID_URI = "https://f-droid.org/repository/browse/?fdid=" + + PACKAGE_NAME; + public final static String ORBOT_PLAY_URI = "https://play.google.com/store/apps/details?id=" + + PACKAGE_NAME; + + public final static int DEFAULT_SOCKS_PORT = 1080; + public final static int DEFAULT_HTTP_PORT = 8080; + + @Override + public boolean isInstalled(Context context) { + return isAppInstalled(context, PACKAGE_NAME); + } + + + private static boolean isAppInstalled(Context context, String uri) { + try { + PackageManager pm = context.getPackageManager(); + pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + @Override + public void requestStatus(final Context context) { + + Thread thread = new Thread () + { + public void run () + { + //can connect to default HTTP proxy port? + boolean isSocksOpen = false; + boolean isHttpOpen = false; + + int socksPort = DEFAULT_SOCKS_PORT; + int httpPort = DEFAULT_HTTP_PORT; + + for (int i = 0; i < 10 && (!isSocksOpen); i++) + isSocksOpen = isPortOpen("127.0.0.1",socksPort++,100); + + for (int i = 0; i < 10 && (!isHttpOpen); i++) + isHttpOpen = isPortOpen("127.0.0.1",httpPort++,100); + + //any other check? + + Intent intent = new Intent(ProxyHelper.ACTION_STATUS); + intent.putExtra(EXTRA_PACKAGE_NAME, PACKAGE_NAME); + + if (isSocksOpen && isHttpOpen) + { + intent.putExtra(EXTRA_STATUS, STATUS_ON); + + intent.putExtra(EXTRA_PROXY_PORT_HTTP, httpPort-1); + intent.putExtra(EXTRA_PROXY_PORT_SOCKS, socksPort-1); + + + } + else + { + intent.putExtra(EXTRA_STATUS, STATUS_OFF); + } + + context.sendBroadcast(intent); + } + }; + + thread.start(); + + } + + @Override + public boolean requestStart(Context context) { + + Intent intent = getStartIntent(context); + // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + + return true; + } + + @Override + public Intent getInstallIntent(Context context) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(MARKET_URI)); + + PackageManager pm = context.getPackageManager(); + List resInfos = pm.queryIntentActivities(intent, 0); + + String foundPackageName = null; + for (ResolveInfo r : resInfos) { + if (TextUtils.equals(r.activityInfo.packageName, FDROID_PACKAGE_NAME) + || TextUtils.equals(r.activityInfo.packageName, PLAY_PACKAGE_NAME)) { + foundPackageName = r.activityInfo.packageName; + break; + } + } + + if (foundPackageName == null) { + intent.setData(Uri.parse(FDROID_URI)); + } else { + intent.setPackage(foundPackageName); + } + return intent; + } + + @Override + public Intent getStartIntent(Context context) { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(PACKAGE_NAME, COMPONENT_NAME)); + + return intent; + } + + public static boolean isPortOpen(final String ip, final int port, final int timeout) { + try { + Socket socket = new Socket(); + socket.connect(new InetSocketAddress(ip, port), timeout); + socket.close(); + return true; + } + + catch(ConnectException ce){ + ce.printStackTrace(); + return false; + } + + catch (Exception ex) { + ex.printStackTrace(); + return false; + } + } + + + @Override + public String getName() { + return PACKAGE_NAME; + } + +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/SetFromMap.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/SetFromMap.java new file mode 100644 index 00000000..809824c4 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/SetFromMap.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.proxy; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +class SetFromMap extends AbstractSet + implements Serializable { + private static final long serialVersionUID = 2454657854757543876L; + // Must be named as is, to pass serialization compatibility test. + private final Map m; + private transient Set backingSet; + SetFromMap(final Map map) { + m = map; + backingSet = map.keySet(); + } + @Override public boolean equals(Object object) { + return backingSet.equals(object); + } + @Override public int hashCode() { + return backingSet.hashCode(); + } + @Override public boolean add(E object) { + return m.put(object, Boolean.TRUE) == null; + } + @Override public void clear() { + m.clear(); + } + @Override public String toString() { + return backingSet.toString(); + } + @Override public boolean contains(Object object) { + return backingSet.contains(object); + } + @Override public boolean containsAll(Collection collection) { + return backingSet.containsAll(collection); + } + @Override public boolean isEmpty() { + return m.isEmpty(); + } + @Override public boolean remove(Object object) { + return m.remove(object) != null; + } + @Override public boolean retainAll(Collection collection) { + return backingSet.retainAll(collection); + } + @Override public Object[] toArray() { + return backingSet.toArray(); + } + @Override + public T[] toArray(T[] contents) { + return backingSet.toArray(contents); + } + @Override public Iterator iterator() { + return backingSet.iterator(); + } + @Override public int size() { + return m.size(); + } + @SuppressWarnings("unchecked") + private void readObject(ObjectInputStream stream) + throws IOException, ClassNotFoundException { + stream.defaultReadObject(); + backingSet = m.keySet(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/SignatureUtils.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/SignatureUtils.java new file mode 100644 index 00000000..9d5f624c --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/SignatureUtils.java @@ -0,0 +1,476 @@ +/*** + Copyright (c) 2014 CommonsWare, LLC + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.proxy; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.Signature; +import android.util.Log; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +public class SignatureUtils { + public static String getOwnSignatureHash(Context ctxt) + throws + NameNotFoundException, + NoSuchAlgorithmException { + return(getSignatureHash(ctxt, ctxt.getPackageName())); + } + + public static String getSignatureHash(Context ctxt, String packageName) + throws + NameNotFoundException, + NoSuchAlgorithmException { + MessageDigest md=MessageDigest.getInstance("SHA-256"); + Signature sig= + ctxt.getPackageManager() + .getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures[0]; + + return(toHexStringWithColons(md.digest(sig.toByteArray()))); + } + + // based on https://stackoverflow.com/a/2197650/115145 + + public static String toHexStringWithColons(byte[] bytes) { + char[] hexArray= + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', + 'C', 'D', 'E', 'F' }; + char[] hexChars=new char[(bytes.length * 3) - 1]; + int v; + + for (int j=0; j < bytes.length; j++) { + v=bytes[j] & 0xFF; + hexChars[j * 3]=hexArray[v / 16]; + hexChars[j * 3 + 1]=hexArray[v % 16]; + + if (j < bytes.length - 1) { + hexChars[j * 3 + 2]=':'; + } + } + + return new String(hexChars); + } + + /** + * Confirms that the broadcast receiver for a given Intent + * has the desired signature hash. + * + * If you know the package name of the receiver, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has the proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some receiver whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * receiver was found that matches the Intent. If failIfHack + * is false, this means that no receiver was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching receiver + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching receiver, so the "broadcast" will only go to this + * one component. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to broadcast + * @param sigHash the signature hash of the app that you expect + * to handle this broadcast + * @param failIfHack true if you want a SecurityException if + * a matching receiver is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching receiver with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target receiver added to the Intent + */ + public static Intent validateBroadcastIntent(Context ctxt, + Intent toValidate, + String sigHash, + boolean failIfHack) { + ArrayList sigHashes=new ArrayList(); + + sigHashes.add(sigHash); + + return(validateBroadcastIntent(ctxt, toValidate, sigHashes, + failIfHack)); + } + + /** + * Confirms that the broadcast receiver for a given Intent + * has a desired signature hash. + * + * If you know the package name of the receiver, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has a proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some receiver whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * receiver was found that matches the Intent. If failIfHack + * is false, this means that no receiver was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching receiver + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching receiver, so the "broadcast" will only go to this + * one component. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to broadcast + * @param sigHashes the possible signature hashes of the app + * that you expect to handle this broadcast + * @param failIfHack true if you want a SecurityException if + * a matching receiver is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching receiver with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target receiver added to the Intent + */ + public static Intent validateBroadcastIntent(Context ctxt, + Intent toValidate, + List sigHashes, + boolean failIfHack) { + PackageManager pm=ctxt.getPackageManager(); + Intent result=null; + List receivers= + pm.queryBroadcastReceivers(toValidate, 0); + + if (receivers!=null) { + for (ResolveInfo info : receivers) { + try { + if (sigHashes.contains(getSignatureHash(ctxt, + info.activityInfo.packageName))) { + ComponentName cn= + new ComponentName(info.activityInfo.packageName, + info.activityInfo.name); + + result=new Intent(toValidate).setComponent(cn); + break; + } + else if (failIfHack) { + throw new SecurityException( + "Package has signature hash mismatch: "+ + info.activityInfo.packageName); + } + } + catch (NoSuchAlgorithmException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + catch (NameNotFoundException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + } + } + + return(result); + } + + /** + * Confirms that the activity for a given Intent has the + * desired signature hash. + * + * If you know the package name of the activity, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has the proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some activity whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * activity was found that matches the Intent. If failIfHack + * is false, this means that no activity was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching activity + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching activity, so a call to startActivity() for this + * Intent is guaranteed to go to this specific activity. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to use with + * startActivity() + * @param sigHash the signature hash of the app that you expect + * to handle this activity + * @param failIfHack true if you want a SecurityException if + * a matching activity is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching activity with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target activity added to the Intent + */ + public static Intent validateActivityIntent(Context ctxt, + Intent toValidate, + String sigHash, + boolean failIfHack) { + ArrayList sigHashes=new ArrayList(); + + sigHashes.add(sigHash); + + return(validateActivityIntent(ctxt, toValidate, sigHashes, + failIfHack)); + } + + /** + * Confirms that the activity for a given Intent has the + * desired signature hash. + * + * If you know the package name of the activity, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has the proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some activity whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * activity was found that matches the Intent. If failIfHack + * is false, this means that no activity was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching activity + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching activity, so a call to startActivity() for this + * Intent is guaranteed to go to this specific activity. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to use with + * startActivity() + * @param sigHashes the signature hashes of the app that you expect + * to handle this activity + * @param failIfHack true if you want a SecurityException if + * a matching activity is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching activity with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target activity added to the Intent + */ + public static Intent validateActivityIntent(Context ctxt, + Intent toValidate, + List sigHashes, + boolean failIfHack) { + PackageManager pm=ctxt.getPackageManager(); + Intent result=null; + List activities= + pm.queryIntentActivities(toValidate, 0); + + if (activities!=null) { + for (ResolveInfo info : activities) { + try { + if (sigHashes.contains(getSignatureHash(ctxt, + info.activityInfo.packageName))) { + ComponentName cn= + new ComponentName(info.activityInfo.packageName, + info.activityInfo.name); + + result=new Intent(toValidate).setComponent(cn); + break; + } + else if (failIfHack) { + throw new SecurityException( + "Package has signature hash mismatch: "+ + info.activityInfo.packageName); + } + } + catch (NoSuchAlgorithmException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + catch (NameNotFoundException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + } + } + + return(result); + } + + /** + * Confirms that the service for a given Intent has the + * desired signature hash. + * + * If you know the package name of the service, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has the proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some service whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * service was found that matches the Intent. If failIfHack + * is false, this means that no service was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching service + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching service, so a call to startService() or + * bindService() for this Intent is guaranteed to go to this + * specific service. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to use with + * startService() or bindService() + * @param sigHash the signature hash of the app that you expect + * to handle this service + * @param failIfHack true if you want a SecurityException if + * a matching service is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching service with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target service added to the Intent + */ + public static Intent validateServiceIntent(Context ctxt, + Intent toValidate, + String sigHash, + boolean failIfHack) { + ArrayList sigHashes=new ArrayList(); + + sigHashes.add(sigHash); + + return(validateServiceIntent(ctxt, toValidate, sigHashes, + failIfHack)); + } + + /** + * Confirms that the service for a given Intent has the + * desired signature hash. + * + * If you know the package name of the service, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has the proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some service whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * service was found that matches the Intent. If failIfHack + * is false, this means that no service was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching service + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching service, so a call to startService() or + * bindService() for this Intent is guaranteed to go to this + * specific service. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to use with + * startService() or bindService() + * @param sigHashes the signature hash of the app that you expect + * to handle this service + * @param failIfHack true if you want a SecurityException if + * a matching service is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching service with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target service added to the Intent + */ + public static Intent validateServiceIntent(Context ctxt, + Intent toValidate, + List sigHashes, + boolean failIfHack) { + PackageManager pm=ctxt.getPackageManager(); + Intent result=null; + List services= + pm.queryIntentServices(toValidate, 0); + + if (services!=null) { + for (ResolveInfo info : services) { + try { + if (sigHashes.contains(getSignatureHash(ctxt, + info.serviceInfo.packageName))) { + ComponentName cn= + new ComponentName(info.serviceInfo.packageName, + info.serviceInfo.name); + + result=new Intent(toValidate).setComponent(cn); + break; + } + else if (failIfHack) { + throw new SecurityException( + "Package has signature hash mismatch: "+ + info.activityInfo.packageName); + } + } + catch (NoSuchAlgorithmException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + catch (NameNotFoundException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + } + } + + return(result); + } +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/StatusCallback.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/StatusCallback.java new file mode 100644 index 00000000..6fc1a14a --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/StatusCallback.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.proxy; + +import android.content.Intent; + +/** + * Callback interface used for reporting Orbot status + */ +public interface StatusCallback { + /** + * Called when Orbot is operational + * + * @param statusIntent an Intent containing information about + * Orbot, including proxy ports + */ + void onEnabled(Intent statusIntent); + + /** + * Called when Orbot reports that it is starting up + */ + void onStarting(); + + /** + * Called when Orbot reports that it is shutting down + */ + void onStopping(); + + /** + * Called when Orbot reports that it is no longer running + */ + void onDisabled(); + + /** + * Called if our attempt to get a status from Orbot failed + * after a defined period of time. See statusTimeout() on + * OrbotInitializer. + */ + void onStatusTimeout(); + + /** + * Called if Orbot is not yet installed. Usually, you handle + * this by checking the return value from init() on OrbotInitializer + * or calling isInstalled() on OrbotInitializer. However, if + * you have need for it, if a callback is registered before + * an init() call determines that Orbot is not installed, your + * callback will be called with onNotYetInstalled(). + */ + void onNotYetInstalled(); +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/TorServiceUtils.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/TorServiceUtils.java new file mode 100644 index 00000000..da86c6f9 --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/proxy/TorServiceUtils.java @@ -0,0 +1,246 @@ +/* + * Copyright 2009-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.proxy; + +import android.content.Context; +import android.util.Log; + + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.URLEncoder; +import java.util.StringTokenizer; + +public class TorServiceUtils { + + private final static String TAG = "TorUtils"; + // various console cmds + public final static String SHELL_CMD_CHMOD = "chmod"; + public final static String SHELL_CMD_KILL = "kill -9"; + public final static String SHELL_CMD_RM = "rm"; + public final static String SHELL_CMD_PS = "ps"; + public final static String SHELL_CMD_PIDOF = "pidof"; + + public final static String CHMOD_EXE_VALUE = "700"; + + public static boolean isRootPossible() + { + + StringBuilder log = new StringBuilder(); + + try { + + // Check if Superuser.apk exists + File fileSU = new File("/system/app/Superuser.apk"); + if (fileSU.exists()) + return true; + + fileSU = new File("/system/app/superuser.apk"); + if (fileSU.exists()) + return true; + + fileSU = new File("/system/bin/su"); + if (fileSU.exists()) + { + String[] cmd = { + "su" + }; + int exitCode = TorServiceUtils.doShellCommand(cmd, log, false, true); + if (exitCode != 0) + return false; + else + return true; + } + + // Check for 'su' binary + String[] cmd = { + "which su" + }; + int exitCode = TorServiceUtils.doShellCommand(cmd, log, false, true); + + if (exitCode == 0) { + Log.d(TAG, "root exists, but not sure about permissions"); + return true; + + } + + } catch (IOException e) { + // this means that there is no root to be had (normally) so we won't + // log anything + Log.e(TAG, "Error checking for root access", e); + + } catch (Exception e) { + Log.e(TAG, "Error checking for root access", e); + // this means that there is no root to be had (normally) + } + + Log.e(TAG, "Could not acquire root permissions"); + + return false; + } + + public static int findProcessId(Context context) { + String dataPath = context.getFilesDir().getParentFile().getParentFile().getAbsolutePath(); + String command = dataPath + "/" + OrbotHelper.ORBOT_PACKAGE_NAME + "/app_bin/tor"; + int procId = -1; + + try { + procId = findProcessIdWithPidOf(command); + + if (procId == -1) + procId = findProcessIdWithPS(command); + } catch (Exception e) { + try { + procId = findProcessIdWithPS(command); + } catch (Exception e2) { + Log.e(TAG, "Unable to get proc id for command: " + URLEncoder.encode(command), e2); + } + } + + return procId; + } + + // use 'pidof' command + public static int findProcessIdWithPidOf(String command) throws Exception + { + + int procId = -1; + + Runtime r = Runtime.getRuntime(); + + Process procPs = null; + + String baseName = new File(command).getName(); + // fix contributed my mikos on 2010.12.10 + procPs = r.exec(new String[] { + SHELL_CMD_PIDOF, baseName + }); + // procPs = r.exec(SHELL_CMD_PIDOF); + + BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream())); + String line = null; + + while ((line = reader.readLine()) != null) + { + + try + { + // this line should just be the process id + procId = Integer.parseInt(line.trim()); + break; + } catch (NumberFormatException e) + { + Log.e("TorServiceUtils", "unable to parse process pid: " + line, e); + } + } + + return procId; + + } + + // use 'ps' command + public static int findProcessIdWithPS(String command) throws Exception + { + + int procId = -1; + + Runtime r = Runtime.getRuntime(); + + Process procPs = null; + + procPs = r.exec(SHELL_CMD_PS); + + BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream())); + String line = null; + + while ((line = reader.readLine()) != null) + { + if (line.indexOf(' ' + command) != -1) + { + + StringTokenizer st = new StringTokenizer(line, " "); + st.nextToken(); // proc owner + + procId = Integer.parseInt(st.nextToken().trim()); + + break; + } + } + + return procId; + + } + + public static int doShellCommand(String[] cmds, StringBuilder log, boolean runAsRoot, + boolean waitFor) throws Exception + { + + Process proc = null; + int exitCode = -1; + + if (runAsRoot) + proc = Runtime.getRuntime().exec("su"); + else + proc = Runtime.getRuntime().exec("sh"); + + OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream()); + + for (int i = 0; i < cmds.length; i++) + { + // TorService.logMessage("executing shell cmd: " + cmds[i] + + // "; runAsRoot=" + runAsRoot + ";waitFor=" + waitFor); + + out.write(cmds[i]); + out.write("\n"); + } + + out.flush(); + out.write("exit\n"); + out.flush(); + + if (waitFor) + { + + final char buf[] = new char[10]; + + // Consume the "stdout" + InputStreamReader reader = new InputStreamReader(proc.getInputStream()); + int read = 0; + while ((read = reader.read(buf)) != -1) { + if (log != null) + log.append(buf, 0, read); + } + + // Consume the "stderr" + reader = new InputStreamReader(proc.getErrorStream()); + read = 0; + while ((read = reader.read(buf)) != -1) { + if (log != null) + log.append(buf, 0, read); + } + + exitCode = proc.waitFor(); + + } + + return exitCode; + + } +} diff --git a/app/src/main/java/com/darkweb/genesissearchengine/netcipher/web/WebkitProxy.java b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/web/WebkitProxy.java new file mode 100644 index 00000000..d45ca73f --- /dev/null +++ b/app/src/main/java/com/darkweb/genesissearchengine/netcipher/web/WebkitProxy.java @@ -0,0 +1,832 @@ +/* + * Copyright 2015 Anthony Restaino + * Copyright 2012-2016 Nathan Freitas + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.darkweb.genesissearchengine.netcipher.web; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.Socket; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Proxy; +import android.net.Uri; +import android.os.Build; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Log; +import android.webkit.WebView; + +import ch.boye.httpclientandroidlib.HttpHost; + +public class WebkitProxy { + + private final static String DEFAULT_HOST = "localhost";//"127.0.0.1"; + private final static int DEFAULT_PORT = 8118; + private final static int DEFAULT_SOCKS_PORT = 9050; + + private final static int REQUEST_CODE = 0; + + private final static String TAG = "OrbotHelpher"; + + public static boolean setProxy(String appClass, Context ctx, WebView wView, String host, int port) throws Exception + { + + setSystemProperties(host, port); + + boolean worked = false; + + if (Build.VERSION.SDK_INT < 13) + { +// worked = setWebkitProxyGingerbread(ctx, host, port); + setProxyUpToHC(wView, host, port); + } + else if (Build.VERSION.SDK_INT < 19) + { + worked = setWebkitProxyICS(ctx, host, port); + } + else if (Build.VERSION.SDK_INT < 20) + { + worked = setKitKatProxy(appClass, ctx, host, port); + + if (!worked) //some kitkat's still use ICS browser component (like Cyanogen 11) + worked = setWebkitProxyICS(ctx, host, port); + + } + else if (Build.VERSION.SDK_INT >= 21) + { + worked = setWebkitProxyLollipop(ctx, host, port); + + } + + return worked; + } + + private static void setSystemProperties(String host, int port) + { + + System.setProperty("proxyHost", host); + System.setProperty("proxyPort", Integer.toString(port)); + + System.setProperty("http.proxyHost", host); + System.setProperty("http.proxyPort", Integer.toString(port)); + + System.setProperty("https.proxyHost", host); + System.setProperty("https.proxyPort", Integer.toString(port)); + + + System.setProperty("socks.proxyHost", host); + System.setProperty("socks.proxyPort", Integer.toString(DEFAULT_SOCKS_PORT)); + + System.setProperty("socksProxyHost", host); + System.setProperty("socksProxyPort", Integer.toString(DEFAULT_SOCKS_PORT)); + + + /* + ProxySelector pSelect = new ProxySelector(); + pSelect.addProxy(Proxy.Type.HTTP, host, port); + ProxySelector.setDefault(pSelect); + */ + /* + System.setProperty("http_proxy", "http://" + host + ":" + port); + System.setProperty("proxy-server", "http://" + host + ":" + port); + System.setProperty("host-resolver-rules","MAP * 0.0.0.0 , EXCLUDE myproxy"); + + System.getProperty("networkaddress.cache.ttl", "-1"); + */ + + } + + private static void resetSystemProperties() + { + + System.setProperty("proxyHost", ""); + System.setProperty("proxyPort", ""); + + System.setProperty("http.proxyHost", ""); + System.setProperty("http.proxyPort", ""); + + System.setProperty("https.proxyHost", ""); + System.setProperty("https.proxyPort", ""); + + + System.setProperty("socks.proxyHost", ""); + System.setProperty("socks.proxyPort", Integer.toString(DEFAULT_SOCKS_PORT)); + + System.setProperty("socksProxyHost", ""); + System.setProperty("socksProxyPort", Integer.toString(DEFAULT_SOCKS_PORT)); + + } + + /** + * Override WebKit Proxy settings + * + * @param ctx Android ApplicationContext + * @param host + * @param port + * @return true if Proxy was successfully set + */ + private static boolean setWebkitProxyGingerbread(Context ctx, String host, int port) + throws Exception + { + + boolean ret = false; + + Object requestQueueObject = getRequestQueue(ctx); + if (requestQueueObject != null) { + // Create Proxy config object and set it into request Q + HttpHost httpHost = new HttpHost(host, port, "http"); + setDeclaredField(requestQueueObject, "mProxyHost", httpHost); + return true; + } + return false; + + } + + +/** + * Set Proxy for Android 3.2 and below. + */ +@SuppressWarnings("all") +private static boolean setProxyUpToHC(WebView webview, String host, int port) { + Log.d(TAG, "Setting proxy with <= 3.2 API."); + + HttpHost proxyServer = new HttpHost(host, port); + // Getting network + Class networkClass = null; + Object network = null; + try { + networkClass = Class.forName("android.webkit.Network"); + if (networkClass == null) { + Log.e(TAG, "failed to get class for android.webkit.Network"); + return false; + } + Method getInstanceMethod = networkClass.getMethod("getInstance", Context.class); + if (getInstanceMethod == null) { + Log.e(TAG, "failed to get getInstance method"); + } + network = getInstanceMethod.invoke(networkClass, new Object[]{webview.getContext()}); + } catch (Exception ex) { + Log.e(TAG, "error getting network: " + ex); + return false; + } + if (network == null) { + Log.e(TAG, "error getting network: network is null"); + return false; + } + Object requestQueue = null; + try { + Field requestQueueField = networkClass + .getDeclaredField("mRequestQueue"); + requestQueue = getFieldValueSafely(requestQueueField, network); + } catch (Exception ex) { + Log.e(TAG, "error getting field value"); + return false; + } + if (requestQueue == null) { + Log.e(TAG, "Request queue is null"); + return false; + } + Field proxyHostField = null; + try { + Class requestQueueClass = Class.forName("android.net.http.RequestQueue"); + proxyHostField = requestQueueClass + .getDeclaredField("mProxyHost"); + } catch (Exception ex) { + Log.e(TAG, "error getting proxy host field"); + return false; + } + + boolean temp = proxyHostField.isAccessible(); + try { + proxyHostField.setAccessible(true); + proxyHostField.set(requestQueue, proxyServer); + } catch (Exception ex) { + Log.e(TAG, "error setting proxy host"); + } finally { + proxyHostField.setAccessible(temp); + } + + Log.d(TAG, "Setting proxy with <= 3.2 API successful!"); + return true; +} + + +private static Object getFieldValueSafely(Field field, Object classInstance) throws IllegalArgumentException, IllegalAccessException { + boolean oldAccessibleValue = field.isAccessible(); + field.setAccessible(true); + Object result = field.get(classInstance); + field.setAccessible(oldAccessibleValue); + return result; +} + + private static boolean setWebkitProxyICS(Context ctx, String host, int port) + { + + // PSIPHON: added support for Android 4.x WebView proxy + try + { + Class webViewCoreClass = Class.forName("android.webkit.WebViewCore"); + + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (webViewCoreClass != null && proxyPropertiesClass != null) + { + Method m = webViewCoreClass.getDeclaredMethod("sendStaticMessage", Integer.TYPE, + Object.class); + Constructor c = proxyPropertiesClass.getConstructor(String.class, Integer.TYPE, + String.class); + + if (m != null && c != null) + { + m.setAccessible(true); + c.setAccessible(true); + Object properties = c.newInstance(host, port, null); + + // android.webkit.WebViewCore.EventHub.PROXY_CHANGED = 193; + m.invoke(null, 193, properties); + + + return true; + } + + + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.net.ProxyProperties: " + + e.toString()); + } catch (Error e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.webkit.Network: " + + e.toString()); + } + + return false; + + } + + @TargetApi(19) + public static boolean resetKitKatProxy(String appClass, Context appContext) { + + return setKitKatProxy(appClass, appContext,null,0); + } + + @TargetApi(19) + private static boolean setKitKatProxy(String appClass, Context appContext, String host, int port) { + //Context appContext = webView.getContext().getApplicationContext(); + + if (host != null) + { + System.setProperty("http.proxyHost", host); + System.setProperty("http.proxyPort", Integer.toString(port)); + System.setProperty("https.proxyHost", host); + System.setProperty("https.proxyPort", Integer.toString(port)); + } + + try { + Class applictionCls = Class.forName(appClass); + Field loadedApkField = applictionCls.getField("mLoadedApk"); + loadedApkField.setAccessible(true); + Object loadedApk = loadedApkField.get(appContext); + Class loadedApkCls = Class.forName("android.app.LoadedApk"); + Field receiversField = loadedApkCls.getDeclaredField("mReceivers"); + receiversField.setAccessible(true); + ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk); + for (Object receiverMap : receivers.values()) { + for (Object rec : ((ArrayMap) receiverMap).keySet()) { + Class clazz = rec.getClass(); + if (clazz.getName().contains("ProxyChangeListener")) { + Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class); + Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); + + if (host != null) + { + /*********** optional, may be need in future *************/ + final String CLASS_NAME = "android.net.ProxyProperties"; + Class cls = Class.forName(CLASS_NAME); + Constructor constructor = cls.getConstructor(String.class, Integer.TYPE, String.class); + constructor.setAccessible(true); + Object proxyProperties = constructor.newInstance(host, port, null); + intent.putExtra("proxy", (Parcelable) proxyProperties); + /*********** optional, may be need in future *************/ + } + + onReceiveMethod.invoke(rec, appContext, intent); + } + } + } + return true; + } catch (ClassNotFoundException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (NoSuchFieldException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (IllegalAccessException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (IllegalArgumentException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (NoSuchMethodException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (InvocationTargetException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (InstantiationException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } + return false; } + + @TargetApi(21) + public static boolean resetLollipopProxy(String appClass, Context appContext) { + + return setWebkitProxyLollipop(appContext,null,0); + } + + // http://stackanswers.com/questions/25272393/android-webview-set-proxy-programmatically-on-android-l + @TargetApi(21) // for android.util.ArrayMap methods + @SuppressWarnings("rawtypes") + private static boolean setWebkitProxyLollipop(Context appContext, String host, int port) + { + System.setProperty("http.proxyHost", host); + System.setProperty("http.proxyPort", Integer.toString(port)); + System.setProperty("https.proxyHost", host); + System.setProperty("https.proxyPort", Integer.toString(port)); + try { + Class applictionClass = Class.forName("android.app.Application"); + Field mLoadedApkField = applictionClass.getDeclaredField("mLoadedApk"); + mLoadedApkField.setAccessible(true); + Object mloadedApk = mLoadedApkField.get(appContext); + Class loadedApkClass = Class.forName("android.app.LoadedApk"); + Field mReceiversField = loadedApkClass.getDeclaredField("mReceivers"); + mReceiversField.setAccessible(true); + ArrayMap receivers = (ArrayMap) mReceiversField.get(mloadedApk); + for (Object receiverMap : receivers.values()) + { + for (Object receiver : ((ArrayMap) receiverMap).keySet()) + { + Class clazz = receiver.getClass(); + if (clazz.getName().contains("ProxyChangeListener")) + { + Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class); + Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); + onReceiveMethod.invoke(receiver, appContext, intent); + } + } + } + return true; + } + catch (ClassNotFoundException e) + { + Log.d("ProxySettings","Exception setting WebKit proxy on Lollipop through ProxyChangeListener: " + e.toString()); + } + catch (NoSuchFieldException e) + { + Log.d("ProxySettings","Exception setting WebKit proxy on Lollipop through ProxyChangeListener: " + e.toString()); + } + catch (IllegalAccessException e) + { + Log.d("ProxySettings","Exception setting WebKit proxy on Lollipop through ProxyChangeListener: " + e.toString()); + } + catch (NoSuchMethodException e) + { + Log.d("ProxySettings","Exception setting WebKit proxy on Lollipop through ProxyChangeListener: " + e.toString()); + } + catch (InvocationTargetException e) + { + Log.d("ProxySettings","Exception setting WebKit proxy on Lollipop through ProxyChangeListener: " + e.toString()); + } + return false; + } + + private static boolean sendProxyChangedIntent(Context ctx, String host, int port) + { + + try + { + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (proxyPropertiesClass != null) + { + Constructor c = proxyPropertiesClass.getConstructor(String.class, Integer.TYPE, + String.class); + + if (c != null) + { + c.setAccessible(true); + Object properties = c.newInstance(host, port, null); + + Intent intent = new Intent(android.net.Proxy.PROXY_CHANGE_ACTION); + intent.putExtra("proxy",(Parcelable)properties); + ctx.sendBroadcast(intent); + + } + + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception sending Intent ",e); + } catch (Error e) + { + Log.e("ProxySettings", + "Exception sending Intent ",e); + } + + return false; + + } + + /** + private static boolean setKitKatProxy0(Context ctx, String host, int port) + { + + try + { + Class cmClass = Class.forName("android.net.ConnectivityManager"); + + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (cmClass != null && proxyPropertiesClass != null) + { + Constructor c = proxyPropertiesClass.getConstructor(String.class, Integer.TYPE, + String.class); + + if (c != null) + { + c.setAccessible(true); + + Object proxyProps = c.newInstance(host, port, null); + ConnectivityManager cm = + (ConnectivityManager)ctx.getSystemService(Context.CONNECTIVITY_SERVICE); + + Method mSetGlobalProxy = cmClass.getDeclaredMethod("setGlobalProxy", proxyPropertiesClass); + + mSetGlobalProxy.invoke(cm, proxyProps); + + return true; + } + + } + } catch (Exception e) + { + Log.e("ProxySettings", + "ConnectivityManager.setGlobalProxy ",e); + } + + return false; + + } + */ + //CommandLine.initFromFile(COMMAND_LINE_FILE); + + /** + private static boolean setKitKatProxy2 (Context ctx, String host, int port) + { + + String commandLinePath = "/data/local/tmp/orweb.conf"; + try + { + Class webViewCoreClass = Class.forName("org.chromium.content.common.CommandLine"); + + if (webViewCoreClass != null) + { + for (Method method : webViewCoreClass.getDeclaredMethods()) + { + Log.d("Orweb","Proxy methods: " + method.getName()); + } + + Method m = webViewCoreClass.getDeclaredMethod("initFromFile", + String.class); + + if (m != null) + { + m.setAccessible(true); + m.invoke(null, commandLinePath); + return true; + } + else + return false; + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.net.ProxyProperties: " + + e.toString()); + } catch (Error e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.webkit.Network: " + + e.toString()); + } + + return false; + } + + /** + private static boolean setKitKatProxy (Context ctx, String host, int port) + { + + try + { + Class webViewCoreClass = Class.forName("android.net.Proxy"); + + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (webViewCoreClass != null && proxyPropertiesClass != null) + { + for (Method method : webViewCoreClass.getDeclaredMethods()) + { + Log.d("Orweb","Proxy methods: " + method.getName()); + } + + Method m = webViewCoreClass.getDeclaredMethod("setHttpProxySystemProperty", + proxyPropertiesClass); + Constructor c = proxyPropertiesClass.getConstructor(String.class, Integer.TYPE, + String.class); + + if (m != null && c != null) + { + m.setAccessible(true); + c.setAccessible(true); + Object properties = c.newInstance(host, port, null); + + m.invoke(null, properties); + return true; + } + else + return false; + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.net.ProxyProperties: " + + e.toString()); + } catch (Error e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.webkit.Network: " + + e.toString()); + } + + return false; + } + + private static boolean resetProxyForKitKat () + { + + try + { + Class webViewCoreClass = Class.forName("android.net.Proxy"); + + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (webViewCoreClass != null && proxyPropertiesClass != null) + { + for (Method method : webViewCoreClass.getDeclaredMethods()) + { + Log.d("Orweb","Proxy methods: " + method.getName()); + } + + Method m = webViewCoreClass.getDeclaredMethod("setHttpProxySystemProperty", + proxyPropertiesClass); + + if (m != null) + { + m.setAccessible(true); + + m.invoke(null, null); + return true; + } + else + return false; + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.net.ProxyProperties: " + + e.toString()); + } catch (Error e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.webkit.Network: " + + e.toString()); + } + + return false; + }**/ + + public static void resetProxy(String appClass, Context ctx) throws Exception { + + resetSystemProperties(); + + if (Build.VERSION.SDK_INT < 14) + { + resetProxyForGingerBread(ctx); + } + else if (Build.VERSION.SDK_INT < 19) + { + resetProxyForICS(); + } + else + { + resetKitKatProxy(appClass, ctx); + } + + } + + private static void resetProxyForICS() throws Exception{ + try + { + Class webViewCoreClass = Class.forName("android.webkit.WebViewCore"); + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (webViewCoreClass != null && proxyPropertiesClass != null) + { + Method m = webViewCoreClass.getDeclaredMethod("sendStaticMessage", Integer.TYPE, + Object.class); + + if (m != null) + { + m.setAccessible(true); + + // android.webkit.WebViewCore.EventHub.PROXY_CHANGED = 193; + m.invoke(null, 193, null); + } + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.net.ProxyProperties: " + + e.toString()); + throw e; + } catch (Error e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.webkit.Network: " + + e.toString()); + throw e; + } + } + + private static void resetProxyForGingerBread(Context ctx) throws Exception { + Object requestQueueObject = getRequestQueue(ctx); + if (requestQueueObject != null) { + setDeclaredField(requestQueueObject, "mProxyHost", null); + } + } + + public static Object getRequestQueue(Context ctx) throws Exception { + Object ret = null; + Class networkClass = Class.forName("android.webkit.Network"); + if (networkClass != null) { + Object networkObj = invokeMethod(networkClass, "getInstance", new Object[] { + ctx + }, Context.class); + if (networkObj != null) { + ret = getDeclaredField(networkObj, "mRequestQueue"); + } + } + return ret; + } + + private static Object getDeclaredField(Object obj, String name) + throws SecurityException, NoSuchFieldException, + IllegalArgumentException, IllegalAccessException { + Field f = obj.getClass().getDeclaredField(name); + f.setAccessible(true); + Object out = f.get(obj); + // System.out.println(obj.getClass().getName() + "." + name + " = "+ + // out); + return out; + } + + private static void setDeclaredField(Object obj, String name, Object value) + throws SecurityException, NoSuchFieldException, + IllegalArgumentException, IllegalAccessException { + Field f = obj.getClass().getDeclaredField(name); + f.setAccessible(true); + f.set(obj, value); + } + + private static Object invokeMethod(Object object, String methodName, Object[] params, + Class... types) throws Exception { + Object out = null; + Class c = object instanceof Class ? (Class) object : object.getClass(); + if (types != null) { + Method method = c.getMethod(methodName, types); + out = method.invoke(object, params); + } else { + Method method = c.getMethod(methodName); + out = method.invoke(object); + } + // System.out.println(object.getClass().getName() + "." + methodName + + // "() = "+ out); + return out; + } + + public static Socket getSocket(Context context, String proxyHost, int proxyPort) + throws IOException + { + Socket sock = new Socket(); + + sock.connect(new InetSocketAddress(proxyHost, proxyPort), 10000); + + return sock; + } + + public static Socket getSocket(Context context) throws IOException + { + return getSocket(context, DEFAULT_HOST, DEFAULT_SOCKS_PORT); + + } + + public static AlertDialog initOrbot(Activity activity, + CharSequence stringTitle, + CharSequence stringMessage, + CharSequence stringButtonYes, + CharSequence stringButtonNo, + CharSequence stringDesiredBarcodeFormats) { + Intent intentScan = new Intent("org.torproject.android.START_TOR"); + intentScan.addCategory(Intent.CATEGORY_DEFAULT); + + try { + activity.startActivityForResult(intentScan, REQUEST_CODE); + return null; + } catch (ActivityNotFoundException e) { + return showDownloadDialog(activity, stringTitle, stringMessage, stringButtonYes, + stringButtonNo); + } + } + + private static AlertDialog showDownloadDialog(final Activity activity, + CharSequence stringTitle, + CharSequence stringMessage, + CharSequence stringButtonYes, + CharSequence stringButtonNo) { + AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity); + downloadDialog.setTitle(stringTitle); + downloadDialog.setMessage(stringMessage); + downloadDialog.setPositiveButton(stringButtonYes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialogInterface, int i) { + Uri uri = Uri.parse("market://search?q=pname:org.torproject.android"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + activity.startActivity(intent); + } + }); + downloadDialog.setNegativeButton(stringButtonNo, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialogInterface, int i) { + } + }); + return downloadDialog.show(); + } + + + +} diff --git a/app/src/main/res/localization.xml b/app/src/main/res/localization.xml index 3a005ae4..570ba8af 100644 --- a/app/src/main/res/localization.xml +++ b/app/src/main/res/localization.xml @@ -12,7 +12,7 @@ digital freedom reload you are facing the one of the following problem. webpage or website might not be working. your internet connection might be poor. you might be using a proxy. website might be blocked by firewall - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Israel Strikes Again diff --git a/app/src/main/res/raw/debiancacerts.bks b/app/src/main/res/raw/debiancacerts.bks new file mode 100644 index 0000000000000000000000000000000000000000..3c7d955942447fd377167140879074aae4744566 GIT binary patch literal 174398 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%0DB=QQ*S;`!@HAKypcVDj?B-25CqmepO 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index a09d419e..2a23363e 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -14,7 +14,7 @@ llibertat digital recarregar us trobeu amb el problema següent. pot ser que la pàgina web o el lloc web no funcionin. és possible que la vostra connexió a Internet sigui deficient. és possible que utilitzeu un servidor intermediari. el tallafoc podria bloquejar el lloc web - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC, Israel torna a atacar diff --git a/app/src/main/res/values-ch/strings.xml b/app/src/main/res/values-ch/strings.xml index 840adc45..5cd65321 100644 --- a/app/src/main/res/values-ch/strings.xml +++ b/app/src/main/res/values-ch/strings.xml @@ -14,7 +14,7 @@ digitální svoboda Znovu načíst čelíte jednomu z následujících problémů. webová stránka nebo web nemusí fungovat. vaše připojení k internetu může být špatné. možná používáte proxy. web může být blokován bránou firewall - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Izrael znovu udeří diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 09ae627a..5e5fbe70 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -14,7 +14,7 @@ digitale Freiheit neu laden Sie stehen vor einem der folgenden Probleme. Webseite oder Website funktioniert möglicherweise nicht. Ihre Internetverbindung ist möglicherweise schlecht. Möglicherweise verwenden Sie einen Proxy. Die Website wird möglicherweise von der Firewall blockiert - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Israel schlägt erneut zu diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index ff7cf38a..418cf98c 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -14,7 +14,7 @@ ψηφιακή ελευθερία φορτώνω πάλι αντιμετωπίζετε ένα από τα ακόλουθα προβλήματα. η ιστοσελίδα ή ο ιστότοπος ενδέχεται να μην λειτουργούν. η σύνδεσή σας στο Διαδίκτυο μπορεί να είναι κακή. μπορεί να χρησιμοποιείτε διακομιστή μεσολάβησης. Ο ιστότοπος ενδέχεται να αποκλειστεί από το τείχος προστασίας - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Το Ισραήλ χτυπά ξανά diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 492f69f8..61022160 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -14,7 +14,7 @@ liberté numérique recharger vous êtes confronté à l\'un des problèmes suivants. la page Web ou le site Web peut ne pas fonctionner. votre connexion Internet est peut-être mauvaise. vous utilisez peut-être un proxy. le site Web peut être bloqué par le pare-feu - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Israël frappe à nouveau diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 3a30bebf..74c8f3d0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -14,7 +14,7 @@ digitális szabadság újratöltés a következő probléma egyikével áll szemben. előfordulhat, hogy egy weboldal vagy webhely nem működik. gyenge lehet az internetkapcsolat. lehet, hogy proxyt használ. előfordulhat, hogy a webhelyet tűzfal blokkolja - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Izrael újra sztrájkol diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 2d00c96d..4abf6ddf 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -14,7 +14,7 @@ libertà digitale ricaricare stai affrontando uno dei seguenti problemi. la pagina web o il sito web potrebbero non funzionare. la tua connessione Internet potrebbe essere scarsa. potresti utilizzare un proxy. il sito Web potrebbe essere bloccato dal firewall - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Israele colpisce ancora diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 889d77f1..18bc4414 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -14,7 +14,7 @@ デジタルの自由 リロード 次のいずれかの問題が発生しています。ウェブページまたはウェブサイトが機能していない可能性があります。インターネット接続が悪い可能性があります。プロキシを使用している可能性があります。ウェブサイトがファイアウォールによってブロックされている可能性があります - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC |イスラエルが再びストライキ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 36f36047..575b7e67 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -14,7 +14,7 @@ 디지털 자유 재 장전 다음 문제 중 하나에 직면하고 있습니다. 웹 페이지 또는 웹 사이트가 작동하지 않을 수 있습니다. 인터넷 연결 상태가 좋지 않을 수 있습니다. 프록시를 사용하고있을 수 있습니다. 웹 사이트가 방화벽에 의해 차단 될 수 있음 - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | 이스라엘이 다시 공격하다 diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index fc12eef6..bfe8142f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -14,7 +14,7 @@ digital freedom recarregar você está enfrentando um dos seguintes problemas. página da Web ou site pode não estar funcionando. sua conexão com a internet pode estar ruim. você pode estar usando um proxy. site pode estar bloqueado por firewall - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Israel ataca novamente diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 56bb1496..a322d9a7 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -14,7 +14,7 @@ libertatea digitală reîncărcați vă confruntați cu una dintre următoarele probleme. este posibil ca pagina web sau site-ul web să nu funcționeze. conexiunea la internet ar putea fi slabă. s-ar putea să utilizați un proxy. site-ul web ar putea fi blocat de firewall - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Israelul lovește din nou diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cfcb3f1c..a48f83cc 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -14,7 +14,7 @@ цифровая свобода перезагрузить вы столкнулись с одной из следующих проблем. веб-страница или веб-сайт могут не работать. ваше интернет-соединение может быть плохим. вы можете использовать прокси. веб-сайт может быть заблокирован брандмауэром - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Израиль снова наносит удар diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 8ac9c8d9..fe02b813 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -14,7 +14,7 @@ เสรีภาพดิจิทัล โหลดใหม่ คุณกำลังประสบปัญหาอย่างใดอย่างหนึ่งต่อไปนี้ หน้าเว็บหรือเว็บไซต์อาจไม่ทำงาน การเชื่อมต่ออินเทอร์เน็ตของคุณอาจไม่ดี คุณอาจใช้พร็อกซี เว็บไซต์อาจถูกปิดกั้นโดยไฟร์วอลล์ - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | อิสราเอลนัดหยุดงานอีกครั้ง diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 0dec8bbe..c8d8348c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -14,7 +14,7 @@ dijital özgürlük Tekrar yükle aşağıdaki problemlerden biriyle karşı karşıyasınız. web sayfası veya web sitesi çalışmıyor olabilir. İnternet bağlantınız zayıf olabilir. bir proxy kullanıyor olabilirsiniz. web sitesi güvenlik duvarı tarafından engelleniyor olabilir - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | İsrail Yeniden Grevde diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index b3d37bd1..7795a8d4 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -14,7 +14,7 @@ ڈیجیٹل آزادی دوبارہ لوڈ کریں آپ کو مندرجہ ذیل میں سے ایک مسئلہ درپیش ہے۔ ہوسکتا ہے کہ ویب صفحہ یا ویب سائٹ کام نہیں کررہی ہے۔ ہوسکتا ہے کہ آپ کا انٹرنیٹ کنیکشن خراب ہو۔ آپ شاید ایک پراکسی استعمال کر رہے ہوں گے۔ ہوسکتا ہے کہ ویب سائٹ فائر وال کے ذریعے مسدود کردی جائے - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider بی بی سی | اسرائیل نے ایک بار پھر حملہ کیا diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 1f588e60..4d8cb4ef 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -14,7 +14,7 @@ tự do kỹ thuật số tải lại bạn đang phải đối mặt với một trong những vấn đề sau. trang web hoặc trang web có thể không hoạt động. kết nối internet của bạn có thể kém. bạn có thể đang sử dụng proxy. trang web có thể bị tường lửa chặn - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Israel lại tấn công diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index e6bc838c..163011f4 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -14,7 +14,7 @@ 数字自由 重装 您正面临以下问题之一。网页或网站可能无法正常工作。您的互联网连接可能不佳。您可能正在使用代理。网站可能被防火墙阻止 - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider 英国广播公司|以色列再次罢工 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc4fd116..9b448a99 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,7 +14,7 @@ Online Freedom Reload These might be the problems you are facing \n\n• Webpage or Website might be down \n• Your Internet connection might be poor \n• You might be using a proxy \n• Website might be blocked by firewall - com.darkweb.genesissearchengine.fileprovider + com.darkweb.genesissearchengine.provider BBC | Israel Strikes Again Search the web ... diff --git a/libnetcipher/.classpath b/libnetcipher/.classpath new file mode 100644 index 00000000..7bc01d9a --- /dev/null +++ b/libnetcipher/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/libnetcipher/.gitignore b/libnetcipher/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/libnetcipher/.gitignore @@ -0,0 +1 @@ +/build diff --git a/libnetcipher/.project b/libnetcipher/.project new file mode 100644 index 00000000..64f977f6 --- /dev/null +++ b/libnetcipher/.project @@ -0,0 +1,33 @@ + + + libnetcipher + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/libnetcipher/AndroidManifest.xml b/libnetcipher/AndroidManifest.xml new file mode 100644 index 00000000..1f586e4b --- /dev/null +++ b/libnetcipher/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/libnetcipher/build.gradle b/libnetcipher/build.gradle new file mode 100644 index 00000000..5fc87166 --- /dev/null +++ b/libnetcipher/build.gradle @@ -0,0 +1,62 @@ +apply plugin: 'com.android.library' + +dependencies { + // If you want to fetch these from Maven, uncomment these lines and change + // the *.jar depend to exclude these libs: + compile fileTree(dir: 'libs', include: '*.jar') + androidTestCompile 'com.android.support.test:runner:0.4.1' + androidTestCompile 'com.android.support.test:rules:0.4.1' + androidTestCompile 'junit:junit:4.12' +} + +android { + compileSdkVersion 22 + buildToolsVersion '26.0.2' + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + resources.srcDirs = ['src'] + aidl.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + } + + androidTest { + manifest.srcFile '../netciphertest/AndroidManifest.xml' + java.srcDirs = ['../netciphertest/src'] + resources.srcDirs = ['../netciphertest/src'] + aidl.srcDirs = ['../netciphertest/src'] + renderscript.srcDirs = ['../netciphertest/src'] + res.srcDirs = ['../netciphertest/res'] + assets.srcDirs = ['../netciphertest/assets'] + } + } + + defaultConfig { + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + lintOptions { + abortOnError false + + htmlReport true + xmlReport false + textReport false + } + + testOptions { + unitTests { + // prevent tests on JVM from dying on android.util.Log calls + returnDefaultValues = true + all { + testLogging { + events "skipped", "failed", "standardOut", "standardError" + showStandardStreams = true + } + } + } + } +} diff --git a/libnetcipher/custom_rules.xml b/libnetcipher/custom_rules.xml new file mode 100644 index 00000000..2402a75b --- /dev/null +++ b/libnetcipher/custom_rules.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libnetcipher/libs/httpclientandroidlib-1.2.1.jar b/libnetcipher/libs/httpclientandroidlib-1.2.1.jar new file mode 100644 index 00000000..e69de29b diff --git a/libnetcipher/netcipher.pom b/libnetcipher/netcipher.pom new file mode 100644 index 00000000..3f243a4b --- /dev/null +++ b/libnetcipher/netcipher.pom @@ -0,0 +1,33 @@ + + + 4.0.0 + info.guardianproject.netcipher + netcipher + 1.2 + NetCipher + https://guardianproject.info/code/netcipher + NetCipher is a library for Android that provides multiple means to improve network security in mobile applications. It provides best practices TLS settings using the standard Android HTTP methods, HttpURLConnection and Apache HTTP Client, provides simple Tor integration, makes it easy to configure proxies for HTTP connections and `WebView` instances. + + + Apache-2.0 + https://github.com/guardianproject/NetCipher/blob/master/LICENSE.txt + + + + + guardianproject + Guardian Project + support@guardianproject.info + + + + https://dev.guardianproject.info/projects/netcipher/issues + Redmine + + + scm:https://github.com/guardianproject/NetCipher.git + scm:git@github.com:guardianproject/NetCipher.git + scm:https://github.com/guardianproject/NetCipher + + diff --git a/libnetcipher/proguard-project.txt b/libnetcipher/proguard-project.txt new file mode 100644 index 00000000..f2fe1559 --- /dev/null +++ b/libnetcipher/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/libnetcipher/project.properties b/libnetcipher/project.properties new file mode 100644 index 00000000..362a0a30 --- /dev/null +++ b/libnetcipher/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-22 +android.library=true diff --git a/libnetcipher/res/raw/debiancacerts.bks b/libnetcipher/res/raw/debiancacerts.bks new file mode 100644 index 00000000..e69de29b diff --git a/libnetcipher/settings.gradle b/libnetcipher/settings.gradle new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/libnetcipher/settings.gradle @@ -0,0 +1 @@ + diff --git a/libnetcipher/src/info/guardianproject/netcipher/NetCipher.java b/libnetcipher/src/info/guardianproject/netcipher/NetCipher.java new file mode 100644 index 00000000..e26d283d --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/NetCipher.java @@ -0,0 +1,357 @@ +/* + * Copyright 2014-2016 Hans-Christoph Steiner + * Copyright 2012-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher; + +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + +import info.guardianproject.netcipher.client.TlsOnlySocketFactory; +import info.guardianproject.netcipher.proxy.OrbotHelper; + +public class NetCipher { + private static final String TAG = "NetCipher"; + + private NetCipher() { + // this is a utility class with only static methods + } + + public final static Proxy ORBOT_HTTP_PROXY = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress("127.0.0.1", 8118)); + + private static Proxy proxy; + + /** + * Set the global HTTP proxy for all new {@link HttpURLConnection}s and + * {@link HttpsURLConnection}s that are created after this is called. + *

+ * {@link #useTor()} will override this setting. Traffic must be directed + * to Tor using the proxy settings, and Orbot has its own proxy settings + * for connections that need proxies to work. So if "use Tor" is enabled, + * as tested by looking for the static instance of Proxy, then no other + * proxy settings are allowed to override the current Tor proxy. + * + * @param host the IP address for the HTTP proxy to use globally + * @param port the port number for the HTTP proxy to use globally + */ + public static void setProxy(String host, int port) { + if (!TextUtils.isEmpty(host) && port > 0) { + InetSocketAddress isa = new InetSocketAddress(host, port); + setProxy(new Proxy(Proxy.Type.HTTP, isa)); + } else if (NetCipher.proxy != ORBOT_HTTP_PROXY) { + setProxy(null); + } + } + + /** + * Set the global HTTP proxy for all new {@link HttpURLConnection}s and + * {@link HttpsURLConnection}s that are created after this is called. + *

+ * {@link #useTor()} will override this setting. Traffic must be directed + * to Tor using the proxy settings, and Orbot has its own proxy settings + * for connections that need proxies to work. So if "use Tor" is enabled, + * as tested by looking for the static instance of Proxy, then no other + * proxy settings are allowed to override the current Tor proxy. + * + * @param proxy the HTTP proxy to use globally + */ + public static void setProxy(Proxy proxy) { + if (proxy != null && NetCipher.proxy == ORBOT_HTTP_PROXY) { + Log.w(TAG, "useTor is enabled, ignoring new proxy settings!"); + } else { + NetCipher.proxy = proxy; + } + } + + /** + * Get the currently active global HTTP {@link Proxy}. + * + * @return the active HTTP {@link Proxy} + */ + public static Proxy getProxy() { + return proxy; + } + + /** + * Clear the global HTTP proxy for all new {@link HttpURLConnection}s and + * {@link HttpsURLConnection}s that are created after this is called. This + * returns things to the default, proxy-less state. + */ + public static void clearProxy() { + setProxy(null); + } + + /** + * Set Orbot as the global HTTP proxy for all new {@link HttpURLConnection} + * s and {@link HttpsURLConnection}s that are created after this is called. + * This overrides all future calls to {@link #setProxy(Proxy)}, except to + * clear the proxy, e.g. {@code #setProxy(null)} or {@link #clearProxy()}. + *

+ * Traffic must be directed to Tor using the proxy settings, and Orbot has its + * own proxy settings for connections that need proxies to work. So if "use + * Tor" is enabled, as tested by looking for the static instance of Proxy, + * then no other proxy settings are allowed to override the current Tor proxy. + */ + public static void useTor() { + setProxy(ORBOT_HTTP_PROXY); + } + + /** + * Get a {@link TlsOnlySocketFactory} from NetCipher. + * + * @see HttpsURLConnection#setDefaultSSLSocketFactory(SSLSocketFactory) + */ + public static TlsOnlySocketFactory getTlsOnlySocketFactory() { + return getTlsOnlySocketFactory(false); + } + + /** + * Get a {@link TlsOnlySocketFactory} from NetCipher, and specify whether + * it should use a more compatible, but less strong, suite of ciphers. + * + * @see HttpsURLConnection#setDefaultSSLSocketFactory(SSLSocketFactory) + */ + public static TlsOnlySocketFactory getTlsOnlySocketFactory(boolean compatible) { + SSLContext sslcontext; + try { + sslcontext = SSLContext.getInstance("TLSv1"); + sslcontext.init(null, null, null); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } catch (KeyManagementException e) { + throw new IllegalArgumentException(e); + } + return new TlsOnlySocketFactory(sslcontext.getSocketFactory(), compatible); + } + + /** + * Get a {@link HttpURLConnection} from a {@link URL}, and specify whether + * it should use a more compatible, but less strong, suite of ciphers. + * + * @param url + * @param compatible + * @return the {@code url} in an instance of {@link HttpURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getHttpURLConnection(URL url, boolean compatible) + throws IOException { + // .onion addresses only work via Tor, so force Tor for all of them + Proxy proxy = NetCipher.proxy; + if (OrbotHelper.isOnionAddress(url)) + proxy = ORBOT_HTTP_PROXY; + + HttpURLConnection connection; + if (proxy != null) { + connection = (HttpURLConnection) url.openConnection(proxy); + } else { + connection = (HttpURLConnection) url.openConnection(); + } + + if (connection instanceof HttpsURLConnection) { + HttpsURLConnection httpsConnection = ((HttpsURLConnection) connection); + SSLSocketFactory tlsOnly = getTlsOnlySocketFactory(compatible); + httpsConnection.setSSLSocketFactory(tlsOnly); + if (Build.VERSION.SDK_INT < 16) { + httpsConnection.setHostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.STRICT_HOSTNAME_VERIFIER); + } + } + return connection; + } + + /** + * Get a {@link HttpsURLConnection} from a URL {@link String} using the best + * TLS configuration available on the device. + * + * @param urlString + * @return the URL in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getHttpsURLConnection(String urlString) throws IOException { + URL url = new URL(urlString.replaceFirst("^[Hh][Tt][Tt][Pp]:", "https:")); + return getHttpsURLConnection(url, false); + } + + /** + * Get a {@link HttpsURLConnection} from a {@link Uri} using the best TLS + * configuration available on the device. + * + * @param uri + * @return the {@code uri} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getHttpsURLConnection(Uri uri) throws IOException { + return getHttpsURLConnection(uri.toString()); + } + + /** + * Get a {@link HttpsURLConnection} from a {@link URI} using the best TLS + * configuration available on the device. + * + * @param uri + * @return the {@code uri} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getHttpsURLConnection(URI uri) throws IOException { + if (TextUtils.equals(uri.getScheme(), "https")) + return getHttpsURLConnection(uri.toURL(), false); + else + // otherwise force scheme to https + return getHttpsURLConnection(uri.toString()); + } + + /** + * Get a {@link HttpsURLConnection} from a {@link URL} using the best TLS + * configuration available on the device. + * + * @param url + * @return the {@code url} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getHttpsURLConnection(URL url) throws IOException { + return getHttpsURLConnection(url, false); + } + + /** + * Get a {@link HttpsURLConnection} from a {@link URL} using a more + * compatible, but less strong, suite of ciphers. + * + * @param url + * @return the {@code url} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getCompatibleHttpsURLConnection(URL url) throws IOException { + return getHttpsURLConnection(url, true); + } + + /** + * Get a {@link HttpsURLConnection} from a {@link URL}, and specify whether + * it should use a more compatible, but less strong, suite of ciphers. + * + * @param url + * @param compatible + * @return the {@code url} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect, + * or if an HTTP URL is given that does not support HTTPS + */ + public static HttpsURLConnection getHttpsURLConnection(URL url, boolean compatible) + throws IOException { + // use default method, but enforce a HttpsURLConnection + HttpURLConnection connection = getHttpURLConnection(url, compatible); + if (connection instanceof HttpsURLConnection) { + return (HttpsURLConnection) connection; + } else { + throw new IllegalArgumentException("not an HTTPS connection!"); + } + } + + /** + * Get a {@link HttpURLConnection} from a {@link URL}. If the connection is + * {@code https://}, it will use a more compatible, but less strong, TLS + * configuration. + * + * @param url + * @return the {@code url} in an instance of {@link HttpsURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getCompatibleHttpURLConnection(URL url) throws IOException { + return getHttpURLConnection(url, true); + } + + /** + * Get a {@link HttpURLConnection} from a URL {@link String}. If it is an + * {@code https://} link, then this will use the best TLS configuration + * available on the device. + * + * @param urlString + * @return the URL in an instance of {@link HttpURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getHttpURLConnection(String urlString) throws IOException { + return getHttpURLConnection(new URL(urlString)); + } + + /** + * Get a {@link HttpURLConnection} from a {@link Uri}. If it is an + * {@code https://} link, then this will use the best TLS configuration + * available on the device. + * + * @param uri + * @return the {@code uri} in an instance of {@link HttpURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getHttpURLConnection(Uri uri) throws IOException { + return getHttpURLConnection(uri.toString()); + } + + /** + * Get a {@link HttpURLConnection} from a {@link URI}. If it is an + * {@code https://} link, then this will use the best TLS configuration + * available on the device. + * + * @param uri + * @return the {@code uri} in an instance of {@link HttpURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getHttpURLConnection(URI uri) throws IOException { + return getHttpURLConnection(uri.toURL()); + } + + /** + * Get a {@link HttpURLConnection} from a {@link URL}. If it is an + * {@code https://} link, then this will use the best TLS configuration + * available on the device. + * + * @param url + * @return the {@code url} in an instance of {@link HttpURLConnection} + * @throws IOException + * @throws IllegalArgumentException if the proxy or TLS setup is incorrect + */ + public static HttpURLConnection getHttpURLConnection(URL url) throws IOException { + return (HttpURLConnection) getHttpURLConnection(url, false); + } +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/client/SocksAwareClientConnOperator.java b/libnetcipher/src/info/guardianproject/netcipher/client/SocksAwareClientConnOperator.java new file mode 100644 index 00000000..3022a398 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/client/SocksAwareClientConnOperator.java @@ -0,0 +1,255 @@ +/* + * Copyright 2015 str4d + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher.client; + +import android.util.Log; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import ch.boye.httpclientandroidlib.HttpHost; +import ch.boye.httpclientandroidlib.conn.HttpHostConnectException; +import ch.boye.httpclientandroidlib.conn.OperatedClientConnection; +import ch.boye.httpclientandroidlib.conn.scheme.Scheme; +import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry; +import ch.boye.httpclientandroidlib.conn.scheme.SchemeSocketFactory; +import ch.boye.httpclientandroidlib.conn.scheme.SocketFactory; +import ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory; +import ch.boye.httpclientandroidlib.impl.conn.DefaultClientConnectionOperator; +import ch.boye.httpclientandroidlib.params.HttpParams; +import ch.boye.httpclientandroidlib.protocol.HttpContext; + +public class SocksAwareClientConnOperator extends DefaultClientConnectionOperator { + + private static final int CONNECT_TIMEOUT_MILLISECONDS = 60000; + private static final int READ_TIMEOUT_MILLISECONDS = 60000; + + private HttpHost mProxyHost; + private String mProxyType; + private SocksAwareProxyRoutePlanner mRoutePlanner; + + public SocksAwareClientConnOperator(SchemeRegistry registry, + HttpHost proxyHost, + String proxyType, + SocksAwareProxyRoutePlanner proxyRoutePlanner) { + super(registry); + + mProxyHost = proxyHost; + mProxyType = proxyType; + mRoutePlanner = proxyRoutePlanner; + } + + @Override + public void openConnection( + final OperatedClientConnection conn, + final HttpHost target, + final InetAddress local, + final HttpContext context, + final HttpParams params) throws IOException { + if (mProxyHost != null) { + if (mProxyType != null && mProxyType.equalsIgnoreCase("socks")) { + Log.d("StrongHTTPS", "proxying using SOCKS"); + openSocksConnection(mProxyHost, conn, target, local, context, params); + } else { + Log.d("StrongHTTPS", "proxying with: " + mProxyType); + openNonSocksConnection(conn, target, local, context, params); + } + } else if (mRoutePlanner != null) { + if (mRoutePlanner.isProxy(target)) { + // HTTP proxy, already handled by the route planner system + Log.d("StrongHTTPS", "proxying using non-SOCKS"); + openNonSocksConnection(conn, target, local, context, params); + } else { + // Either SOCKS or direct + HttpHost proxy = mRoutePlanner.determineRequiredProxy(target, null, context); + if (proxy == null) { + Log.d("StrongHTTPS", "not proxying"); + openNonSocksConnection(conn, target, local, context, params); + } else if (mRoutePlanner.isSocksProxy(proxy)) { + Log.d("StrongHTTPS", "proxying using SOCKS"); + openSocksConnection(proxy, conn, target, local, context, params); + } else { + throw new IllegalStateException("Non-SOCKS proxy returned"); + } + } + } else { + Log.d("StrongHTTPS", "not proxying"); + openNonSocksConnection(conn, target, local, context, params); + } + } + + private void openNonSocksConnection( + final OperatedClientConnection conn, + final HttpHost target, + final InetAddress local, + final HttpContext context, + final HttpParams params) throws IOException { + if (conn == null) { + throw new IllegalArgumentException("Connection must not be null."); + } + if (target == null) { + throw new IllegalArgumentException("Target host must not be null."); + } + // local address may be null + // @@@ is context allowed to be null? + if (params == null) { + throw new IllegalArgumentException("Parameters must not be null."); + } + if (conn.isOpen()) { + throw new IllegalArgumentException("Connection must not be open."); + } + + final Scheme schm = schemeRegistry.getScheme(target.getSchemeName()); + final SocketFactory sf = schm.getSocketFactory(); + + Socket sock = sf.createSocket(); + conn.opening(sock, target); + + try { + Socket connsock = sf.connectSocket(sock, target.getHostName(), + schm.resolvePort(target.getPort()), + local, 0, params); + + if (sock != connsock) { + sock = connsock; + conn.opening(sock, target); + } + } catch (ConnectException ex) { + throw new HttpHostConnectException(target, ex); + } + prepareSocket(sock, context, params); + conn.openCompleted(sf.isSecure(sock), params); + } + + // Derived from the original DefaultClientConnectionOperator.java in Apache HttpClient 4.2 + private void openSocksConnection( + final HttpHost proxy, + final OperatedClientConnection conn, + final HttpHost target, + final InetAddress local, + final HttpContext context, + final HttpParams params) throws IOException { + Socket socket = null; + Socket sslSocket = null; + try { + if (conn == null || target == null || params == null) { + throw new IllegalArgumentException("Required argument may not be null"); + } + if (conn.isOpen()) { + throw new IllegalStateException("Connection must not be open"); + } + + Scheme scheme = schemeRegistry.getScheme(target.getSchemeName()); + SchemeSocketFactory schemeSocketFactory = scheme.getSchemeSocketFactory(); + + int port = scheme.resolvePort(target.getPort()); + String host = target.getHostName(); + + // Perform explicit SOCKS4a connection request. SOCKS4a supports remote host name resolution + // (i.e., Tor resolves the hostname, which may be an onion address). + // The Android (Apache Harmony) Socket class appears to support only SOCKS4 and throws an + // exception on an address created using INetAddress.createUnresolved() -- so the typical + // technique for using Java SOCKS4a/5 doesn't appear to work on Android: + // https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/java/net/PlainSocketImpl.java + // See also: http://www.mit.edu/~foley/TinFoil/src/tinfoil/TorLib.java, for a similar implementation + + // From http://en.wikipedia.org/wiki/SOCKS#SOCKS4a: + // + // field 1: SOCKS version number, 1 byte, must be 0x04 for this version + // field 2: command code, 1 byte: + // 0x01 = establish a TCP/IP stream connection + // 0x02 = establish a TCP/IP port binding + // field 3: network byte order port number, 2 bytes + // field 4: deliberate invalid IP address, 4 bytes, first three must be 0x00 and the last one must not be 0x00 + // field 5: the user ID string, variable length, terminated with a null (0x00) + // field 6: the domain name of the host we want to contact, variable length, terminated with a null (0x00) + + + socket = new Socket(); + conn.opening(socket, target); + socket.setSoTimeout(READ_TIMEOUT_MILLISECONDS); + socket.connect(new InetSocketAddress(proxy.getHostName(), proxy.getPort()), CONNECT_TIMEOUT_MILLISECONDS); + + DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream()); + outputStream.write((byte) 0x04); + outputStream.write((byte) 0x01); + outputStream.writeShort((short) port); + outputStream.writeInt(0x01); + outputStream.write((byte) 0x00); + outputStream.write(host.getBytes()); + outputStream.write((byte) 0x00); + + DataInputStream inputStream = new DataInputStream(socket.getInputStream()); + if (inputStream.readByte() != (byte) 0x00 || inputStream.readByte() != (byte) 0x5a) { + throw new IOException("SOCKS4a connect failed"); + } + inputStream.readShort(); + inputStream.readInt(); + + if (schemeSocketFactory instanceof SSLSocketFactory) { + sslSocket = ((SSLSocketFactory) schemeSocketFactory).createLayeredSocket(socket, host, port, params); + conn.opening(sslSocket, target); + sslSocket.setSoTimeout(READ_TIMEOUT_MILLISECONDS); + prepareSocket(sslSocket, context, params); + conn.openCompleted(schemeSocketFactory.isSecure(sslSocket), params); + } else { + conn.opening(socket, target); + socket.setSoTimeout(READ_TIMEOUT_MILLISECONDS); + prepareSocket(socket, context, params); + conn.openCompleted(schemeSocketFactory.isSecure(socket), params); + } + // TODO: clarify which connection throws java.net.SocketTimeoutException? + } catch (IOException e) { + try { + if (sslSocket != null) { + sslSocket.close(); + } + if (socket != null) { + socket.close(); + } + } catch (IOException ioe) { + } + throw e; + } + } + + @Override + public void updateSecureConnection( + final OperatedClientConnection conn, + final HttpHost target, + final HttpContext context, + final HttpParams params) throws IOException { + if (mProxyHost != null && mProxyType.equalsIgnoreCase("socks")) + throw new RuntimeException("operation not supported"); + else + super.updateSecureConnection(conn, target, context, params); + } + + @Override + protected InetAddress[] resolveHostname(final String host) throws UnknownHostException { + if (mProxyHost != null && mProxyType.equalsIgnoreCase("socks")) + throw new RuntimeException("operation not supported"); + else + return super.resolveHostname(host); + } +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/client/SocksAwareProxyRoutePlanner.java b/libnetcipher/src/info/guardianproject/netcipher/client/SocksAwareProxyRoutePlanner.java new file mode 100644 index 00000000..7c6ed0e2 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/client/SocksAwareProxyRoutePlanner.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 str4d + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package info.guardianproject.netcipher.client; + +import ch.boye.httpclientandroidlib.HttpException; +import ch.boye.httpclientandroidlib.HttpHost; +import ch.boye.httpclientandroidlib.HttpRequest; +import ch.boye.httpclientandroidlib.conn.SchemePortResolver; +import ch.boye.httpclientandroidlib.impl.conn.DefaultRoutePlanner; +import ch.boye.httpclientandroidlib.protocol.HttpContext; + +public abstract class SocksAwareProxyRoutePlanner extends DefaultRoutePlanner { + public SocksAwareProxyRoutePlanner(SchemePortResolver schemePortResolver) { + super(schemePortResolver); + } + + @Override + protected HttpHost determineProxy( + HttpHost target, + HttpRequest request, + HttpContext context) throws HttpException { + HttpHost proxy = determineRequiredProxy(target, request, context); + if (isSocksProxy(proxy)) + proxy = null; + return proxy; + } + + /** + * Determine the proxy required for the provided target. + * + * @param target see {@link #determineProxy(HttpHost, HttpRequest, HttpContext) determineProxy()} + * @param request see {@link #determineProxy(HttpHost, HttpRequest, HttpContext) determineProxy()}. + * Will be null when called from {@link SocksAwareClientConnOperator} to + * determine if target requires a SOCKS proxy, so don't rely on it in this case. + * @param context see {@link #determineProxy(HttpHost, HttpRequest, HttpContext) determineProxy()} + * @return the proxy required for this target, or null if should connect directly. + */ + protected abstract HttpHost determineRequiredProxy( + HttpHost target, + HttpRequest request, + HttpContext context); + + /** + * Checks if the provided target is a proxy we define. + * + * @param target to check + * @return true if this is a proxy, false otherwise + */ + protected abstract boolean isProxy(HttpHost target); + + /** + * Checks if the provided target is a SOCKS proxy we define. + * + * @param target to check + * @return true if this target is a SOCKS proxy, false otherwise. + */ + protected abstract boolean isSocksProxy(HttpHost target); +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/client/StrongBuilder.java b/libnetcipher/src/info/guardianproject/netcipher/client/StrongBuilder.java new file mode 100644 index 00000000..ef603e9b --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/client/StrongBuilder.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher.client; + +import android.content.Intent; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import javax.net.ssl.TrustManager; + +public interface StrongBuilder { + /** + * Callback to get a connection handed to you for use, + * already set up for NetCipher. + * + * @param the type of connection created by this builder + */ + interface Callback { + /** + * Called when the NetCipher-enhanced connection is ready + * for use. + * + * @param connection the connection + */ + void onConnected(C connection); + + /** + * Called if we tried to connect through to Orbot but failed + * for some reason + * + * @param e the reason + */ + void onConnectionException(Exception e); + + /** + * Called if our attempt to get a status from Orbot failed + * after a defined period of time. See statusTimeout() on + * OrbotInitializer. + */ + void onTimeout(); + + /** + * Called if you requested validation that we are connecting + * through Tor, and while we were able to connect to Orbot, that + * validation failed. + */ + void onInvalid(); + } + + /** + * Call this to configure the Tor proxy from the results + * returned by Orbot, using the best available proxy + * (SOCKS if possible, else HTTP) + * + * @return the builder + */ + T withBestProxy(); + + /** + * @return true if this builder supports HTTP proxies, false + * otherwise + */ + boolean supportsHttpProxy(); + + /** + * Call this to configure the Tor proxy from the results + * returned by Orbot, using the HTTP proxy. + * + * @return the builder + */ + T withHttpProxy(); + + /** + * @return true if this builder supports SOCKS proxies, false + * otherwise + */ + boolean supportsSocksProxy(); + + /** + * Call this to configure the Tor proxy from the results + * returned by Orbot, using the SOCKS proxy. + * + * @return the builder + */ + T withSocksProxy(); + + /** + * Applies your own custom TrustManagers, such as for + * replacing the stock keystore support with a custom + * keystore. + * + * @param trustManagers the TrustManagers to use + * @return the builder + */ + T withTrustManagers(TrustManager[] trustManagers) + throws NoSuchAlgorithmException, KeyManagementException; + + /** + * Call this if you want a weaker set of supported ciphers, + * because you are running into compatibility problems with + * some server due to a cipher mismatch. The better solution + * is to fix the server. + * + * @return the builder + */ + T withWeakCiphers(); + + /** + * Call this if you want the builder to confirm that we are + * communicating over Tor, by reaching out to a Tor test + * server and confirming our connection status. By default, + * this is skipped. Adding this check adds security, but it + * has the chance of false negatives (e.g., we cannot reach + * that Tor server for some reason). + * + * @return the builder + */ + T withTorValidation(); + + /** + * Builds a connection, applying the configuration already + * specified in the builder. + * + * @param status status Intent from OrbotInitializer + * @return the connection + * @throws IOException + */ + C build(Intent status) throws Exception; + + /** + * Asynchronous version of build(), one that uses OrbotInitializer + * internally to get the status and checks the validity of the Tor + * connection (if requested). Note that your callback methods may + * be invoked on any thread; do not assume that they will be called + * on any particular thread. + * + * @param callback Callback to get a connection handed to you + * for use, already set up for NetCipher + */ + void build(Callback callback); +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/client/StrongBuilderBase.java b/libnetcipher/src/info/guardianproject/netcipher/client/StrongBuilderBase.java new file mode 100644 index 00000000..cfe1d454 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/client/StrongBuilderBase.java @@ -0,0 +1,287 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * Copyright 2015 str4d + * Portions Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher.client; + +import android.content.Context; +import android.content.Intent; +import org.json.JSONObject; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import info.guardianproject.netcipher.proxy.OrbotHelper; + +/** + * Builds an HttpUrlConnection that connects via Tor through + * Orbot. + */ +abstract public class + StrongBuilderBase + implements StrongBuilder { + /** + * Performs an HTTP GET request using the supplied connection + * to a supplied URL, returning the String response or + * throws an Exception (e.g., cannot reach the server). + * This is used as part of validating the Tor connection. + * + * @param status the status Intent we got back from Orbot + * @param connection a connection of the type for the builder + * @param url an public Web page + * @return the String response from the GET request + */ + abstract protected String get(Intent status, C connection, String url) + throws Exception; + + final static String TOR_CHECK_URL="https://check.torproject.org/api/ip"; + private final static String PROXY_HOST="127.0.0.1"; + protected final Context ctxt; + protected Proxy.Type proxyType; + protected SSLContext sslContext=null; + protected boolean useWeakCiphers=false; + protected boolean validateTor=false; + + /** + * Standard constructor. + * + * @param ctxt any Context will do; the StrongBuilderBase + * will hold onto the Application singleton + */ + public StrongBuilderBase(Context ctxt) { + this.ctxt=ctxt.getApplicationContext(); + } + + /** + * Copy constructor. + * + * @param original builder to clone + */ + public StrongBuilderBase(StrongBuilderBase original) { + this.ctxt=original.ctxt; + this.proxyType=original.proxyType; + this.sslContext=original.sslContext; + this.useWeakCiphers=original.useWeakCiphers; + } + + /** + * {@inheritDoc} + */ + @Override + public T withBestProxy() { + if (supportsSocksProxy()) { + return(withSocksProxy()); + } + else { + return(withHttpProxy()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportsHttpProxy() { + return(true); + } + + /** + * {@inheritDoc} + */ + @Override + public T withHttpProxy() { + proxyType=Proxy.Type.HTTP; + + return((T)this); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportsSocksProxy() { + return(false); + } + + /** + * {@inheritDoc} + */ + @Override + public T withSocksProxy() { + proxyType=Proxy.Type.SOCKS; + + return((T)this); + } + + /** + * {@inheritDoc} + */ + @Override + public T withTrustManagers(TrustManager[] trustManagers) + throws NoSuchAlgorithmException, KeyManagementException { + + sslContext=SSLContext.getInstance("TLSv1"); + sslContext.init(null, trustManagers, null); + + return((T)this); + } + + /** + * {@inheritDoc} + */ + @Override + public T withWeakCiphers() { + useWeakCiphers=true; + + return((T)this); + } + + /** + * {@inheritDoc} + */ + @Override + public T withTorValidation() { + validateTor=true; + + return((T)this); + } + + public SSLContext getSSLContext() { + return(sslContext); + } + + public int getSocksPort(Intent status) { + if (status.getStringExtra(OrbotHelper.EXTRA_STATUS) + .equals(OrbotHelper.STATUS_ON)) { + return(status.getIntExtra(OrbotHelper.EXTRA_PROXY_PORT_SOCKS, + 9050)); + } + + return(-1); + } + + public int getHttpPort(Intent status) { + if (status.getStringExtra(OrbotHelper.EXTRA_STATUS) + .equals(OrbotHelper.STATUS_ON)) { + return(status.getIntExtra(OrbotHelper.EXTRA_PROXY_PORT_HTTP, + 8118)); + } + + return(-1); + } + + protected SSLSocketFactory buildSocketFactory() { + if (sslContext==null) { + return(null); + } + + SSLSocketFactory result= + new TlsOnlySocketFactory(sslContext.getSocketFactory(), + useWeakCiphers); + + return(result); + } + + public Proxy buildProxy(Intent status) { + Proxy result=null; + + if (status.getStringExtra(OrbotHelper.EXTRA_STATUS) + .equals(OrbotHelper.STATUS_ON)) { + if (proxyType==Proxy.Type.SOCKS) { + result=new Proxy(Proxy.Type.SOCKS, + new InetSocketAddress(PROXY_HOST, getSocksPort(status))); + } + else if (proxyType==Proxy.Type.HTTP) { + result=new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(PROXY_HOST, getHttpPort(status))); + } + } + + return(result); + } + + @Override + public void build(final Callback callback) { + OrbotHelper.get(ctxt).addStatusCallback( + new OrbotHelper.SimpleStatusCallback() { + @Override + public void onEnabled(Intent statusIntent) { + OrbotHelper.get(ctxt).removeStatusCallback(this); + + try { + C connection=build(statusIntent); + + if (validateTor) { + validateTor=false; + checkTor(callback, statusIntent, connection); + } + else { + callback.onConnected(connection); + } + } + catch (Exception e) { + callback.onConnectionException(e); + } + } + + @Override + public void onNotYetInstalled() { + OrbotHelper.get(ctxt).removeStatusCallback(this); + callback.onTimeout(); + } + + @Override + public void onStatusTimeout() { + OrbotHelper.get(ctxt).removeStatusCallback(this); + callback.onTimeout(); + } + }); + } + + protected void checkTor(final Callback callback, final Intent status, + final C connection) { + new Thread() { + @Override + public void run() { + try { + String result=get(status, connection, TOR_CHECK_URL); + JSONObject json=new JSONObject(result); + + if (json.optBoolean("IsTor", false)) { + callback.onConnected(connection); + } + else { + callback.onInvalid(); + } + } + catch (Exception e) { + callback.onConnectionException(e); + } + } + }.start(); + } +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/client/StrongConnectionBuilder.java b/libnetcipher/src/info/guardianproject/netcipher/client/StrongConnectionBuilder.java new file mode 100644 index 00000000..09d496c0 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/client/StrongConnectionBuilder.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher.client; + +import android.content.Context; +import android.content.Intent; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +/** + * Builds an HttpUrlConnection that connects via Tor through + * Orbot. + */ +public class StrongConnectionBuilder + extends StrongBuilderBase { + private URL url; + + /** + * Creates a StrongConnectionBuilder using the strongest set + * of options for security. Use this if the strongest set of + * options is what you want; otherwise, create a + * builder via the constructor and configure it as you see fit. + * + * @param ctxt any Context will do + * @return a configured StrongConnectionBuilder + * @throws Exception + */ + static public StrongConnectionBuilder forMaxSecurity(Context ctxt) + throws Exception { + return(new StrongConnectionBuilder(ctxt) + .withBestProxy()); + } + + /** + * Creates a builder instance. + * + * @param ctxt any Context will do; builder will hold onto + * Application context + */ + public StrongConnectionBuilder(Context ctxt) { + super(ctxt); + } + + /** + * Copy constructor. + * + * @param original builder to clone + */ + public StrongConnectionBuilder(StrongConnectionBuilder original) { + super(original); + this.url=original.url; + } +/* + public boolean supportsSocksProxy() { + return(false); + } +*/ + + /** + * Sets the URL to build a connection for. + * + * @param url the URL + * @return the builder + * @throws MalformedURLException + */ + public StrongConnectionBuilder connectTo(String url) + throws MalformedURLException { + connectTo(new URL(url)); + + return(this); + } + + /** + * Sets the URL to build a connection for. + * + * @param url the URL + * @return the builder + */ + public StrongConnectionBuilder connectTo(URL url) { + this.url=url; + + return(this); + } + + /** + * {@inheritDoc} + */ + @Override + public HttpURLConnection build(Intent status) throws IOException { + return(buildForUrl(status, url)); + } + + @Override + protected String get(Intent status, HttpURLConnection connection, + String url) throws Exception { + HttpURLConnection realConnection=buildForUrl(status, new URL(url)); + + return(slurp(realConnection.getInputStream())); + } + + private HttpURLConnection buildForUrl(Intent status, URL urlToUse) + throws IOException { + URLConnection result; + Proxy proxy=buildProxy(status); + + if (proxy==null) { + result=urlToUse.openConnection(); + } + else { + result=urlToUse.openConnection(proxy); + } + + if (result instanceof HttpsURLConnection && sslContext!=null) { + SSLSocketFactory tlsOnly=buildSocketFactory(); + HttpsURLConnection https=(HttpsURLConnection)result; + + https.setSSLSocketFactory(tlsOnly); + } + + return((HttpURLConnection)result); + } + + // based on http://stackoverflow.com/a/309718/115145 + + public static String slurp(final InputStream is) + throws IOException { + final char[] buffer = new char[128]; + final StringBuilder out = new StringBuilder(); + final Reader in = new InputStreamReader(is, "UTF-8"); + + for (;;) { + int rsz = in.read(buffer, 0, buffer.length); + if (rsz < 0) + break; + out.append(buffer, 0, rsz); + } + + in.close(); + + return out.toString(); + } +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/client/StrongConstants.java b/libnetcipher/src/info/guardianproject/netcipher/client/StrongConstants.java new file mode 100644 index 00000000..fae25589 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/client/StrongConstants.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package info.guardianproject.netcipher.client; + +public class StrongConstants { + + /** + * Ordered to prefer the stronger cipher suites as noted + * http://op-co.de/blog/posts/android_ssl_downgrade/ + */ + public static final String ENABLED_CIPHERS[] = { + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", "SSL_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_WITH_RC4_128_SHA", "SSL_RSA_WITH_RC4_128_MD5" }; + + /** + * Ordered to prefer the stronger/newer TLS versions as noted + * http://op-co.de/blog/posts/android_ssl_downgrade/ + */ + public static final String ENABLED_PROTOCOLS[] = { "TLSv1.2", "TLSv1.1", + "TLSv1" }; + +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/client/StrongHttpsClient.java b/libnetcipher/src/info/guardianproject/netcipher/client/StrongHttpsClient.java new file mode 100644 index 00000000..ca20db28 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/client/StrongHttpsClient.java @@ -0,0 +1,164 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * Copyright 2015 str4d + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher.client; + +import android.content.Context; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import javax.net.ssl.TrustManagerFactory; + +import ch.boye.httpclientandroidlib.HttpHost; +import ch.boye.httpclientandroidlib.conn.ClientConnectionOperator; +import ch.boye.httpclientandroidlib.conn.params.ConnRoutePNames; +import ch.boye.httpclientandroidlib.conn.scheme.PlainSocketFactory; +import ch.boye.httpclientandroidlib.conn.scheme.Scheme; +import ch.boye.httpclientandroidlib.conn.scheme.SchemeRegistry; +import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; +import ch.boye.httpclientandroidlib.impl.conn.tsccm.ThreadSafeClientConnManager; +import info.guardianproject.netcipher.R; + +public class StrongHttpsClient extends DefaultHttpClient { + + final Context context; + private HttpHost proxyHost; + private String proxyType; + private SocksAwareProxyRoutePlanner routePlanner; + + private StrongSSLSocketFactory sFactory; + private SchemeRegistry mRegistry; + + private final static String TRUSTSTORE_TYPE = "BKS"; + private final static String TRUSTSTORE_PASSWORD = "changeit"; + + public StrongHttpsClient(Context context) { + this.context = context; + + mRegistry = new SchemeRegistry(); + mRegistry.register( + new Scheme(TYPE_HTTP, 80, PlainSocketFactory.getSocketFactory())); + + + try { + KeyStore keyStore = loadKeyStore(); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + sFactory = new StrongSSLSocketFactory(context, trustManagerFactory.getTrustManagers(), keyStore, TRUSTSTORE_PASSWORD); + mRegistry.register(new Scheme("https", 443, sFactory)); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + private KeyStore loadKeyStore () throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException + { + + KeyStore trustStore = KeyStore.getInstance(TRUSTSTORE_TYPE); + // load our bundled cacerts from raw assets + InputStream in = context.getResources().openRawResource(R.raw.debiancacerts); + trustStore.load(in, TRUSTSTORE_PASSWORD.toCharArray()); + + return trustStore; + } + + public StrongHttpsClient(Context context, KeyStore keystore) { + this.context = context; + + mRegistry = new SchemeRegistry(); + mRegistry.register( + new Scheme(TYPE_HTTP, 80, PlainSocketFactory.getSocketFactory())); + + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + sFactory = new StrongSSLSocketFactory(context, trustManagerFactory.getTrustManagers(), keystore, TRUSTSTORE_PASSWORD); + mRegistry.register(new Scheme("https", 443, sFactory)); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + @Override + protected ThreadSafeClientConnManager createClientConnectionManager() { + + return new ThreadSafeClientConnManager(getParams(), mRegistry) + { + @Override + protected ClientConnectionOperator createConnectionOperator( + SchemeRegistry schreg) { + + return new SocksAwareClientConnOperator(schreg, proxyHost, proxyType, + routePlanner); + } + }; + } + + public void useProxy(boolean enableTor, String type, String host, int port) + { + if (enableTor) + { + this.proxyType = type; + + if (type.equalsIgnoreCase(TYPE_SOCKS)) + { + proxyHost = new HttpHost(host, port); + } + else + { + proxyHost = new HttpHost(host, port, type); + getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxyHost); + } + } + else + { + getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY); + proxyHost = null; + } + + } + + public void disableProxy () + { + getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY); + proxyHost = null; + } + + public void useProxyRoutePlanner(SocksAwareProxyRoutePlanner proxyRoutePlanner) + { + routePlanner = proxyRoutePlanner; + setRoutePlanner(proxyRoutePlanner); + } + + /** + * NOT ADVISED, but some sites don't yet have latest protocols and ciphers available, and some + * apps still need to support them + * https://dev.guardianproject.info/issues/5644 + */ + public void enableSSLCompatibilityMode() { + sFactory.setEnableStongerDefaultProtocalVersion(false); + sFactory.setEnableStongerDefaultSSLCipherSuite(false); + } + + public final static String TYPE_SOCKS = "socks"; + public final static String TYPE_HTTP = "http"; + +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/client/StrongSSLSocketFactory.java b/libnetcipher/src/info/guardianproject/netcipher/client/StrongSSLSocketFactory.java new file mode 100644 index 00000000..2c339729 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/client/StrongSSLSocketFactory.java @@ -0,0 +1,202 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package info.guardianproject.netcipher.client; + +import android.content.Context; + +import ch.boye.httpclientandroidlib.conn.scheme.LayeredSchemeSocketFactory; +import ch.boye.httpclientandroidlib.params.HttpParams; + +import java.io.IOException; +import java.net.Proxy; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +public class StrongSSLSocketFactory extends + ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory implements + LayeredSchemeSocketFactory { + + private SSLSocketFactory mFactory = null; + + private Proxy mProxy = null; + + public static final String TLS = "TLS"; + public static final String SSL = "SSL"; + public static final String SSLV2 = "SSLv2"; + + // private X509HostnameVerifier mHostnameVerifier = new + // StrictHostnameVerifier(); + // private final HostNameResolver mNameResolver = new + // StrongHostNameResolver(); + + private boolean mEnableStongerDefaultSSLCipherSuite = true; + private boolean mEnableStongerDefaultProtocalVersion = true; + + private String[] mProtocols; + private String[] mCipherSuites; + + public StrongSSLSocketFactory(Context context, + TrustManager[] trustManagers, KeyStore keyStore, String keyStorePassword) + throws KeyManagementException, UnrecoverableKeyException, + NoSuchAlgorithmException, KeyStoreException, CertificateException, + IOException { + super(keyStore); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + KeyManager[] km = createKeyManagers( + keyStore, + keyStorePassword); + sslContext.init(km, trustManagers, new SecureRandom()); + + mFactory = sslContext.getSocketFactory(); + + } + + private void readSSLParameters(SSLSocket sslSocket) { + List protocolsToEnable = new ArrayList(); + List supportedProtocols = Arrays.asList(sslSocket.getSupportedProtocols()); + for(String enabledProtocol : StrongConstants.ENABLED_PROTOCOLS) { + if(supportedProtocols.contains(enabledProtocol)) { + protocolsToEnable.add(enabledProtocol); + } + } + this.mProtocols = protocolsToEnable.toArray(new String[protocolsToEnable.size()]); + + List cipherSuitesToEnable = new ArrayList(); + List supportedCipherSuites = Arrays.asList(sslSocket.getSupportedCipherSuites()); + for(String enabledCipherSuite : StrongConstants.ENABLED_CIPHERS) { + if(supportedCipherSuites.contains(enabledCipherSuite)) { + cipherSuitesToEnable.add(enabledCipherSuite); + } + } + this.mCipherSuites = cipherSuitesToEnable.toArray(new String[cipherSuitesToEnable.size()]); + } + + private KeyManager[] createKeyManagers(final KeyStore keystore, + final String password) throws KeyStoreException, + NoSuchAlgorithmException, UnrecoverableKeyException { + if (keystore == null) { + throw new IllegalArgumentException("Keystore may not be null"); + } + KeyManagerFactory kmfactory = KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmfactory.init(keystore, password != null ? password.toCharArray() + : null); + return kmfactory.getKeyManagers(); + } + + @Override + public Socket createSocket() throws IOException { + Socket newSocket = mFactory.createSocket(); + enableStrongerDefaults(newSocket); + return newSocket; + } + + @Override + public Socket createSocket(Socket socket, String host, int port, + boolean autoClose) throws IOException, UnknownHostException { + + Socket newSocket = mFactory.createSocket(socket, host, port, autoClose); + + enableStrongerDefaults(newSocket); + + return newSocket; + } + + /** + * Defaults the SSL connection to use a strong cipher suite and TLS version + * + * @param socket + */ + private void enableStrongerDefaults(Socket socket) { + if (isSecure(socket)) { + SSLSocket sslSocket = (SSLSocket) socket; + readSSLParameters(sslSocket); + + if (mEnableStongerDefaultProtocalVersion && mProtocols != null) { + sslSocket.setEnabledProtocols(mProtocols); + } + + if (mEnableStongerDefaultSSLCipherSuite && mCipherSuites != null) { + sslSocket.setEnabledCipherSuites(mCipherSuites); + } + } + } + + @Override + public boolean isSecure(Socket sock) throws IllegalArgumentException { + return (sock instanceof SSLSocket); + } + + public void setProxy(Proxy proxy) { + mProxy = proxy; + } + + public Proxy getProxy() { + return mProxy; + } + + public boolean isEnableStongerDefaultSSLCipherSuite() { + return mEnableStongerDefaultSSLCipherSuite; + } + + public void setEnableStongerDefaultSSLCipherSuite(boolean enable) { + this.mEnableStongerDefaultSSLCipherSuite = enable; + } + + public boolean isEnableStongerDefaultProtocalVersion() { + return mEnableStongerDefaultProtocalVersion; + } + + public void setEnableStongerDefaultProtocalVersion(boolean enable) { + this.mEnableStongerDefaultProtocalVersion = enable; + } + + @Override + public Socket createSocket(HttpParams httpParams) throws IOException { + Socket newSocket = mFactory.createSocket(); + + enableStrongerDefaults(newSocket); + + return newSocket; + + } + + @Override + public Socket createLayeredSocket(Socket arg0, String arg1, int arg2, + boolean arg3) throws IOException, UnknownHostException { + return ((LayeredSchemeSocketFactory) mFactory).createLayeredSocket( + arg0, arg1, arg2, arg3); + } + +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/client/TlsOnlySocketFactory.java b/libnetcipher/src/info/guardianproject/netcipher/client/TlsOnlySocketFactory.java new file mode 100644 index 00000000..f2892e07 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/client/TlsOnlySocketFactory.java @@ -0,0 +1,544 @@ +/* + * Copyright 2015 Bhavit Singh Sengar + * Copyright 2015-2016 Hans-Christoph Steiner + * Copyright 2015-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * From https://stackoverflow.com/a/29946540 + */ + +package info.guardianproject.netcipher.client; + +import android.net.SSLCertificateSocketFactory; +import android.os.Build; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * While making a secure connection, Android's {@link HttpsURLConnection} falls + * back to SSLv3 from TLSv1. This is a bug in android versions < 4.4. It can be + * fixed by removing the SSLv3 protocol from Enabled Protocols list. Use this as + * the {@link SSLSocketFactory} for + * {@link HttpsURLConnection#setDefaultSSLSocketFactory(SSLSocketFactory)} + * + * @author Bhavit S. Sengar + * @author Hans-Christoph Steiner + */ +public class TlsOnlySocketFactory extends SSLSocketFactory { + private static final int HANDSHAKE_TIMEOUT=0; + private static final String TAG = "TlsOnlySocketFactory"; + private final SSLSocketFactory delegate; + private final boolean compatible; + + public TlsOnlySocketFactory() { + this.delegate =SSLCertificateSocketFactory.getDefault(HANDSHAKE_TIMEOUT, null); + this.compatible = false; + } + + public TlsOnlySocketFactory(SSLSocketFactory delegate) { + this.delegate = delegate; + this.compatible = false; + } + + /** + * Make {@link SSLSocket}s that are compatible with outdated servers. + * + * @param delegate + * @param compatible + */ + public TlsOnlySocketFactory(SSLSocketFactory delegate, boolean compatible) { + this.delegate = delegate; + this.compatible = compatible; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + private Socket makeSocketSafe(Socket socket, String host) { + if (socket instanceof SSLSocket) { + TlsOnlySSLSocket tempSocket= + new TlsOnlySSLSocket((SSLSocket) socket, compatible); + + if (delegate instanceof SSLCertificateSocketFactory && + Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN_MR1) { + ((android.net.SSLCertificateSocketFactory)delegate) + .setHostname(socket, host); + } + else { + tempSocket.setHostname(host); + } + + socket = tempSocket; + } + return socket; + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + return makeSocketSafe(delegate.createSocket(s, host, port, autoClose), host); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return makeSocketSafe(delegate.createSocket(host, port), host); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException { + return makeSocketSafe(delegate.createSocket(host, port, localHost, localPort), host); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return makeSocketSafe(delegate.createSocket(host, port), host.getHostName()); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, + int localPort) throws IOException { + return makeSocketSafe(delegate.createSocket(address, port, localAddress, localPort), + address.getHostName()); + } + + private class TlsOnlySSLSocket extends DelegateSSLSocket { + + final boolean compatible; + + private TlsOnlySSLSocket(SSLSocket delegate, boolean compatible) { + super(delegate); + this.compatible = compatible; + + // badly configured servers can't handle a good config + if (compatible) { + ArrayList protocols = new ArrayList(Arrays.asList(delegate + .getEnabledProtocols())); + protocols.remove("SSLv2"); + protocols.remove("SSLv3"); + super.setEnabledProtocols(protocols.toArray(new String[protocols.size()])); + + /* + * Exclude extremely weak EXPORT ciphers. NULL ciphers should + * never even have been an option in TLS. + */ + ArrayList enabled = new ArrayList(10); + Pattern exclude = Pattern.compile(".*(EXPORT|NULL).*"); + for (String cipher : delegate.getEnabledCipherSuites()) { + if (!exclude.matcher(cipher).matches()) { + enabled.add(cipher); + } + } + super.setEnabledCipherSuites(enabled.toArray(new String[enabled.size()])); + return; + } // else + + // 16-19 support v1.1 and v1.2 but only by default starting in 20+ + // https://developer.android.com/reference/javax/net/ssl/SSLSocket.html + ArrayList protocols = new ArrayList(Arrays.asList(delegate + .getSupportedProtocols())); + protocols.remove("SSLv2"); + protocols.remove("SSLv3"); + super.setEnabledProtocols(protocols.toArray(new String[protocols.size()])); + + /* + * Exclude weak ciphers, like EXPORT, MD5, DES, and DH. NULL ciphers + * should never even have been an option in TLS. + */ + ArrayList enabledCiphers = new ArrayList(10); + Pattern exclude = Pattern.compile(".*(_DES|DH_|DSS|EXPORT|MD5|NULL|RC4).*"); + for (String cipher : delegate.getSupportedCipherSuites()) { + if (!exclude.matcher(cipher).matches()) { + enabledCiphers.add(cipher); + } + } + super.setEnabledCipherSuites(enabledCiphers.toArray(new String[enabledCiphers.size()])); + } + + /** + * This works around a bug in Android < 19 where SSLv3 is forced + */ + @Override + public void setEnabledProtocols(String[] protocols) { + if (protocols != null && protocols.length == 1 && "SSLv3".equals(protocols[0])) { + List systemProtocols; + if (this.compatible) { + systemProtocols = Arrays.asList(delegate.getEnabledProtocols()); + } else { + systemProtocols = Arrays.asList(delegate.getSupportedProtocols()); + } + List enabledProtocols = new ArrayList(systemProtocols); + if (enabledProtocols.size() > 1) { + enabledProtocols.remove("SSLv2"); + enabledProtocols.remove("SSLv3"); + } else { + Log.w(TAG, "SSL stuck with protocol available for " + + String.valueOf(enabledProtocols)); + } + protocols = enabledProtocols.toArray(new String[enabledProtocols.size()]); + } + super.setEnabledProtocols(protocols); + } + } + + public class DelegateSSLSocket extends SSLSocket { + + protected final SSLSocket delegate; + + DelegateSSLSocket(SSLSocket delegate) { + this.delegate = delegate; + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public String[] getEnabledCipherSuites() { + return delegate.getEnabledCipherSuites(); + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + delegate.setEnabledCipherSuites(suites); + } + + @Override + public String[] getSupportedProtocols() { + return delegate.getSupportedProtocols(); + } + + @Override + public String[] getEnabledProtocols() { + return delegate.getEnabledProtocols(); + } + + @Override + public void setEnabledProtocols(String[] protocols) { + delegate.setEnabledProtocols(protocols); + } + + @Override + public SSLSession getSession() { + return delegate.getSession(); + } + + @Override + public void addHandshakeCompletedListener(HandshakeCompletedListener listener) { + delegate.addHandshakeCompletedListener(listener); + } + + @Override + public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) { + delegate.removeHandshakeCompletedListener(listener); + } + + @Override + public void startHandshake() throws IOException { + delegate.startHandshake(); + } + + @Override + public void setUseClientMode(boolean mode) { + delegate.setUseClientMode(mode); + } + + @Override + public boolean getUseClientMode() { + return delegate.getUseClientMode(); + } + + @Override + public void setNeedClientAuth(boolean need) { + delegate.setNeedClientAuth(need); + } + + @Override + public void setWantClientAuth(boolean want) { + delegate.setWantClientAuth(want); + } + + @Override + public boolean getNeedClientAuth() { + return delegate.getNeedClientAuth(); + } + + @Override + public boolean getWantClientAuth() { + return delegate.getWantClientAuth(); + } + + @Override + public void setEnableSessionCreation(boolean flag) { + delegate.setEnableSessionCreation(flag); + } + + @Override + public boolean getEnableSessionCreation() { + return delegate.getEnableSessionCreation(); + } + + @Override + public void bind(SocketAddress localAddr) throws IOException { + delegate.bind(localAddr); + } + + @Override + public synchronized void close() throws IOException { + delegate.close(); + } + + @Override + public void connect(SocketAddress remoteAddr) throws IOException { + delegate.connect(remoteAddr); + } + + @Override + public void connect(SocketAddress remoteAddr, int timeout) throws IOException { + delegate.connect(remoteAddr, timeout); + } + + @Override + public SocketChannel getChannel() { + return delegate.getChannel(); + } + + @Override + public InetAddress getInetAddress() { + return delegate.getInetAddress(); + } + + @Override + public InputStream getInputStream() throws IOException { + return delegate.getInputStream(); + } + + @Override + public boolean getKeepAlive() throws SocketException { + return delegate.getKeepAlive(); + } + + @Override + public InetAddress getLocalAddress() { + return delegate.getLocalAddress(); + } + + @Override + public int getLocalPort() { + return delegate.getLocalPort(); + } + + @Override + public SocketAddress getLocalSocketAddress() { + return delegate.getLocalSocketAddress(); + } + + @Override + public boolean getOOBInline() throws SocketException { + return delegate.getOOBInline(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return delegate.getOutputStream(); + } + + @Override + public int getPort() { + return delegate.getPort(); + } + + @Override + public synchronized int getReceiveBufferSize() throws SocketException { + return delegate.getReceiveBufferSize(); + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return delegate.getRemoteSocketAddress(); + } + + @Override + public boolean getReuseAddress() throws SocketException { + return delegate.getReuseAddress(); + } + + @Override + public synchronized int getSendBufferSize() throws SocketException { + return delegate.getSendBufferSize(); + } + + @Override + public int getSoLinger() throws SocketException { + return delegate.getSoLinger(); + } + + @Override + public synchronized int getSoTimeout() throws SocketException { + return delegate.getSoTimeout(); + } + + @Override + public boolean getTcpNoDelay() throws SocketException { + return delegate.getTcpNoDelay(); + } + + @Override + public int getTrafficClass() throws SocketException { + return delegate.getTrafficClass(); + } + + @Override + public boolean isBound() { + return delegate.isBound(); + } + + @Override + public boolean isClosed() { + return delegate.isClosed(); + } + + @Override + public boolean isConnected() { + return delegate.isConnected(); + } + + @Override + public boolean isInputShutdown() { + return delegate.isInputShutdown(); + } + + @Override + public boolean isOutputShutdown() { + return delegate.isOutputShutdown(); + } + + @Override + public void sendUrgentData(int value) throws IOException { + delegate.sendUrgentData(value); + } + + @Override + public void setKeepAlive(boolean keepAlive) throws SocketException { + delegate.setKeepAlive(keepAlive); + } + + @Override + public void setOOBInline(boolean oobinline) throws SocketException { + delegate.setOOBInline(oobinline); + } + + @Override + public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { + delegate.setPerformancePreferences(connectionTime, + latency, bandwidth); + } + + @Override + public synchronized void setReceiveBufferSize(int size) throws SocketException { + delegate.setReceiveBufferSize(size); + } + + @Override + public void setReuseAddress(boolean reuse) throws SocketException { + delegate.setReuseAddress(reuse); + } + + @Override + public synchronized void setSendBufferSize(int size) throws SocketException { + delegate.setSendBufferSize(size); + } + + @Override + public void setSoLinger(boolean on, int timeout) throws SocketException { + delegate.setSoLinger(on, timeout); + } + + @Override + public synchronized void setSoTimeout(int timeout) throws SocketException { + delegate.setSoTimeout(timeout); + } + + @Override + public void setTcpNoDelay(boolean on) throws SocketException { + delegate.setTcpNoDelay(on); + } + + @Override + public void setTrafficClass(int value) throws SocketException { + delegate.setTrafficClass(value); + } + + @Override + public void shutdownInput() throws IOException { + delegate.shutdownInput(); + } + + @Override + public void shutdownOutput() throws IOException { + delegate.shutdownOutput(); + } + + // inspired by https://github.com/k9mail/k-9/commit/54f9fd36a77423a55f63fbf9b1bcea055a239768 + + public DelegateSSLSocket setHostname(String host) { + try { + delegate + .getClass() + .getMethod("setHostname", String.class) + .invoke(delegate, host); + } + catch (Exception e) { + throw new IllegalStateException("Could not enable SNI", e); + } + + return(this); + } + + @Override + public String toString() { + return delegate.toString(); + } + + @Override + public boolean equals(Object o) { + return delegate.equals(o); + } + } +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/proxy/OrbotHelper.java b/libnetcipher/src/info/guardianproject/netcipher/proxy/OrbotHelper.java new file mode 100644 index 00000000..2d0b699e --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/proxy/OrbotHelper.java @@ -0,0 +1,701 @@ +/* + * Copyright 2014-2016 Hans-Christoph Steiner + * Copyright 2012-2016 Nathan Freitas + * Portions Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher.proxy; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Log; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Utility class to simplify setting up a proxy connection + * to Orbot. + * + * If you are using classes in the info.guardianproject.netcipher.client + * package, call OrbotHelper.get(this).init(); from onCreate() + * of a custom Application subclass, or from some other guaranteed + * entry point to your app. At that point, the + * info.guardianproject.netcipher.client classes will be ready + * for use. + */ +public class OrbotHelper implements ProxyHelper { + + private final static int REQUEST_CODE_STATUS = 100; + + public final static String ORBOT_PACKAGE_NAME = "org.torproject.android"; + public final static String ORBOT_MARKET_URI = "market://details?id=" + ORBOT_PACKAGE_NAME; + public final static String ORBOT_FDROID_URI = "https://f-droid.org/repository/browse/?fdid=" + + ORBOT_PACKAGE_NAME; + public final static String ORBOT_PLAY_URI = "https://play.google.com/store/apps/details?id=" + + ORBOT_PACKAGE_NAME; + + /** + * A request to Orbot to transparently start Tor services + */ + public final static String ACTION_START = "org.torproject.android.intent.action.START"; + + /** + * {@link Intent} send by Orbot with {@code ON/OFF/STARTING/STOPPING} status + * included as an {@link #EXTRA_STATUS} {@code String}. Your app should + * always receive {@code ACTION_STATUS Intent}s since any other app could + * start Orbot. Also, user-triggered starts and stops will also cause + * {@code ACTION_STATUS Intent}s to be broadcast. + */ + public final static String ACTION_STATUS = "org.torproject.android.intent.action.STATUS"; + + /** + * {@code String} that contains a status constant: {@link #STATUS_ON}, + * {@link #STATUS_OFF}, {@link #STATUS_STARTING}, or + * {@link #STATUS_STOPPING} + */ + public final static String EXTRA_STATUS = "org.torproject.android.intent.extra.STATUS"; + /** + * A {@link String} {@code packageName} for Orbot to direct its status reply + * to, used in {@link #ACTION_START} {@link Intent}s sent to Orbot + */ + public final static String EXTRA_PACKAGE_NAME = "org.torproject.android.intent.extra.PACKAGE_NAME"; + + public final static String EXTRA_PROXY_PORT_HTTP = "org.torproject.android.intent.extra.HTTP_PROXY_PORT"; + public final static String EXTRA_PROXY_PORT_SOCKS = "org.torproject.android.intent.extra.SOCKS_PROXY_PORT"; + + + /** + * All tor-related services and daemons are stopped + */ + public final static String STATUS_OFF = "OFF"; + /** + * All tor-related services and daemons have completed starting + */ + public final static String STATUS_ON = "ON"; + public final static String STATUS_STARTING = "STARTING"; + public final static String STATUS_STOPPING = "STOPPING"; + /** + * The user has disabled the ability for background starts triggered by + * apps. Fallback to the old Intent that brings up Orbot. + */ + public final static String STATUS_STARTS_DISABLED = "STARTS_DISABLED"; + + public final static String ACTION_START_TOR = "org.torproject.android.START_TOR"; + public final static String ACTION_REQUEST_HS = "org.torproject.android.REQUEST_HS_PORT"; + public final static int START_TOR_RESULT = 0x9234; + public final static int HS_REQUEST_CODE = 9999; + + +/* + private OrbotHelper() { + // only static utility methods, do not instantiate + } +*/ + + /** + * Test whether a {@link URL} is a Tor Hidden Service host name, also known + * as an ".onion address". + * + * @return whether the host name is a Tor .onion address + */ + public static boolean isOnionAddress(URL url) { + return url.getHost().endsWith(".onion"); + } + + /** + * Test whether a URL {@link String} is a Tor Hidden Service host name, also known + * as an ".onion address". + * + * @return whether the host name is a Tor .onion address + */ + public static boolean isOnionAddress(String urlString) { + try { + return isOnionAddress(new URL(urlString)); + } catch (MalformedURLException e) { + return false; + } + } + + /** + * Test whether a {@link Uri} is a Tor Hidden Service host name, also known + * as an ".onion address". + * + * @return whether the host name is a Tor .onion address + */ + public static boolean isOnionAddress(Uri uri) { + return uri.getHost().endsWith(".onion"); + } + + /** + * Check if the tor process is running. This method is very + * brittle, and is therefore deprecated in favor of using the + * {@link #ACTION_STATUS} {@code Intent} along with the + * {@link #requestStartTor(Context)} method. + */ + @Deprecated + public static boolean isOrbotRunning(Context context) { + int procId = TorServiceUtils.findProcessId(context); + + return (procId != -1); + } + + public static boolean isOrbotInstalled(Context context) { + return isAppInstalled(context, ORBOT_PACKAGE_NAME); + } + + private static boolean isAppInstalled(Context context, String uri) { + try { + PackageManager pm = context.getPackageManager(); + pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + public static void requestHiddenServiceOnPort(Activity activity, int port) { + Intent intent = new Intent(ACTION_REQUEST_HS); + intent.setPackage(ORBOT_PACKAGE_NAME); + intent.putExtra("hs_port", port); + + activity.startActivityForResult(intent, HS_REQUEST_CODE); + } + + /** + * First, checks whether Orbot is installed. If Orbot is installed, then a + * broadcast {@link Intent} is sent to request Orbot to start + * transparently in the background. When Orbot receives this {@code + * Intent}, it will immediately reply to the app that called this method + * with an {@link #ACTION_STATUS} {@code Intent} that is broadcast to the + * {@code packageName} of the provided {@link Context} (i.e. {@link + * Context#getPackageName()}. + *

+ * That reply {@link #ACTION_STATUS} {@code Intent} could say that the user + * has disabled background starts with the status + * {@link #STATUS_STARTS_DISABLED}. That means that Orbot ignored this + * request. To directly prompt the user to start Tor, use + * {@link #requestShowOrbotStart(Activity)}, which will bring up + * Orbot itself for the user to manually start Tor. Orbot always broadcasts + * it's status, so your app will receive those no matter how Tor gets + * started. + * + * @param context the app {@link Context} will receive the reply + * @return whether the start request was sent to Orbot + * @see #requestShowOrbotStart(Activity activity) + */ + public static boolean requestStartTor(Context context) { + if (OrbotHelper.isOrbotInstalled(context)) { + Log.i("OrbotHelper", "requestStartTor " + context.getPackageName()); + Intent intent = getOrbotStartIntent(context); + context.sendBroadcast(intent); + return true; + } + return false; + } + + /** + * Gets an {@link Intent} for starting Orbot. Orbot will reply with the + * current status to the {@code packageName} of the app in the provided + * {@link Context} (i.e. {@link Context#getPackageName()}. + */ + public static Intent getOrbotStartIntent(Context context) { + Intent intent = new Intent(ACTION_START); + intent.setPackage(ORBOT_PACKAGE_NAME); + intent.putExtra(EXTRA_PACKAGE_NAME, context.getPackageName()); + return intent; + } + + /** + * Gets a barebones {@link Intent} for starting Orbot. This is deprecated + * in favor of {@link #getOrbotStartIntent(Context)}. + */ + @Deprecated + public static Intent getOrbotStartIntent() { + Intent intent = new Intent(ACTION_START); + intent.setPackage(ORBOT_PACKAGE_NAME); + return intent; + } + + /** + * First, checks whether Orbot is installed, then checks whether Orbot is + * running. If Orbot is installed and not running, then an {@link Intent} is + * sent to request the user to start Orbot, which will show the main Orbot screen. + * The result will be returned in + * {@link Activity#onActivityResult(int requestCode, int resultCode, Intent data)} + * with a {@code requestCode} of {@code START_TOR_RESULT} + *

+ * Orbot will also always broadcast the status of starting Tor via the + * {@link #ACTION_STATUS} Intent, no matter how it is started. + * + * @param activity the {@code Activity} that gets the result of the + * {@link #START_TOR_RESULT} request + * @return whether the start request was sent to Orbot + * @see #requestStartTor(Context context) + */ + public static boolean requestShowOrbotStart(Activity activity) { + if (OrbotHelper.isOrbotInstalled(activity)) { + if (!OrbotHelper.isOrbotRunning(activity)) { + Intent intent = getShowOrbotStartIntent(); + activity.startActivityForResult(intent, START_TOR_RESULT); + return true; + } + } + return false; + } + + public static Intent getShowOrbotStartIntent() { + Intent intent = new Intent(ACTION_START_TOR); + intent.setPackage(ORBOT_PACKAGE_NAME); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } + + public static Intent getOrbotInstallIntent(Context context) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(ORBOT_MARKET_URI)); + + PackageManager pm = context.getPackageManager(); + List resInfos = pm.queryIntentActivities(intent, 0); + + String foundPackageName = null; + for (ResolveInfo r : resInfos) { + Log.i("OrbotHelper", "market: " + r.activityInfo.packageName); + if (TextUtils.equals(r.activityInfo.packageName, FDROID_PACKAGE_NAME) + || TextUtils.equals(r.activityInfo.packageName, PLAY_PACKAGE_NAME)) { + foundPackageName = r.activityInfo.packageName; + break; + } + } + + if (foundPackageName == null) { + intent.setData(Uri.parse(ORBOT_FDROID_URI)); + } else { + intent.setPackage(foundPackageName); + } + return intent; + } + + @Override + public boolean isInstalled(Context context) { + return isOrbotInstalled(context); + } + + @Override + public void requestStatus(Context context) { + isOrbotRunning(context); + } + + @Override + public boolean requestStart(Context context) { + return requestStartTor(context); + } + + @Override + public Intent getInstallIntent(Context context) { + return getOrbotInstallIntent(context); + } + + @Override + public Intent getStartIntent(Context context) { + return getOrbotStartIntent(); + } + + @Override + public String getName() { + return "Orbot"; + } + + /* MLM additions */ + + private final Context ctxt; + private final Handler handler; + private boolean isInstalled=false; + private Intent lastStatusIntent=null; + private Set statusCallbacks= + newSetFromMap(new WeakHashMap()); + private Set installCallbacks= + newSetFromMap(new WeakHashMap()); + private long statusTimeoutMs=30000L; + private long installTimeoutMs=60000L; + private boolean validateOrbot=true; + + abstract public static class SimpleStatusCallback + implements StatusCallback { + @Override + public void onEnabled(Intent statusIntent) { + // no-op; extend and override if needed + } + + @Override + public void onStarting() { + // no-op; extend and override if needed + } + + @Override + public void onStopping() { + // no-op; extend and override if needed + } + + @Override + public void onDisabled() { + // no-op; extend and override if needed + } + + @Override + public void onNotYetInstalled() { + // no-op; extend and override if needed + } + } + + /** + * Callback interface used for reporting the results of an + * attempt to install Orbot + */ + public interface InstallCallback { + void onInstalled(); + void onInstallTimeout(); + } + + private static volatile OrbotHelper INSTANCE; + + /** + * Retrieves the singleton, initializing if if needed + * + * @param ctxt any Context will do, as we will hold onto + * the Application + * @return the singleton + */ + synchronized public static OrbotHelper get(Context ctxt) { + if (INSTANCE==null) { + INSTANCE=new OrbotHelper(ctxt); + } + + return(INSTANCE); + } + + /** + * Standard constructor + * + * @param ctxt any Context will do; OrbotInitializer will hold + * onto the Application context + */ + private OrbotHelper(Context ctxt) { + this.ctxt=ctxt.getApplicationContext(); + this.handler=new Handler(Looper.getMainLooper()); + } + + /** + * Adds a StatusCallback to be called when we find out that + * Orbot is ready. If Orbot is ready for use, your callback + * will be called with onEnabled() immediately, before this + * method returns. + * + * @param cb a callback + * @return the singleton, for chaining + */ + public OrbotHelper addStatusCallback(StatusCallback cb) { + statusCallbacks.add(cb); + + if (lastStatusIntent!=null) { + String status= + lastStatusIntent.getStringExtra(OrbotHelper.EXTRA_STATUS); + + if (status.equals(OrbotHelper.STATUS_ON)) { + cb.onEnabled(lastStatusIntent); + } + } + + return(this); + } + + /** + * Removes an existing registered StatusCallback. + * + * @param cb the callback to remove + * @return the singleton, for chaining + */ + public OrbotHelper removeStatusCallback(StatusCallback cb) { + statusCallbacks.remove(cb); + + return(this); + } + + + /** + * Adds an InstallCallback to be called when we find out that + * Orbot is installed + * + * @param cb a callback + * @return the singleton, for chaining + */ + public OrbotHelper addInstallCallback(InstallCallback cb) { + installCallbacks.add(cb); + + return(this); + } + + /** + * Removes an existing registered InstallCallback. + * + * @param cb the callback to remove + * @return the singleton, for chaining + */ + public OrbotHelper removeInstallCallback(InstallCallback cb) { + installCallbacks.remove(cb); + + return(this); + } + + /** + * Sets how long of a delay, in milliseconds, after trying + * to get a status from Orbot before we give up. + * Defaults to 30000ms = 30 seconds = 0.000347222 days + * + * @param timeoutMs delay period in milliseconds + * @return the singleton, for chaining + */ + public OrbotHelper statusTimeout(long timeoutMs) { + statusTimeoutMs=timeoutMs; + + return(this); + } + + /** + * Sets how long of a delay, in milliseconds, after trying + * to install Orbot do we assume that it's not happening. + * Defaults to 60000ms = 60 seconds = 1 minute = 1.90259e-6 years + * + * @param timeoutMs delay period in milliseconds + * @return the singleton, for chaining + */ + public OrbotHelper installTimeout(long timeoutMs) { + installTimeoutMs=timeoutMs; + + return(this); + } + + /** + * By default, NetCipher ensures that the Orbot on the + * device is one of the official builds. Call this method + * to skip that validation. Mostly, this is for developers + * who have their own custom Orbot builds (e.g., for + * dedicated hardware). + * + * @return the singleton, for chaining + */ + public OrbotHelper skipOrbotValidation() { + validateOrbot=false; + + return(this); + } + + /** + * @return true if Orbot is installed (the last time we checked), + * false otherwise + */ + public boolean isInstalled() { + return(isInstalled); + } + + /** + * Initializes the connection to Orbot, revalidating that it + * is installed and requesting fresh status broadcasts. + * + * @return true if initialization is proceeding, false if + * Orbot is not installed + */ + public boolean init() { + Intent orbot=OrbotHelper.getOrbotStartIntent(ctxt); + + if (validateOrbot) { + ArrayList hashes=new ArrayList(); + + hashes.add("A4:54:B8:7A:18:47:A8:9E:D7:F5:E7:0F:BA:6B:BA:96:F3:EF:29:C2:6E:09:81:20:4F:E3:47:BF:23:1D:FD:5B"); + hashes.add("A7:02:07:92:4F:61:FF:09:37:1D:54:84:14:5C:4B:EE:77:2C:55:C1:9E:EE:23:2F:57:70:E1:82:71:F7:CB:AE"); + + orbot= + SignatureUtils.validateBroadcastIntent(ctxt, orbot, + hashes, false); + } + + if (orbot!=null) { + isInstalled=true; + handler.postDelayed(onStatusTimeout, statusTimeoutMs); + ctxt.registerReceiver(orbotStatusReceiver, + new IntentFilter(OrbotHelper.ACTION_STATUS)); + ctxt.sendBroadcast(orbot); + } + else { + isInstalled=false; + + for (StatusCallback cb : statusCallbacks) { + cb.onNotYetInstalled(); + } + } + + return(isInstalled); + } + + /** + * Given that init() returned false, calling installOrbot() + * will trigger an attempt to install Orbot from an available + * distribution channel (e.g., the Play Store). Only call this + * if the user is expecting it, such as in response to tapping + * a dialog button or an action bar item. + * + * Note that installation may take a long time, even if + * the user is proceeding with the installation, due to network + * speeds, waiting for user input, and so on. Either specify + * a long timeout, or consider the timeout to be merely advisory + * and use some other user input to cause you to try + * init() again after, presumably, Orbot has been installed + * and configured by the user. + * + * If the user does install Orbot, we will attempt init() + * again automatically. Hence, you will probably need user input + * to tell you when the user has gotten Orbot up and going. + * + * @param host the Activity that is triggering this work + */ + public void installOrbot(Activity host) { + handler.postDelayed(onInstallTimeout, installTimeoutMs); + + IntentFilter filter= + new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + + filter.addDataScheme("package"); + + ctxt.registerReceiver(orbotInstallReceiver, filter); + host.startActivity(OrbotHelper.getOrbotInstallIntent(ctxt)); + } + + private BroadcastReceiver orbotStatusReceiver=new BroadcastReceiver() { + @Override + public void onReceive(Context ctxt, Intent intent) { + if (TextUtils.equals(intent.getAction(), + OrbotHelper.ACTION_STATUS)) { + String status=intent.getStringExtra(OrbotHelper.EXTRA_STATUS); + + if (status.equals(OrbotHelper.STATUS_ON)) { + lastStatusIntent=intent; + handler.removeCallbacks(onStatusTimeout); + + for (StatusCallback cb : statusCallbacks) { + cb.onEnabled(intent); + } + } + else if (status.equals(OrbotHelper.STATUS_OFF)) { + for (StatusCallback cb : statusCallbacks) { + cb.onDisabled(); + } + } + else if (status.equals(OrbotHelper.STATUS_STARTING)) { + for (StatusCallback cb : statusCallbacks) { + cb.onStarting(); + } + } + else if (status.equals(OrbotHelper.STATUS_STOPPING)) { + for (StatusCallback cb : statusCallbacks) { + cb.onStopping(); + } + } + } + } + }; + + private Runnable onStatusTimeout=new Runnable() { + @Override + public void run() { + ctxt.unregisterReceiver(orbotStatusReceiver); + + for (StatusCallback cb : statusCallbacks) { + cb.onStatusTimeout(); + } + } + }; + + private BroadcastReceiver orbotInstallReceiver=new BroadcastReceiver() { + @Override + public void onReceive(Context ctxt, Intent intent) { + if (TextUtils.equals(intent.getAction(), + Intent.ACTION_PACKAGE_ADDED)) { + String pkgName=intent.getData().getEncodedSchemeSpecificPart(); + + if (OrbotHelper.ORBOT_PACKAGE_NAME.equals(pkgName)) { + isInstalled=true; + handler.removeCallbacks(onInstallTimeout); + ctxt.unregisterReceiver(orbotInstallReceiver); + + for (InstallCallback cb : installCallbacks) { + cb.onInstalled(); + } + + init(); + } + } + } + }; + + private Runnable onInstallTimeout=new Runnable() { + @Override + public void run() { + ctxt.unregisterReceiver(orbotInstallReceiver); + + for (InstallCallback cb : installCallbacks) { + cb.onInstallTimeout(); + } + } + }; + + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + static Set newSetFromMap(Map map) { + if (map.isEmpty()) { + return new SetFromMap(map); + } + throw new IllegalArgumentException("map not empty"); + } +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/proxy/ProxyHelper.java b/libnetcipher/src/info/guardianproject/netcipher/proxy/ProxyHelper.java new file mode 100644 index 00000000..54cbb5e0 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/proxy/ProxyHelper.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2016 Nathan Freitas + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package info.guardianproject.netcipher.proxy; + +import android.content.Context; +import android.content.Intent; + +public interface ProxyHelper { + + public boolean isInstalled (Context context); + public void requestStatus (Context context); + public boolean requestStart (Context context); + public Intent getInstallIntent (Context context); + public Intent getStartIntent (Context context); + public String getName (); + + public final static String FDROID_PACKAGE_NAME = "org.fdroid.fdroid"; + public final static String PLAY_PACKAGE_NAME = "com.android.vending"; + + /** + * A request to Orbot to transparently start Tor services + */ + public final static String ACTION_START = "android.intent.action.PROXY_START"; + /** + * {@link Intent} send by Orbot with {@code ON/OFF/STARTING/STOPPING} status + */ + public final static String ACTION_STATUS = "android.intent.action.PROXY_STATUS"; + /** + * {@code String} that contains a status constant: {@link #STATUS_ON}, + * {@link #STATUS_OFF}, {@link #STATUS_STARTING}, or + * {@link #STATUS_STOPPING} + */ + public final static String EXTRA_STATUS = "android.intent.extra.PROXY_STATUS"; + + public final static String EXTRA_PROXY_PORT_HTTP = "android.intent.extra.PROXY_PORT_HTTP"; + public final static String EXTRA_PROXY_PORT_SOCKS = "android.intent.extra.PROXY_PORT_SOCKS"; + + /** + * A {@link String} {@code packageName} for Orbot to direct its status reply + * to, used in {@link #ACTION_START} {@link Intent}s sent to Orbot + */ + public final static String EXTRA_PACKAGE_NAME = "android.intent.extra.PROXY_PACKAGE_NAME"; + + /** + * All tor-related services and daemons are stopped + */ + public final static String STATUS_OFF = "OFF"; + /** + * All tor-related services and daemons have completed starting + */ + public final static String STATUS_ON = "ON"; + public final static String STATUS_STARTING = "STARTING"; + public final static String STATUS_STOPPING = "STOPPING"; + /** + * The user has disabled the ability for background starts triggered by + * apps. Fallback to the old Intent that brings up Orbot. + */ + public final static String STATUS_STARTS_DISABLED = "STARTS_DISABLED"; +} + diff --git a/libnetcipher/src/info/guardianproject/netcipher/proxy/ProxySelector.java b/libnetcipher/src/info/guardianproject/netcipher/proxy/ProxySelector.java new file mode 100644 index 00000000..bb012afb --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/proxy/ProxySelector.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package info.guardianproject.netcipher.proxy; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.SocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import android.util.Log; + +public class ProxySelector extends java.net.ProxySelector { + + private ArrayList listProxies; + + public ProxySelector () + { + super (); + + listProxies = new ArrayList(); + + + } + + public void addProxy (Proxy.Type type,String host, int port) + { + Proxy proxy = new Proxy(type,new InetSocketAddress(host, port)); + listProxies.add(proxy); + } + + @Override + public void connectFailed(URI uri, SocketAddress address, + IOException failure) { + Log.w("ProxySelector","could not connect to " + address.toString() + ": " + failure.getMessage()); + } + + @Override + public List select(URI uri) { + + return listProxies; + } + +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/proxy/PsiphonHelper.java b/libnetcipher/src/info/guardianproject/netcipher/proxy/PsiphonHelper.java new file mode 100644 index 00000000..bcb3b45c --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/proxy/PsiphonHelper.java @@ -0,0 +1,177 @@ +/* + * Copyright 2012-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package info.guardianproject.netcipher.proxy; + +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.List; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.text.TextUtils; + +public class PsiphonHelper implements ProxyHelper { + + public final static String PACKAGE_NAME = "com.psiphon3"; + public final static String COMPONENT_NAME = "com.psiphon3.StatusActivity"; + + + public final static String MARKET_URI = "market://details?id=" + PACKAGE_NAME; + public final static String FDROID_URI = "https://f-droid.org/repository/browse/?fdid=" + + PACKAGE_NAME; + public final static String ORBOT_PLAY_URI = "https://play.google.com/store/apps/details?id=" + + PACKAGE_NAME; + + public final static int DEFAULT_SOCKS_PORT = 1080; + public final static int DEFAULT_HTTP_PORT = 8080; + + @Override + public boolean isInstalled(Context context) { + return isAppInstalled(context, PACKAGE_NAME); + } + + + private static boolean isAppInstalled(Context context, String uri) { + try { + PackageManager pm = context.getPackageManager(); + pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + @Override + public void requestStatus(final Context context) { + + Thread thread = new Thread () + { + public void run () + { + //can connect to default HTTP proxy port? + boolean isSocksOpen = false; + boolean isHttpOpen = false; + + int socksPort = DEFAULT_SOCKS_PORT; + int httpPort = DEFAULT_HTTP_PORT; + + for (int i = 0; i < 10 && (!isSocksOpen); i++) + isSocksOpen = isPortOpen("127.0.0.1",socksPort++,100); + + for (int i = 0; i < 10 && (!isHttpOpen); i++) + isHttpOpen = isPortOpen("127.0.0.1",httpPort++,100); + + //any other check? + + Intent intent = new Intent(ProxyHelper.ACTION_STATUS); + intent.putExtra(EXTRA_PACKAGE_NAME, PACKAGE_NAME); + + if (isSocksOpen && isHttpOpen) + { + intent.putExtra(EXTRA_STATUS, STATUS_ON); + + intent.putExtra(EXTRA_PROXY_PORT_HTTP, httpPort-1); + intent.putExtra(EXTRA_PROXY_PORT_SOCKS, socksPort-1); + + + } + else + { + intent.putExtra(EXTRA_STATUS, STATUS_OFF); + } + + context.sendBroadcast(intent); + } + }; + + thread.start(); + + } + + @Override + public boolean requestStart(Context context) { + + Intent intent = getStartIntent(context); + // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + + return true; + } + + @Override + public Intent getInstallIntent(Context context) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(MARKET_URI)); + + PackageManager pm = context.getPackageManager(); + List resInfos = pm.queryIntentActivities(intent, 0); + + String foundPackageName = null; + for (ResolveInfo r : resInfos) { + if (TextUtils.equals(r.activityInfo.packageName, FDROID_PACKAGE_NAME) + || TextUtils.equals(r.activityInfo.packageName, PLAY_PACKAGE_NAME)) { + foundPackageName = r.activityInfo.packageName; + break; + } + } + + if (foundPackageName == null) { + intent.setData(Uri.parse(FDROID_URI)); + } else { + intent.setPackage(foundPackageName); + } + return intent; + } + + @Override + public Intent getStartIntent(Context context) { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(PACKAGE_NAME, COMPONENT_NAME)); + + return intent; + } + + public static boolean isPortOpen(final String ip, final int port, final int timeout) { + try { + Socket socket = new Socket(); + socket.connect(new InetSocketAddress(ip, port), timeout); + socket.close(); + return true; + } + + catch(ConnectException ce){ + ce.printStackTrace(); + return false; + } + + catch (Exception ex) { + ex.printStackTrace(); + return false; + } + } + + + @Override + public String getName() { + return PACKAGE_NAME; + } + +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/proxy/SetFromMap.java b/libnetcipher/src/info/guardianproject/netcipher/proxy/SetFromMap.java new file mode 100644 index 00000000..10abbbcc --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/proxy/SetFromMap.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher.proxy; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +class SetFromMap extends AbstractSet + implements Serializable { + private static final long serialVersionUID = 2454657854757543876L; + // Must be named as is, to pass serialization compatibility test. + private final Map m; + private transient Set backingSet; + SetFromMap(final Map map) { + m = map; + backingSet = map.keySet(); + } + @Override public boolean equals(Object object) { + return backingSet.equals(object); + } + @Override public int hashCode() { + return backingSet.hashCode(); + } + @Override public boolean add(E object) { + return m.put(object, Boolean.TRUE) == null; + } + @Override public void clear() { + m.clear(); + } + @Override public String toString() { + return backingSet.toString(); + } + @Override public boolean contains(Object object) { + return backingSet.contains(object); + } + @Override public boolean containsAll(Collection collection) { + return backingSet.containsAll(collection); + } + @Override public boolean isEmpty() { + return m.isEmpty(); + } + @Override public boolean remove(Object object) { + return m.remove(object) != null; + } + @Override public boolean retainAll(Collection collection) { + return backingSet.retainAll(collection); + } + @Override public Object[] toArray() { + return backingSet.toArray(); + } + @Override + public T[] toArray(T[] contents) { + return backingSet.toArray(contents); + } + @Override public Iterator iterator() { + return backingSet.iterator(); + } + @Override public int size() { + return m.size(); + } + @SuppressWarnings("unchecked") + private void readObject(ObjectInputStream stream) + throws IOException, ClassNotFoundException { + stream.defaultReadObject(); + backingSet = m.keySet(); + } +} \ No newline at end of file diff --git a/libnetcipher/src/info/guardianproject/netcipher/proxy/SignatureUtils.java b/libnetcipher/src/info/guardianproject/netcipher/proxy/SignatureUtils.java new file mode 100644 index 00000000..19a897f1 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/proxy/SignatureUtils.java @@ -0,0 +1,476 @@ +/*** + Copyright (c) 2014 CommonsWare, LLC + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package info.guardianproject.netcipher.proxy; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.Signature; +import android.util.Log; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +public class SignatureUtils { + public static String getOwnSignatureHash(Context ctxt) + throws + NameNotFoundException, + NoSuchAlgorithmException { + return(getSignatureHash(ctxt, ctxt.getPackageName())); + } + + public static String getSignatureHash(Context ctxt, String packageName) + throws + NameNotFoundException, + NoSuchAlgorithmException { + MessageDigest md=MessageDigest.getInstance("SHA-256"); + Signature sig= + ctxt.getPackageManager() + .getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures[0]; + + return(toHexStringWithColons(md.digest(sig.toByteArray()))); + } + + // based on https://stackoverflow.com/a/2197650/115145 + + public static String toHexStringWithColons(byte[] bytes) { + char[] hexArray= + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', + 'C', 'D', 'E', 'F' }; + char[] hexChars=new char[(bytes.length * 3) - 1]; + int v; + + for (int j=0; j < bytes.length; j++) { + v=bytes[j] & 0xFF; + hexChars[j * 3]=hexArray[v / 16]; + hexChars[j * 3 + 1]=hexArray[v % 16]; + + if (j < bytes.length - 1) { + hexChars[j * 3 + 2]=':'; + } + } + + return new String(hexChars); + } + + /** + * Confirms that the broadcast receiver for a given Intent + * has the desired signature hash. + * + * If you know the package name of the receiver, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has the proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some receiver whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * receiver was found that matches the Intent. If failIfHack + * is false, this means that no receiver was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching receiver + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching receiver, so the "broadcast" will only go to this + * one component. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to broadcast + * @param sigHash the signature hash of the app that you expect + * to handle this broadcast + * @param failIfHack true if you want a SecurityException if + * a matching receiver is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching receiver with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target receiver added to the Intent + */ + public static Intent validateBroadcastIntent(Context ctxt, + Intent toValidate, + String sigHash, + boolean failIfHack) { + ArrayList sigHashes=new ArrayList(); + + sigHashes.add(sigHash); + + return(validateBroadcastIntent(ctxt, toValidate, sigHashes, + failIfHack)); + } + + /** + * Confirms that the broadcast receiver for a given Intent + * has a desired signature hash. + * + * If you know the package name of the receiver, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has a proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some receiver whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * receiver was found that matches the Intent. If failIfHack + * is false, this means that no receiver was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching receiver + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching receiver, so the "broadcast" will only go to this + * one component. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to broadcast + * @param sigHashes the possible signature hashes of the app + * that you expect to handle this broadcast + * @param failIfHack true if you want a SecurityException if + * a matching receiver is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching receiver with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target receiver added to the Intent + */ + public static Intent validateBroadcastIntent(Context ctxt, + Intent toValidate, + List sigHashes, + boolean failIfHack) { + PackageManager pm=ctxt.getPackageManager(); + Intent result=null; + List receivers= + pm.queryBroadcastReceivers(toValidate, 0); + + if (receivers!=null) { + for (ResolveInfo info : receivers) { + try { + if (sigHashes.contains(getSignatureHash(ctxt, + info.activityInfo.packageName))) { + ComponentName cn= + new ComponentName(info.activityInfo.packageName, + info.activityInfo.name); + + result=new Intent(toValidate).setComponent(cn); + break; + } + else if (failIfHack) { + throw new SecurityException( + "Package has signature hash mismatch: "+ + info.activityInfo.packageName); + } + } + catch (NoSuchAlgorithmException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + catch (NameNotFoundException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + } + } + + return(result); + } + + /** + * Confirms that the activity for a given Intent has the + * desired signature hash. + * + * If you know the package name of the activity, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has the proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some activity whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * activity was found that matches the Intent. If failIfHack + * is false, this means that no activity was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching activity + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching activity, so a call to startActivity() for this + * Intent is guaranteed to go to this specific activity. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to use with + * startActivity() + * @param sigHash the signature hash of the app that you expect + * to handle this activity + * @param failIfHack true if you want a SecurityException if + * a matching activity is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching activity with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target activity added to the Intent + */ + public static Intent validateActivityIntent(Context ctxt, + Intent toValidate, + String sigHash, + boolean failIfHack) { + ArrayList sigHashes=new ArrayList(); + + sigHashes.add(sigHash); + + return(validateActivityIntent(ctxt, toValidate, sigHashes, + failIfHack)); + } + + /** + * Confirms that the activity for a given Intent has the + * desired signature hash. + * + * If you know the package name of the activity, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has the proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some activity whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * activity was found that matches the Intent. If failIfHack + * is false, this means that no activity was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching activity + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching activity, so a call to startActivity() for this + * Intent is guaranteed to go to this specific activity. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to use with + * startActivity() + * @param sigHashes the signature hashes of the app that you expect + * to handle this activity + * @param failIfHack true if you want a SecurityException if + * a matching activity is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching activity with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target activity added to the Intent + */ + public static Intent validateActivityIntent(Context ctxt, + Intent toValidate, + List sigHashes, + boolean failIfHack) { + PackageManager pm=ctxt.getPackageManager(); + Intent result=null; + List activities= + pm.queryIntentActivities(toValidate, 0); + + if (activities!=null) { + for (ResolveInfo info : activities) { + try { + if (sigHashes.contains(getSignatureHash(ctxt, + info.activityInfo.packageName))) { + ComponentName cn= + new ComponentName(info.activityInfo.packageName, + info.activityInfo.name); + + result=new Intent(toValidate).setComponent(cn); + break; + } + else if (failIfHack) { + throw new SecurityException( + "Package has signature hash mismatch: "+ + info.activityInfo.packageName); + } + } + catch (NoSuchAlgorithmException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + catch (NameNotFoundException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + } + } + + return(result); + } + + /** + * Confirms that the service for a given Intent has the + * desired signature hash. + * + * If you know the package name of the service, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has the proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some service whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * service was found that matches the Intent. If failIfHack + * is false, this means that no service was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching service + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching service, so a call to startService() or + * bindService() for this Intent is guaranteed to go to this + * specific service. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to use with + * startService() or bindService() + * @param sigHash the signature hash of the app that you expect + * to handle this service + * @param failIfHack true if you want a SecurityException if + * a matching service is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching service with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target service added to the Intent + */ + public static Intent validateServiceIntent(Context ctxt, + Intent toValidate, + String sigHash, + boolean failIfHack) { + ArrayList sigHashes=new ArrayList(); + + sigHashes.add(sigHash); + + return(validateServiceIntent(ctxt, toValidate, sigHashes, + failIfHack)); + } + + /** + * Confirms that the service for a given Intent has the + * desired signature hash. + * + * If you know the package name of the service, call + * setPackage() on the Intent before passing into this method. + * That will validate whether the package is installed and whether + * it has the proper signature hash. You can distinguish between + * these cases by passing true for the failIfHack parameter. + * + * In general, there are three possible outcomes of calling + * this method: + * + * 1. You get a SecurityException, because failIfHack is true, + * and we found some service whose app does not match the + * desired hash. The user may have installed a repackaged + * version of this app that is signed by the wrong key. + * + * 2. You get null. If failIfHack is true, this means that no + * service was found that matches the Intent. If failIfHack + * is false, this means that no service was found that matches + * the Intent and has a valid matching signature. + * + * 3. You get an Intent. This means we found a matching service + * that has a matching signature. The Intent will be a copy of + * the passed-in Intent, with the component name set to the + * matching service, so a call to startService() or + * bindService() for this Intent is guaranteed to go to this + * specific service. + * + * @param ctxt any Context will do; the value is not retained + * @param toValidate the Intent that you intend to use with + * startService() or bindService() + * @param sigHashes the signature hash of the app that you expect + * to handle this service + * @param failIfHack true if you want a SecurityException if + * a matching service is found but it has + * the wrong signature hash, false otherwise + * @return null if there is no matching service with the correct + * hash, or a copy of the toValidate parameter with the full component + * name of the target service added to the Intent + */ + public static Intent validateServiceIntent(Context ctxt, + Intent toValidate, + List sigHashes, + boolean failIfHack) { + PackageManager pm=ctxt.getPackageManager(); + Intent result=null; + List services= + pm.queryIntentServices(toValidate, 0); + + if (services!=null) { + for (ResolveInfo info : services) { + try { + if (sigHashes.contains(getSignatureHash(ctxt, + info.serviceInfo.packageName))) { + ComponentName cn= + new ComponentName(info.serviceInfo.packageName, + info.serviceInfo.name); + + result=new Intent(toValidate).setComponent(cn); + break; + } + else if (failIfHack) { + throw new SecurityException( + "Package has signature hash mismatch: "+ + info.activityInfo.packageName); + } + } + catch (NoSuchAlgorithmException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + catch (NameNotFoundException e) { + Log.w("SignatureUtils", + "Exception when computing signature hash", e); + } + } + } + + return(result); + } +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/proxy/StatusCallback.java b/libnetcipher/src/info/guardianproject/netcipher/proxy/StatusCallback.java new file mode 100644 index 00000000..c267fdd1 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/proxy/StatusCallback.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2016 CommonsWare, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher.proxy; + +import android.content.Intent; + +/** + * Callback interface used for reporting Orbot status + */ +public interface StatusCallback { + /** + * Called when Orbot is operational + * + * @param statusIntent an Intent containing information about + * Orbot, including proxy ports + */ + void onEnabled(Intent statusIntent); + + /** + * Called when Orbot reports that it is starting up + */ + void onStarting(); + + /** + * Called when Orbot reports that it is shutting down + */ + void onStopping(); + + /** + * Called when Orbot reports that it is no longer running + */ + void onDisabled(); + + /** + * Called if our attempt to get a status from Orbot failed + * after a defined period of time. See statusTimeout() on + * OrbotInitializer. + */ + void onStatusTimeout(); + + /** + * Called if Orbot is not yet installed. Usually, you handle + * this by checking the return value from init() on OrbotInitializer + * or calling isInstalled() on OrbotInitializer. However, if + * you have need for it, if a callback is registered before + * an init() call determines that Orbot is not installed, your + * callback will be called with onNotYetInstalled(). + */ + void onNotYetInstalled(); +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/proxy/TorServiceUtils.java b/libnetcipher/src/info/guardianproject/netcipher/proxy/TorServiceUtils.java new file mode 100644 index 00000000..2a409876 --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/proxy/TorServiceUtils.java @@ -0,0 +1,246 @@ +/* + * Copyright 2009-2016 Nathan Freitas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher.proxy; + +import android.content.Context; +import android.util.Log; + + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.URLEncoder; +import java.util.StringTokenizer; + +public class TorServiceUtils { + + private final static String TAG = "TorUtils"; + // various console cmds + public final static String SHELL_CMD_CHMOD = "chmod"; + public final static String SHELL_CMD_KILL = "kill -9"; + public final static String SHELL_CMD_RM = "rm"; + public final static String SHELL_CMD_PS = "ps"; + public final static String SHELL_CMD_PIDOF = "pidof"; + + public final static String CHMOD_EXE_VALUE = "700"; + + public static boolean isRootPossible() + { + + StringBuilder log = new StringBuilder(); + + try { + + // Check if Superuser.apk exists + File fileSU = new File("/system/app/Superuser.apk"); + if (fileSU.exists()) + return true; + + fileSU = new File("/system/app/superuser.apk"); + if (fileSU.exists()) + return true; + + fileSU = new File("/system/bin/su"); + if (fileSU.exists()) + { + String[] cmd = { + "su" + }; + int exitCode = TorServiceUtils.doShellCommand(cmd, log, false, true); + if (exitCode != 0) + return false; + else + return true; + } + + // Check for 'su' binary + String[] cmd = { + "which su" + }; + int exitCode = TorServiceUtils.doShellCommand(cmd, log, false, true); + + if (exitCode == 0) { + Log.d(TAG, "root exists, but not sure about permissions"); + return true; + + } + + } catch (IOException e) { + // this means that there is no root to be had (normally) so we won't + // log anything + Log.e(TAG, "Error checking for root access", e); + + } catch (Exception e) { + Log.e(TAG, "Error checking for root access", e); + // this means that there is no root to be had (normally) + } + + Log.e(TAG, "Could not acquire root permissions"); + + return false; + } + + public static int findProcessId(Context context) { + String dataPath = context.getFilesDir().getParentFile().getParentFile().getAbsolutePath(); + String command = dataPath + "/" + OrbotHelper.ORBOT_PACKAGE_NAME + "/app_bin/tor"; + int procId = -1; + + try { + procId = findProcessIdWithPidOf(command); + + if (procId == -1) + procId = findProcessIdWithPS(command); + } catch (Exception e) { + try { + procId = findProcessIdWithPS(command); + } catch (Exception e2) { + Log.e(TAG, "Unable to get proc id for command: " + URLEncoder.encode(command), e2); + } + } + + return procId; + } + + // use 'pidof' command + public static int findProcessIdWithPidOf(String command) throws Exception + { + + int procId = -1; + + Runtime r = Runtime.getRuntime(); + + Process procPs = null; + + String baseName = new File(command).getName(); + // fix contributed my mikos on 2010.12.10 + procPs = r.exec(new String[] { + SHELL_CMD_PIDOF, baseName + }); + // procPs = r.exec(SHELL_CMD_PIDOF); + + BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream())); + String line = null; + + while ((line = reader.readLine()) != null) + { + + try + { + // this line should just be the process id + procId = Integer.parseInt(line.trim()); + break; + } catch (NumberFormatException e) + { + Log.e("TorServiceUtils", "unable to parse process pid: " + line, e); + } + } + + return procId; + + } + + // use 'ps' command + public static int findProcessIdWithPS(String command) throws Exception + { + + int procId = -1; + + Runtime r = Runtime.getRuntime(); + + Process procPs = null; + + procPs = r.exec(SHELL_CMD_PS); + + BufferedReader reader = new BufferedReader(new InputStreamReader(procPs.getInputStream())); + String line = null; + + while ((line = reader.readLine()) != null) + { + if (line.indexOf(' ' + command) != -1) + { + + StringTokenizer st = new StringTokenizer(line, " "); + st.nextToken(); // proc owner + + procId = Integer.parseInt(st.nextToken().trim()); + + break; + } + } + + return procId; + + } + + public static int doShellCommand(String[] cmds, StringBuilder log, boolean runAsRoot, + boolean waitFor) throws Exception + { + + Process proc = null; + int exitCode = -1; + + if (runAsRoot) + proc = Runtime.getRuntime().exec("su"); + else + proc = Runtime.getRuntime().exec("sh"); + + OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream()); + + for (int i = 0; i < cmds.length; i++) + { + // TorService.logMessage("executing shell cmd: " + cmds[i] + + // "; runAsRoot=" + runAsRoot + ";waitFor=" + waitFor); + + out.write(cmds[i]); + out.write("\n"); + } + + out.flush(); + out.write("exit\n"); + out.flush(); + + if (waitFor) + { + + final char buf[] = new char[10]; + + // Consume the "stdout" + InputStreamReader reader = new InputStreamReader(proc.getInputStream()); + int read = 0; + while ((read = reader.read(buf)) != -1) { + if (log != null) + log.append(buf, 0, read); + } + + // Consume the "stderr" + reader = new InputStreamReader(proc.getErrorStream()); + read = 0; + while ((read = reader.read(buf)) != -1) { + if (log != null) + log.append(buf, 0, read); + } + + exitCode = proc.waitFor(); + + } + + return exitCode; + + } +} diff --git a/libnetcipher/src/info/guardianproject/netcipher/web/WebkitProxy.java b/libnetcipher/src/info/guardianproject/netcipher/web/WebkitProxy.java new file mode 100644 index 00000000..369a61af --- /dev/null +++ b/libnetcipher/src/info/guardianproject/netcipher/web/WebkitProxy.java @@ -0,0 +1,833 @@ +/* + * Copyright 2015 Anthony Restaino + * Copyright 2012-2016 Nathan Freitas + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package info.guardianproject.netcipher.web; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.Socket; + +import org.apache.http.HttpHost; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Proxy; +import android.net.Uri; +import android.os.Build; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Log; +import android.webkit.WebView; + +public class WebkitProxy { + + private final static String DEFAULT_HOST = "localhost";//"127.0.0.1"; + private final static int DEFAULT_PORT = 8118; + private final static int DEFAULT_SOCKS_PORT = 9050; + + private final static int REQUEST_CODE = 0; + + private final static String TAG = "OrbotHelpher"; + + public static boolean setProxy(String appClass, Context ctx, WebView wView, String host, int port) throws Exception + { + + setSystemProperties(host, port); + + boolean worked = false; + + if (Build.VERSION.SDK_INT < 13) + { +// worked = setWebkitProxyGingerbread(ctx, host, port); + setProxyUpToHC(wView, host, port); + } + else if (Build.VERSION.SDK_INT < 19) + { + worked = setWebkitProxyICS(ctx, host, port); + } + else if (Build.VERSION.SDK_INT < 20) + { + worked = setKitKatProxy(appClass, ctx, host, port); + + if (!worked) //some kitkat's still use ICS browser component (like Cyanogen 11) + worked = setWebkitProxyICS(ctx, host, port); + + } + else if (Build.VERSION.SDK_INT >= 21) + { + worked = setWebkitProxyLollipop(ctx, host, port); + + } + + return worked; + } + + private static void setSystemProperties(String host, int port) + { + + System.setProperty("proxyHost", host); + System.setProperty("proxyPort", Integer.toString(port)); + + System.setProperty("http.proxyHost", host); + System.setProperty("http.proxyPort", Integer.toString(port)); + + System.setProperty("https.proxyHost", host); + System.setProperty("https.proxyPort", Integer.toString(port)); + + + System.setProperty("socks.proxyHost", host); + System.setProperty("socks.proxyPort", Integer.toString(DEFAULT_SOCKS_PORT)); + + System.setProperty("socksProxyHost", host); + System.setProperty("socksProxyPort", Integer.toString(DEFAULT_SOCKS_PORT)); + + + /* + ProxySelector pSelect = new ProxySelector(); + pSelect.addProxy(Proxy.Type.HTTP, host, port); + ProxySelector.setDefault(pSelect); + */ + /* + System.setProperty("http_proxy", "http://" + host + ":" + port); + System.setProperty("proxy-server", "http://" + host + ":" + port); + System.setProperty("host-resolver-rules","MAP * 0.0.0.0 , EXCLUDE myproxy"); + + System.getProperty("networkaddress.cache.ttl", "-1"); + */ + + } + + private static void resetSystemProperties() + { + + System.setProperty("proxyHost", ""); + System.setProperty("proxyPort", ""); + + System.setProperty("http.proxyHost", ""); + System.setProperty("http.proxyPort", ""); + + System.setProperty("https.proxyHost", ""); + System.setProperty("https.proxyPort", ""); + + + System.setProperty("socks.proxyHost", ""); + System.setProperty("socks.proxyPort", Integer.toString(DEFAULT_SOCKS_PORT)); + + System.setProperty("socksProxyHost", ""); + System.setProperty("socksProxyPort", Integer.toString(DEFAULT_SOCKS_PORT)); + + } + + /** + * Override WebKit Proxy settings + * + * @param ctx Android ApplicationContext + * @param host + * @param port + * @return true if Proxy was successfully set + */ + private static boolean setWebkitProxyGingerbread(Context ctx, String host, int port) + throws Exception + { + + boolean ret = false; + + Object requestQueueObject = getRequestQueue(ctx); + if (requestQueueObject != null) { + // Create Proxy config object and set it into request Q + HttpHost httpHost = new HttpHost(host, port, "http"); + setDeclaredField(requestQueueObject, "mProxyHost", httpHost); + return true; + } + return false; + + } + + +/** + * Set Proxy for Android 3.2 and below. + */ +@SuppressWarnings("all") +private static boolean setProxyUpToHC(WebView webview, String host, int port) { + Log.d(TAG, "Setting proxy with <= 3.2 API."); + + HttpHost proxyServer = new HttpHost(host, port); + // Getting network + Class networkClass = null; + Object network = null; + try { + networkClass = Class.forName("android.webkit.Network"); + if (networkClass == null) { + Log.e(TAG, "failed to get class for android.webkit.Network"); + return false; + } + Method getInstanceMethod = networkClass.getMethod("getInstance", Context.class); + if (getInstanceMethod == null) { + Log.e(TAG, "failed to get getInstance method"); + } + network = getInstanceMethod.invoke(networkClass, new Object[]{webview.getContext()}); + } catch (Exception ex) { + Log.e(TAG, "error getting network: " + ex); + return false; + } + if (network == null) { + Log.e(TAG, "error getting network: network is null"); + return false; + } + Object requestQueue = null; + try { + Field requestQueueField = networkClass + .getDeclaredField("mRequestQueue"); + requestQueue = getFieldValueSafely(requestQueueField, network); + } catch (Exception ex) { + Log.e(TAG, "error getting field value"); + return false; + } + if (requestQueue == null) { + Log.e(TAG, "Request queue is null"); + return false; + } + Field proxyHostField = null; + try { + Class requestQueueClass = Class.forName("android.net.http.RequestQueue"); + proxyHostField = requestQueueClass + .getDeclaredField("mProxyHost"); + } catch (Exception ex) { + Log.e(TAG, "error getting proxy host field"); + return false; + } + + boolean temp = proxyHostField.isAccessible(); + try { + proxyHostField.setAccessible(true); + proxyHostField.set(requestQueue, proxyServer); + } catch (Exception ex) { + Log.e(TAG, "error setting proxy host"); + } finally { + proxyHostField.setAccessible(temp); + } + + Log.d(TAG, "Setting proxy with <= 3.2 API successful!"); + return true; +} + + +private static Object getFieldValueSafely(Field field, Object classInstance) throws IllegalArgumentException, IllegalAccessException { + boolean oldAccessibleValue = field.isAccessible(); + field.setAccessible(true); + Object result = field.get(classInstance); + field.setAccessible(oldAccessibleValue); + return result; +} + + private static boolean setWebkitProxyICS(Context ctx, String host, int port) + { + + // PSIPHON: added support for Android 4.x WebView proxy + try + { + Class webViewCoreClass = Class.forName("android.webkit.WebViewCore"); + + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (webViewCoreClass != null && proxyPropertiesClass != null) + { + Method m = webViewCoreClass.getDeclaredMethod("sendStaticMessage", Integer.TYPE, + Object.class); + Constructor c = proxyPropertiesClass.getConstructor(String.class, Integer.TYPE, + String.class); + + if (m != null && c != null) + { + m.setAccessible(true); + c.setAccessible(true); + Object properties = c.newInstance(host, port, null); + + // android.webkit.WebViewCore.EventHub.PROXY_CHANGED = 193; + m.invoke(null, 193, properties); + + + return true; + } + + + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.net.ProxyProperties: " + + e.toString()); + } catch (Error e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.webkit.Network: " + + e.toString()); + } + + return false; + + } + + @TargetApi(19) + public static boolean resetKitKatProxy(String appClass, Context appContext) { + + return setKitKatProxy(appClass, appContext,null,0); + } + + @TargetApi(19) + private static boolean setKitKatProxy(String appClass, Context appContext, String host, int port) { + //Context appContext = webView.getContext().getApplicationContext(); + + if (host != null) + { + System.setProperty("http.proxyHost", host); + System.setProperty("http.proxyPort", Integer.toString(port)); + System.setProperty("https.proxyHost", host); + System.setProperty("https.proxyPort", Integer.toString(port)); + } + + try { + Class applictionCls = Class.forName(appClass); + Field loadedApkField = applictionCls.getField("mLoadedApk"); + loadedApkField.setAccessible(true); + Object loadedApk = loadedApkField.get(appContext); + Class loadedApkCls = Class.forName("android.app.LoadedApk"); + Field receiversField = loadedApkCls.getDeclaredField("mReceivers"); + receiversField.setAccessible(true); + ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk); + for (Object receiverMap : receivers.values()) { + for (Object rec : ((ArrayMap) receiverMap).keySet()) { + Class clazz = rec.getClass(); + if (clazz.getName().contains("ProxyChangeListener")) { + Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class); + Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); + + if (host != null) + { + /*********** optional, may be need in future *************/ + final String CLASS_NAME = "android.net.ProxyProperties"; + Class cls = Class.forName(CLASS_NAME); + Constructor constructor = cls.getConstructor(String.class, Integer.TYPE, String.class); + constructor.setAccessible(true); + Object proxyProperties = constructor.newInstance(host, port, null); + intent.putExtra("proxy", (Parcelable) proxyProperties); + /*********** optional, may be need in future *************/ + } + + onReceiveMethod.invoke(rec, appContext, intent); + } + } + } + return true; + } catch (ClassNotFoundException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (NoSuchFieldException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (IllegalAccessException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (IllegalArgumentException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (NoSuchMethodException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (InvocationTargetException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } catch (InstantiationException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + String exceptionAsString = sw.toString(); + Log.v(TAG, e.getMessage()); + Log.v(TAG, exceptionAsString); + } + return false; } + + @TargetApi(21) + public static boolean resetLollipopProxy(String appClass, Context appContext) { + + return setWebkitProxyLollipop(appContext,null,0); + } + + // http://stackanswers.com/questions/25272393/android-webview-set-proxy-programmatically-on-android-l + @TargetApi(21) // for android.util.ArrayMap methods + @SuppressWarnings("rawtypes") + private static boolean setWebkitProxyLollipop(Context appContext, String host, int port) + { + System.setProperty("http.proxyHost", host); + System.setProperty("http.proxyPort", Integer.toString(port)); + System.setProperty("https.proxyHost", host); + System.setProperty("https.proxyPort", Integer.toString(port)); + try { + Class applictionClass = Class.forName("android.app.Application"); + Field mLoadedApkField = applictionClass.getDeclaredField("mLoadedApk"); + mLoadedApkField.setAccessible(true); + Object mloadedApk = mLoadedApkField.get(appContext); + Class loadedApkClass = Class.forName("android.app.LoadedApk"); + Field mReceiversField = loadedApkClass.getDeclaredField("mReceivers"); + mReceiversField.setAccessible(true); + ArrayMap receivers = (ArrayMap) mReceiversField.get(mloadedApk); + for (Object receiverMap : receivers.values()) + { + for (Object receiver : ((ArrayMap) receiverMap).keySet()) + { + Class clazz = receiver.getClass(); + if (clazz.getName().contains("ProxyChangeListener")) + { + Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class); + Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); + onReceiveMethod.invoke(receiver, appContext, intent); + } + } + } + return true; + } + catch (ClassNotFoundException e) + { + Log.d("ProxySettings","Exception setting WebKit proxy on Lollipop through ProxyChangeListener: " + e.toString()); + } + catch (NoSuchFieldException e) + { + Log.d("ProxySettings","Exception setting WebKit proxy on Lollipop through ProxyChangeListener: " + e.toString()); + } + catch (IllegalAccessException e) + { + Log.d("ProxySettings","Exception setting WebKit proxy on Lollipop through ProxyChangeListener: " + e.toString()); + } + catch (NoSuchMethodException e) + { + Log.d("ProxySettings","Exception setting WebKit proxy on Lollipop through ProxyChangeListener: " + e.toString()); + } + catch (InvocationTargetException e) + { + Log.d("ProxySettings","Exception setting WebKit proxy on Lollipop through ProxyChangeListener: " + e.toString()); + } + return false; + } + + private static boolean sendProxyChangedIntent(Context ctx, String host, int port) + { + + try + { + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (proxyPropertiesClass != null) + { + Constructor c = proxyPropertiesClass.getConstructor(String.class, Integer.TYPE, + String.class); + + if (c != null) + { + c.setAccessible(true); + Object properties = c.newInstance(host, port, null); + + Intent intent = new Intent(android.net.Proxy.PROXY_CHANGE_ACTION); + intent.putExtra("proxy",(Parcelable)properties); + ctx.sendBroadcast(intent); + + } + + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception sending Intent ",e); + } catch (Error e) + { + Log.e("ProxySettings", + "Exception sending Intent ",e); + } + + return false; + + } + + /** + private static boolean setKitKatProxy0(Context ctx, String host, int port) + { + + try + { + Class cmClass = Class.forName("android.net.ConnectivityManager"); + + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (cmClass != null && proxyPropertiesClass != null) + { + Constructor c = proxyPropertiesClass.getConstructor(String.class, Integer.TYPE, + String.class); + + if (c != null) + { + c.setAccessible(true); + + Object proxyProps = c.newInstance(host, port, null); + ConnectivityManager cm = + (ConnectivityManager)ctx.getSystemService(Context.CONNECTIVITY_SERVICE); + + Method mSetGlobalProxy = cmClass.getDeclaredMethod("setGlobalProxy", proxyPropertiesClass); + + mSetGlobalProxy.invoke(cm, proxyProps); + + return true; + } + + } + } catch (Exception e) + { + Log.e("ProxySettings", + "ConnectivityManager.setGlobalProxy ",e); + } + + return false; + + } + */ + //CommandLine.initFromFile(COMMAND_LINE_FILE); + + /** + private static boolean setKitKatProxy2 (Context ctx, String host, int port) + { + + String commandLinePath = "/data/local/tmp/orweb.conf"; + try + { + Class webViewCoreClass = Class.forName("org.chromium.content.common.CommandLine"); + + if (webViewCoreClass != null) + { + for (Method method : webViewCoreClass.getDeclaredMethods()) + { + Log.d("Orweb","Proxy methods: " + method.getName()); + } + + Method m = webViewCoreClass.getDeclaredMethod("initFromFile", + String.class); + + if (m != null) + { + m.setAccessible(true); + m.invoke(null, commandLinePath); + return true; + } + else + return false; + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.net.ProxyProperties: " + + e.toString()); + } catch (Error e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.webkit.Network: " + + e.toString()); + } + + return false; + } + + /** + private static boolean setKitKatProxy (Context ctx, String host, int port) + { + + try + { + Class webViewCoreClass = Class.forName("android.net.Proxy"); + + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (webViewCoreClass != null && proxyPropertiesClass != null) + { + for (Method method : webViewCoreClass.getDeclaredMethods()) + { + Log.d("Orweb","Proxy methods: " + method.getName()); + } + + Method m = webViewCoreClass.getDeclaredMethod("setHttpProxySystemProperty", + proxyPropertiesClass); + Constructor c = proxyPropertiesClass.getConstructor(String.class, Integer.TYPE, + String.class); + + if (m != null && c != null) + { + m.setAccessible(true); + c.setAccessible(true); + Object properties = c.newInstance(host, port, null); + + m.invoke(null, properties); + return true; + } + else + return false; + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.net.ProxyProperties: " + + e.toString()); + } catch (Error e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.webkit.Network: " + + e.toString()); + } + + return false; + } + + private static boolean resetProxyForKitKat () + { + + try + { + Class webViewCoreClass = Class.forName("android.net.Proxy"); + + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (webViewCoreClass != null && proxyPropertiesClass != null) + { + for (Method method : webViewCoreClass.getDeclaredMethods()) + { + Log.d("Orweb","Proxy methods: " + method.getName()); + } + + Method m = webViewCoreClass.getDeclaredMethod("setHttpProxySystemProperty", + proxyPropertiesClass); + + if (m != null) + { + m.setAccessible(true); + + m.invoke(null, null); + return true; + } + else + return false; + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.net.ProxyProperties: " + + e.toString()); + } catch (Error e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.webkit.Network: " + + e.toString()); + } + + return false; + }**/ + + public static void resetProxy(String appClass, Context ctx) throws Exception { + + resetSystemProperties(); + + if (Build.VERSION.SDK_INT < 14) + { + resetProxyForGingerBread(ctx); + } + else if (Build.VERSION.SDK_INT < 19) + { + resetProxyForICS(); + } + else + { + resetKitKatProxy(appClass, ctx); + } + + } + + private static void resetProxyForICS() throws Exception{ + try + { + Class webViewCoreClass = Class.forName("android.webkit.WebViewCore"); + Class proxyPropertiesClass = Class.forName("android.net.ProxyProperties"); + if (webViewCoreClass != null && proxyPropertiesClass != null) + { + Method m = webViewCoreClass.getDeclaredMethod("sendStaticMessage", Integer.TYPE, + Object.class); + + if (m != null) + { + m.setAccessible(true); + + // android.webkit.WebViewCore.EventHub.PROXY_CHANGED = 193; + m.invoke(null, 193, null); + } + } + } catch (Exception e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.net.ProxyProperties: " + + e.toString()); + throw e; + } catch (Error e) + { + Log.e("ProxySettings", + "Exception setting WebKit proxy through android.webkit.Network: " + + e.toString()); + throw e; + } + } + + private static void resetProxyForGingerBread(Context ctx) throws Exception { + Object requestQueueObject = getRequestQueue(ctx); + if (requestQueueObject != null) { + setDeclaredField(requestQueueObject, "mProxyHost", null); + } + } + + public static Object getRequestQueue(Context ctx) throws Exception { + Object ret = null; + Class networkClass = Class.forName("android.webkit.Network"); + if (networkClass != null) { + Object networkObj = invokeMethod(networkClass, "getInstance", new Object[] { + ctx + }, Context.class); + if (networkObj != null) { + ret = getDeclaredField(networkObj, "mRequestQueue"); + } + } + return ret; + } + + private static Object getDeclaredField(Object obj, String name) + throws SecurityException, NoSuchFieldException, + IllegalArgumentException, IllegalAccessException { + Field f = obj.getClass().getDeclaredField(name); + f.setAccessible(true); + Object out = f.get(obj); + // System.out.println(obj.getClass().getName() + "." + name + " = "+ + // out); + return out; + } + + private static void setDeclaredField(Object obj, String name, Object value) + throws SecurityException, NoSuchFieldException, + IllegalArgumentException, IllegalAccessException { + Field f = obj.getClass().getDeclaredField(name); + f.setAccessible(true); + f.set(obj, value); + } + + private static Object invokeMethod(Object object, String methodName, Object[] params, + Class... types) throws Exception { + Object out = null; + Class c = object instanceof Class ? (Class) object : object.getClass(); + if (types != null) { + Method method = c.getMethod(methodName, types); + out = method.invoke(object, params); + } else { + Method method = c.getMethod(methodName); + out = method.invoke(object); + } + // System.out.println(object.getClass().getName() + "." + methodName + + // "() = "+ out); + return out; + } + + public static Socket getSocket(Context context, String proxyHost, int proxyPort) + throws IOException + { + Socket sock = new Socket(); + + sock.connect(new InetSocketAddress(proxyHost, proxyPort), 10000); + + return sock; + } + + public static Socket getSocket(Context context) throws IOException + { + return getSocket(context, DEFAULT_HOST, DEFAULT_SOCKS_PORT); + + } + + public static AlertDialog initOrbot(Activity activity, + CharSequence stringTitle, + CharSequence stringMessage, + CharSequence stringButtonYes, + CharSequence stringButtonNo, + CharSequence stringDesiredBarcodeFormats) { + Intent intentScan = new Intent("org.torproject.android.START_TOR"); + intentScan.addCategory(Intent.CATEGORY_DEFAULT); + + try { + activity.startActivityForResult(intentScan, REQUEST_CODE); + return null; + } catch (ActivityNotFoundException e) { + return showDownloadDialog(activity, stringTitle, stringMessage, stringButtonYes, + stringButtonNo); + } + } + + private static AlertDialog showDownloadDialog(final Activity activity, + CharSequence stringTitle, + CharSequence stringMessage, + CharSequence stringButtonYes, + CharSequence stringButtonNo) { + AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity); + downloadDialog.setTitle(stringTitle); + downloadDialog.setMessage(stringMessage); + downloadDialog.setPositiveButton(stringButtonYes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialogInterface, int i) { + Uri uri = Uri.parse("market://search?q=pname:org.torproject.android"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + activity.startActivity(intent); + } + }); + downloadDialog.setNegativeButton(stringButtonNo, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialogInterface, int i) { + } + }); + return downloadDialog.show(); + } + + + +}