summaryrefslogtreecommitdiff
path: root/bitmask_android/src/main/java/se
diff options
context:
space:
mode:
Diffstat (limited to 'bitmask_android/src/main/java/se')
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/AboutActivity.java40
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/ConfigHelper.java194
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/ConfigurationWizard.java581
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/Dashboard.java479
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java94
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/EIP.java623
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/EipServiceFragment.java306
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/LeapHttpClient.java77
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java341
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/LogInDialog.java151
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/NewProviderDialog.java123
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/PRNGFixes.java338
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/Provider.java212
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderAPI.java875
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java56
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderDetailFragment.java115
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java114
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListContent.java112
-rw-r--r--bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListFragment.java234
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/CIDRIP.java58
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/ConfigParser.java569
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/LICENSE.txt24
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/LaunchVPN.java385
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/LogWindow.java340
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/NetworkSateReceiver.java86
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/OpenVPN.java250
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/OpenVPNThread.java130
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/OpenVpnManagementThread.java592
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/OpenVpnService.java504
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/ProfileManager.java220
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/ProxyDetection.java54
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/VPNLaunchHelper.java76
-rw-r--r--bitmask_android/src/main/java/se/leap/openvpn/VpnProfile.java758
33 files changed, 9111 insertions, 0 deletions
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/AboutActivity.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/AboutActivity.java
new file mode 100644
index 00000000..6d025422
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/AboutActivity.java
@@ -0,0 +1,40 @@
+package se.leap.bitmaskclient;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import se.leap.bitmaskclient.R;
+
+public class AboutActivity extends Activity {
+
+ final public static String TAG = "aboutFragment";
+ final public static int VIEWED = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.about);
+ TextView ver = (TextView) findViewById(R.id.version);
+
+ String version;
+ String name="Openvpn";
+ try {
+ PackageInfo packageinfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+ version = packageinfo.versionName;
+ name = getString(R.string.app);
+ } catch (NameNotFoundException e) {
+ version = "error fetching version";
+ }
+
+
+ ver.setText(getString(R.string.version_info,name,version));
+ setResult(VIEWED);
+ }
+
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
new file mode 100644
index 00000000..a8bd3b7a
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.io.InputStream;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Base64;
+
+/**
+ * Stores constants, and implements auxiliary methods used across all LEAP Android classes.
+ *
+ * @author parmegv
+ * @author MeanderingCode
+ *
+ */
+public class ConfigHelper {
+ private static KeyStore keystore_trusted;
+
+ final public static String NG_1024 =
+ "eeaf0ab9adb38dd69c33f80afa8fc5e86072618775ff3c0b9ea2314c9c256576d674df7496ea81d3383b4813d692c6e0e0d5d8e250b98be48e495c1d6089dad15dc7d7b46154d6b6ce8ef4ad69b15d4982559b297bcf1885c529f566660e57ec68edbc3c05726cc02fd4cbf4976eaa9afd5138fe8376435b9fc61d2fc0eb06e3";
+ final public static BigInteger G = new BigInteger("2");
+
+ public static boolean checkErroneousDownload(String downloaded_string) {
+ try {
+ if(new JSONObject(downloaded_string).has(ProviderAPI.ERRORS) || downloaded_string.isEmpty()) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch(JSONException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Treat the input as the MSB representation of a number,
+ * and lop off leading zero elements. For efficiency, the
+ * input is simply returned if no leading zeroes are found.
+ *
+ * @param in array to be trimmed
+ */
+ public static byte[] trim(byte[] in) {
+ if(in.length == 0 || in[0] != 0)
+ return in;
+
+ int len = in.length;
+ int i = 1;
+ while(in[i] == 0 && i < len)
+ ++i;
+ byte[] ret = new byte[len - i];
+ System.arraycopy(in, i, ret, 0, len - i);
+ return ret;
+ }
+
+ public static X509Certificate parseX509CertificateFromString(String certificate_string) {
+ java.security.cert.Certificate certificate = null;
+ CertificateFactory cf;
+ try {
+ cf = CertificateFactory.getInstance("X.509");
+
+ certificate_string = certificate_string.replaceFirst("-----BEGIN CERTIFICATE-----", "").replaceFirst("-----END CERTIFICATE-----", "").trim();
+ byte[] cert_bytes = Base64.decode(certificate_string, Base64.DEFAULT);
+ InputStream caInput = new ByteArrayInputStream(cert_bytes);
+ try {
+ certificate = cf.generateCertificate(caInput);
+ System.out.println("ca=" + ((X509Certificate) certificate).getSubjectDN());
+ } finally {
+ caInput.close();
+ }
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ return null;
+ }
+
+ return (X509Certificate) certificate;
+ }
+
+ protected static RSAPrivateKey parseRsaKeyFromString(String RsaKeyString) {
+ RSAPrivateKey key = null;
+ try {
+ KeyFactory kf = KeyFactory.getInstance("RSA", "BC");
+
+ RsaKeyString = RsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", "");
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec( Base64.decode(RsaKeyString, Base64.DEFAULT) );
+ key = (RSAPrivateKey) kf.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return null;
+ } catch (NoSuchProviderException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return null;
+ }
+
+ return key;
+ }
+
+ /**
+ * Adds a new X509 certificate given its input stream and its provider name
+ * @param provider used to store the certificate in the keystore
+ * @param inputStream from which X509 certificate must be generated.
+ */
+ public static void addTrustedCertificate(String provider, InputStream inputStream) {
+ CertificateFactory cf;
+ try {
+ cf = CertificateFactory.getInstance("X.509");
+ X509Certificate cert =
+ (X509Certificate)cf.generateCertificate(inputStream);
+ keystore_trusted.setCertificateEntry(provider, cert);
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Adds a new X509 certificate given in its string from and using its provider name
+ * @param provider used to store the certificate in the keystore
+ * @param certificate
+ */
+ public static void addTrustedCertificate(String provider, String certificate) {
+
+ try {
+ X509Certificate cert = ConfigHelper.parseX509CertificateFromString(certificate);
+ if(keystore_trusted == null) {
+ keystore_trusted = KeyStore.getInstance("BKS");
+ keystore_trusted.load(null);
+ }
+ keystore_trusted.setCertificateEntry(provider, cert);
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * @return class wide keystore
+ */
+ public static KeyStore getKeystore() {
+ return keystore_trusted;
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/ConfigurationWizard.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/ConfigurationWizard.java
new file mode 100644
index 00000000..f0aac40b
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/ConfigurationWizard.java
@@ -0,0 +1,581 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+
+
+
+
+
+import android.app.Activity;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.res.AssetManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Display;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View.MeasureSpec;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Iterator;
+import org.json.JSONException;
+import org.json.JSONObject;
+import se.leap.bitmaskclient.DownloadFailedDialog.DownloadFailedDialogInterface;
+import se.leap.bitmaskclient.NewProviderDialog.NewProviderDialogInterface;
+import se.leap.bitmaskclient.ProviderAPIResultReceiver.Receiver;
+import se.leap.bitmaskclient.ProviderDetailFragment.ProviderDetailFragmentInterface;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import se.leap.bitmaskclient.R;
+
+/**
+ * Activity that builds and shows the list of known available providers.
+ *
+ * It also allows the user to enter custom providers with a button.
+ *
+ * @author parmegv
+ *
+ */
+public class ConfigurationWizard extends Activity
+implements ProviderListFragment.Callbacks, NewProviderDialogInterface, ProviderDetailFragmentInterface, DownloadFailedDialogInterface, Receiver {
+
+ private ProgressBar mProgressBar;
+ private TextView progressbar_description;
+ private ProviderListFragment provider_list_fragment;
+ private Intent mConfigState = new Intent();
+
+ final public static String TAG = "se.leap.bitmaskclient.ConfigurationWizard";
+ final public static String TYPE_OF_CERTIFICATE = "type_of_certificate";
+ final public static String ANON_CERTIFICATE = "anon_certificate";
+ final public static String AUTHED_CERTIFICATE = "authed_certificate";
+
+ final protected static String PROVIDER_SET = "PROVIDER SET";
+ final protected static String SERVICES_RETRIEVED = "SERVICES RETRIEVED";
+
+ public ProviderAPIResultReceiver providerAPI_result_receiver;
+ private ProviderAPIBroadcastReceiver_Update providerAPI_broadcast_receiver_update;
+
+ private static SharedPreferences preferences;
+ private static boolean setting_up_provider = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ preferences = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE);
+
+ setContentView(R.layout.configuration_wizard_activity);
+ mProgressBar = (ProgressBar) findViewById(R.id.progressbar_configuration_wizard);
+ mProgressBar.setVisibility(ProgressBar.INVISIBLE);
+ progressbar_description = (TextView) findViewById(R.id.progressbar_description);
+ progressbar_description.setVisibility(TextView.INVISIBLE);
+ providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler());
+ providerAPI_result_receiver.setReceiver(this);
+ providerAPI_broadcast_receiver_update = new ProviderAPIBroadcastReceiver_Update();
+ IntentFilter update_intent_filter = new IntentFilter(ProviderAPI.UPDATE_PROGRESSBAR);
+ update_intent_filter.addCategory(Intent.CATEGORY_DEFAULT);
+ registerReceiver(providerAPI_broadcast_receiver_update, update_intent_filter);
+
+ loadPreseededProviders();
+
+ // Only create our fragments if we're not restoring a saved instance
+ if ( savedInstanceState == null ){
+ // TODO Some welcome screen?
+ // We will need better flow control when we have more Fragments (e.g. user auth)
+ provider_list_fragment = ProviderListFragment.newInstance();
+ Bundle arguments = new Bundle();
+ int configuration_wizard_request_code = getIntent().getIntExtra(Dashboard.REQUEST_CODE, -1);
+ if(configuration_wizard_request_code == Dashboard.SWITCH_PROVIDER) {
+ arguments.putBoolean(ProviderListFragment.SHOW_ALL_PROVIDERS, true);
+ }
+ provider_list_fragment.setArguments(arguments);
+
+ FragmentManager fragmentManager = getFragmentManager();
+ fragmentManager.beginTransaction()
+ .replace(R.id.configuration_wizard_layout, provider_list_fragment, ProviderListFragment.TAG)
+ .commit();
+ }
+
+ // TODO: If exposing deep links into your app, handle intents here.
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(providerAPI_broadcast_receiver_update);
+ }
+
+ public void refreshProviderList(int top_padding) {
+ ProviderListFragment new_provider_list_fragment = new ProviderListFragment();
+ Bundle top_padding_bundle = new Bundle();
+ top_padding_bundle.putInt(ProviderListFragment.TOP_PADDING, top_padding);
+ new_provider_list_fragment.setArguments(top_padding_bundle);
+
+ FragmentManager fragmentManager = getFragmentManager();
+ fragmentManager.beginTransaction()
+ .replace(R.id.configuration_wizard_layout, new_provider_list_fragment, ProviderListFragment.TAG)
+ .commit();
+ }
+
+ @Override
+ public void onReceiveResult(int resultCode, Bundle resultData) {
+ if(resultCode == ProviderAPI.PROVIDER_OK) {
+ mConfigState.setAction(PROVIDER_SET);
+
+ if (preferences.getBoolean(EIP.ALLOWED_ANON, false)){
+ mConfigState.putExtra(SERVICES_RETRIEVED, true);
+ downloadAnonCert();
+ } else {
+ mProgressBar.incrementProgressBy(1);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ progressbar_description.setVisibility(TextView.GONE);
+ setResult(RESULT_OK);
+ showProviderDetails(getCurrentFocus());
+ }
+ } else if(resultCode == ProviderAPI.PROVIDER_NOK) {
+ //refreshProviderList(0);
+ String reason_to_fail = resultData.getString(ProviderAPI.ERRORS);
+ showDownloadFailedDialog(getCurrentFocus(), reason_to_fail);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ progressbar_description.setVisibility(TextView.GONE);
+ preferences.edit().remove(Provider.KEY).commit();
+ setting_up_provider = false;
+ setResult(RESULT_CANCELED, mConfigState);
+ }
+ else if(resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE) {
+ mProgressBar.incrementProgressBy(1);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ progressbar_description.setVisibility(TextView.GONE);
+ //refreshProviderList(0);
+ setResult(RESULT_OK);
+ showProviderDetails(getCurrentFocus());
+ } else if(resultCode == ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE) {
+ //refreshProviderList(0);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ progressbar_description.setVisibility(TextView.GONE);
+ //Toast.makeText(getApplicationContext(), R.string.incorrectly_downloaded_certificate_message, Toast.LENGTH_LONG).show();
+ setResult(RESULT_CANCELED, mConfigState);
+ } else if(resultCode == AboutActivity.VIEWED) {
+ // Do nothing, right now
+ // I need this for CW to wait for the About activity to end before going back to Dashboard.
+ }
+ }
+
+ /**
+ * Callback method from {@link ProviderListFragment.Callbacks}
+ * indicating that the item with the given ID was selected.
+ */
+ @Override
+ public void onItemSelected(String id) {
+ //TODO Code 2 pane view
+ // resetOldConnection();
+ ProviderItem selected_provider = getProvider(id);
+ int provider_index = getProviderIndex(id);
+
+
+ startProgressBar(provider_index+1);
+ provider_list_fragment.hideAllBut(provider_index);
+
+ boolean danger_on = true;
+ if(preferences.contains(ProviderItem.DANGER_ON))
+ danger_on = preferences.getBoolean(ProviderItem.DANGER_ON, false);
+ setUpProvider(selected_provider.providerMainUrl(), danger_on);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if(setting_up_provider) {
+ stopSettingUpProvider();
+ } else {
+ usualBackButton();
+ }
+ }
+
+ private void stopSettingUpProvider() {
+ ProviderAPI.stop();
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ mProgressBar.setProgress(0);
+ progressbar_description.setVisibility(TextView.GONE);
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, Activity.MODE_PRIVATE).edit().remove(Provider.KEY).commit();
+ setting_up_provider = false;
+ showAllProviders();
+ }
+
+ private void usualBackButton() {
+ try {
+ boolean is_provider_set_up = new JSONObject(preferences.getString(Provider.KEY, "no provider")) != null ? true : false;
+ boolean is_provider_set_up_truly = new JSONObject(preferences.getString(Provider.KEY, "no provider")).length() != 0 ? true : false;
+ if(!is_provider_set_up || !is_provider_set_up_truly) {
+ askDashboardToQuitApp();
+ } else {
+ setResult(RESULT_OK);
+ }
+ } catch (JSONException e) {
+ askDashboardToQuitApp();
+ super.onBackPressed();
+ e.printStackTrace();
+ }
+ super.onBackPressed();
+ }
+ private void askDashboardToQuitApp() {
+ Intent ask_quit = new Intent();
+ ask_quit.putExtra(Dashboard.ACTION_QUIT, Dashboard.ACTION_QUIT);
+ setResult(RESULT_CANCELED, ask_quit);
+ }
+
+ private ProviderItem getProvider(String name) {
+ Iterator<ProviderItem> providers_iterator = ProviderListContent.ITEMS.iterator();
+ while(providers_iterator.hasNext()) {
+ ProviderItem provider = providers_iterator.next();
+ if(provider.name().equalsIgnoreCase(name)) {
+ return provider;
+ }
+ }
+ return null;
+ }
+
+ private String getId(String provider_main_url_string) {
+ try {
+ URL provider_url = new URL(provider_main_url_string);
+ URL aux_provider_url;
+ Iterator<ProviderItem> providers_iterator = ProviderListContent.ITEMS.iterator();
+ while(providers_iterator.hasNext()) {
+ ProviderItem provider = providers_iterator.next();
+ aux_provider_url = new URL(provider.providerMainUrl());
+ if(isSameURL(provider_url, aux_provider_url)) {
+ return provider.name();
+ }
+ }
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ /**
+ * Checks, whether 2 urls are pointing to the same location.
+ *
+ * @param url a url
+ * @param baseUrl an other url, that should be compared.
+ * @return true, if the urls point to the same host and port and use the
+ * same protocol, false otherwise.
+ */
+ private boolean isSameURL(final URL url, final URL baseUrl) {
+ if (!url.getProtocol().equals(baseUrl.getProtocol())) {
+ return false;
+ }
+ if (!url.getHost().equals(baseUrl.getHost())) {
+ return false;
+ }
+ if (url.getPort() != baseUrl.getPort()) {
+ return false;
+ }
+ return true;
+ }
+
+ private void startProgressBar() {
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ mProgressBar.setProgress(0);
+ mProgressBar.setMax(3);
+ }
+
+ private void startProgressBar(int list_item_index) {
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ progressbar_description.setVisibility(TextView.VISIBLE);
+ mProgressBar.setProgress(0);
+ mProgressBar.setMax(3);
+ int measured_height = listItemHeight(list_item_index);
+ mProgressBar.setTranslationY(measured_height);
+ progressbar_description.setTranslationY(measured_height + mProgressBar.getHeight());
+ }
+
+ private int getProviderIndex(String id) {
+ int index = 0;
+ Iterator<ProviderItem> providers_iterator = ProviderListContent.ITEMS.iterator();
+ while(providers_iterator.hasNext()) {
+ ProviderItem provider = providers_iterator.next();
+ if(provider.name().equalsIgnoreCase(id)) {
+ break;
+ } else index++;
+ }
+ return index;
+ }
+
+ private int listItemHeight(int list_item_index) {
+ ListView provider_list_view = (ListView)findViewById(android.R.id.list);
+ ListAdapter provider_list_adapter = provider_list_view.getAdapter();
+ View listItem = provider_list_adapter.getView(0, null, provider_list_view);
+ listItem.setLayoutParams(new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.WRAP_CONTENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT));
+ WindowManager wm = (WindowManager) getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ int screenWidth = display.getWidth(); // deprecated
+
+ int listViewWidth = screenWidth - 10 - 10;
+ int widthSpec = MeasureSpec.makeMeasureSpec(listViewWidth,
+ MeasureSpec.AT_MOST);
+ listItem.measure(widthSpec, 0);
+
+ return listItem.getMeasuredHeight();
+}
+
+ /**
+ * Loads providers data from url file contained in the project
+ * @return true if the file was read correctly
+ */
+ private boolean loadPreseededProviders() {
+ boolean loaded_preseeded_providers = false;
+ AssetManager asset_manager = getAssets();
+ String[] urls_filepaths = null;
+ try {
+ String url_files_folder = "urls";
+ //TODO Put that folder in a better place (also inside the "for")
+ urls_filepaths = asset_manager.list(url_files_folder);
+ String provider_name = "";
+ for(String url_filepath : urls_filepaths)
+ {
+ boolean custom = false;
+ provider_name = url_filepath.subSequence(0, url_filepath.indexOf(".")).toString();
+ if(ProviderListContent.ITEMS.isEmpty()) //TODO I have to implement a way of checking if a provider new or is already present in that ITEMS list
+ ProviderListContent.addItem(new ProviderItem(provider_name, asset_manager.open(url_files_folder + "/" + url_filepath)));
+ loaded_preseeded_providers = true;
+ }
+ } catch (IOException e) {
+ loaded_preseeded_providers = false;
+ }
+
+ return loaded_preseeded_providers;
+ }
+
+ /**
+ * Asks ProviderAPI to download an anonymous (anon) VPN certificate.
+ */
+ private void downloadAnonCert() {
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ Bundle parameters = new Bundle();
+
+ parameters.putString(TYPE_OF_CERTIFICATE, ANON_CERTIFICATE);
+
+ provider_API_command.setAction(ProviderAPI.DOWNLOAD_CERTIFICATE);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ startService(provider_API_command);
+ }
+
+ /**
+ * Open the new provider dialog
+ */
+ public void addAndSelectNewProvider() {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_new_provider_dialog = getFragmentManager().findFragmentByTag(NewProviderDialog.TAG);
+ if (previous_new_provider_dialog != null) {
+ fragment_transaction.remove(previous_new_provider_dialog);
+ }
+ fragment_transaction.addToBackStack(null);
+
+ DialogFragment newFragment = NewProviderDialog.newInstance();
+ newFragment.show(fragment_transaction, NewProviderDialog.TAG);
+ }
+
+ /**
+ * Open the new provider dialog with data
+ */
+ public void addAndSelectNewProvider(String main_url, boolean danger_on) {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_new_provider_dialog = getFragmentManager().findFragmentByTag(NewProviderDialog.TAG);
+ if (previous_new_provider_dialog != null) {
+ fragment_transaction.remove(previous_new_provider_dialog);
+ }
+
+ DialogFragment newFragment = NewProviderDialog.newInstance();
+ Bundle data = new Bundle();
+ data.putString(Provider.MAIN_URL, main_url);
+ data.putBoolean(ProviderItem.DANGER_ON, danger_on);
+ newFragment.setArguments(data);
+ newFragment.show(fragment_transaction, NewProviderDialog.TAG);
+ }
+
+ /**
+ * Once selected a provider, this fragment offers the user to log in,
+ * use it anonymously (if possible)
+ * or cancel his/her election pressing the back button.
+ * @param view
+ * @param reason_to_fail
+ */
+ public void showDownloadFailedDialog(View view, String reason_to_fail) {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_provider_details_dialog = getFragmentManager().findFragmentByTag(DownloadFailedDialog.TAG);
+ if (previous_provider_details_dialog != null) {
+ fragment_transaction.remove(previous_provider_details_dialog);
+ }
+ fragment_transaction.addToBackStack(null);
+
+ DialogFragment newFragment = DownloadFailedDialog.newInstance(reason_to_fail);
+ newFragment.show(fragment_transaction, DownloadFailedDialog.TAG);
+ }
+
+ /**
+ * Once selected a provider, this fragment offers the user to log in,
+ * use it anonymously (if possible)
+ * or cancel his/her election pressing the back button.
+ * @param view
+ */
+ public void showProviderDetails(View view) {
+ if(setting_up_provider) {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_provider_details_dialog = getFragmentManager().findFragmentByTag(ProviderDetailFragment.TAG);
+ if (previous_provider_details_dialog != null) {
+ fragment_transaction.remove(previous_provider_details_dialog);
+ }
+ fragment_transaction.addToBackStack(null);
+
+ DialogFragment newFragment = ProviderDetailFragment.newInstance();
+ newFragment.show(fragment_transaction, ProviderDetailFragment.TAG);
+ }
+ }
+
+ public void showAndSelectProvider(String provider_main_url, boolean danger_on) {
+ if(getId(provider_main_url).isEmpty())
+ showProvider(provider_main_url, danger_on);
+ autoSelectProvider(provider_main_url, danger_on);
+ }
+
+ private void showProvider(final String provider_main_url, final boolean danger_on) {
+ String provider_name = provider_main_url.replaceFirst("http[s]?://", "").replaceFirst("\\/", "_");
+ ProviderItem added_provider = new ProviderItem(provider_name, provider_main_url);
+ provider_list_fragment.addItem(added_provider);
+ }
+
+ private void autoSelectProvider(String provider_main_url, boolean danger_on) {
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(ProviderItem.DANGER_ON, danger_on).commit();
+ onItemSelected(getId(provider_main_url));
+ }
+
+ /**
+ * Asks ProviderAPI to download a new provider.json file
+ * @param provider_name
+ * @param provider_main_url
+ * @param danger_on tells if HTTPS client should bypass certificate errors
+ */
+ public void setUpProvider(String provider_main_url, boolean danger_on) {
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+ Bundle parameters = new Bundle();
+ parameters.putString(Provider.MAIN_URL, provider_main_url);
+ parameters.putBoolean(ProviderItem.DANGER_ON, danger_on);
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ startService(provider_API_command);
+ setting_up_provider = true;
+ }
+
+ public void retrySetUpProvider() {
+ cancelSettingUpProvider();
+ if(!ProviderAPI.caCertDownloaded()) {
+ addAndSelectNewProvider(ProviderAPI.lastProviderMainUrl(), ProviderAPI.lastDangerOn());
+ } else {
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ startService(provider_API_command);
+ }
+ }
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.configuration_wizard_activity, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item){
+ switch (item.getItemId()){
+ case R.id.about_leap:
+ startActivityForResult(new Intent(this, AboutActivity.class), 0);
+ return true;
+ case R.id.new_provider:
+ addAndSelectNewProvider();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public void showAllProviders() {
+ provider_list_fragment = (ProviderListFragment) getFragmentManager().findFragmentByTag(ProviderListFragment.TAG);
+ if(provider_list_fragment != null)
+ provider_list_fragment.unhideAll();
+ }
+
+ public void cancelSettingUpProvider() {
+ provider_list_fragment = (ProviderListFragment) getFragmentManager().findFragmentByTag(ProviderListFragment.TAG);
+ if(provider_list_fragment != null && preferences.contains(ProviderItem.DANGER_ON)) {
+ provider_list_fragment.removeLastItem();
+ }
+ preferences.edit().remove(Provider.KEY).remove(ProviderItem.DANGER_ON).remove(EIP.ALLOWED_ANON).remove(EIP.KEY).commit();
+ }
+
+ @Override
+ public void login() {
+ Intent ask_login = new Intent();
+ ask_login.putExtra(LogInDialog.VERB, LogInDialog.VERB);
+ setResult(RESULT_OK, ask_login);
+ setting_up_provider = false;
+ finish();
+ }
+
+ @Override
+ public void use_anonymously() {
+ setResult(RESULT_OK);
+ setting_up_provider = false;
+ finish();
+ }
+
+ public class ProviderAPIBroadcastReceiver_Update extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int update = intent.getIntExtra(ProviderAPI.CURRENT_PROGRESS, 0);
+ mProgressBar.setProgress(update);
+ }
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/Dashboard.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/Dashboard.java
new file mode 100644
index 00000000..b388b84a
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/Dashboard.java
@@ -0,0 +1,479 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.ProviderAPIResultReceiver.Receiver;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * The main user facing Activity of LEAP Android, consisting of status, controls,
+ * and access to preferences.
+ *
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ * @author parmegv
+ */
+public class Dashboard extends Activity implements LogInDialog.LogInDialogInterface,Receiver {
+
+ protected static final int CONFIGURE_LEAP = 0;
+ protected static final int SWITCH_PROVIDER = 1;
+
+ final public static String SHARED_PREFERENCES = "LEAPPreferences";
+ final public static String ACTION_QUIT = "quit";
+ public static final String REQUEST_CODE = "request_code";
+
+ private ProgressBar mProgressBar;
+ private TextView eipStatus;
+ private static Context app;
+ private static SharedPreferences preferences;
+ private static Provider provider;
+
+ private TextView providerNameTV;
+
+ private boolean authed_eip = false;
+
+ public ProviderAPIResultReceiver providerAPI_result_receiver;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ app = this;
+
+ PRNGFixes.apply();
+ // mProgressBar = (ProgressBar) findViewById(R.id.progressbar_dashboard);
+ // mProgressBar = (ProgressBar) findViewById(R.id.eipProgress);
+ // eipStatus = (TextView) findViewById(R.id.eipStatus);
+
+ mProgressBar = (ProgressBar) findViewById(R.id.eipProgress);
+
+ preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+
+ authed_eip = preferences.getBoolean(EIP.AUTHED_EIP, false);
+ if (preferences.getString(Provider.KEY, "").isEmpty())
+ startActivityForResult(new Intent(this,ConfigurationWizard.class),CONFIGURE_LEAP);
+ else
+ buildDashboard();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data){
+ if ( requestCode == CONFIGURE_LEAP || requestCode == SWITCH_PROVIDER) {
+ // It should be equivalent: if ( (requestCode == CONFIGURE_LEAP) || (data!= null && data.hasExtra(STOP_FIRST))) {
+ if ( resultCode == RESULT_OK ){
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putInt(EIP.PARSED_SERIAL, 0).commit();
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(EIP.AUTHED_EIP, authed_eip).commit();
+ Intent updateEIP = new Intent(getApplicationContext(), EIP.class);
+ updateEIP.setAction(EIP.ACTION_UPDATE_EIP_SERVICE);
+ startService(updateEIP);
+ buildDashboard();
+ invalidateOptionsMenu();
+ if(data != null && data.hasExtra(LogInDialog.VERB)) {
+ View view = ((ViewGroup)findViewById(android.R.id.content)).getChildAt(0);
+ logInDialog(view, Bundle.EMPTY);
+ }
+ } else if(resultCode == RESULT_CANCELED && data.hasExtra(ACTION_QUIT)) {
+ finish();
+ } else
+ configErrorDialog();
+ }
+ }
+
+ /**
+ * Dialog shown when encountering a configuration error. Such errors require
+ * reconfiguring LEAP or aborting the application.
+ */
+ private void configErrorDialog() {
+ AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getAppContext());
+ alertBuilder.setTitle(getResources().getString(R.string.setup_error_title));
+ alertBuilder
+ .setMessage(getResources().getString(R.string.setup_error_text))
+ .setCancelable(false)
+ .setPositiveButton(getResources().getString(R.string.setup_error_configure_button), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startActivityForResult(new Intent(getAppContext(),ConfigurationWizard.class),CONFIGURE_LEAP);
+ }
+ })
+ .setNegativeButton(getResources().getString(R.string.setup_error_close_button), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ SharedPreferences.Editor prefsEdit = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE).edit();
+ prefsEdit.remove(Provider.KEY).commit();
+ finish();
+ }
+ })
+ .show();
+ }
+
+ /**
+ * Inflates permanent UI elements of the View and contains logic for what
+ * service dependent UI elements to include.
+ */
+ private void buildDashboard() {
+ provider = Provider.getInstance();
+ provider.init( this );
+
+ setContentView(R.layout.client_dashboard);
+
+ providerNameTV = (TextView) findViewById(R.id.providerName);
+ providerNameTV.setText(provider.getDomain());
+ providerNameTV.setTextSize(28);
+
+ mProgressBar = (ProgressBar) findViewById(R.id.eipProgress);
+
+ FragmentManager fragMan = getFragmentManager();
+ if ( provider.hasEIP()){
+ EipServiceFragment eipFragment = new EipServiceFragment();
+ fragMan.beginTransaction().replace(R.id.servicesCollection, eipFragment, EipServiceFragment.TAG).commit();
+ }
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ JSONObject provider_json;
+ try {
+ provider_json = new JSONObject(getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.KEY, ""));
+ JSONObject service_description = provider_json.getJSONObject(Provider.SERVICE);
+
+ if(service_description.getBoolean(Provider.ALLOW_REGISTRATION)) {
+ if(authed_eip) {
+ menu.findItem(R.id.login_button).setVisible(false);
+ menu.findItem(R.id.logout_button).setVisible(true);
+ } else {
+ menu.findItem(R.id.login_button).setVisible(true);
+ menu.findItem(R.id.logout_button).setVisible(false);
+ }
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.client_dashboard, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item){
+ Intent intent;
+ switch (item.getItemId()){
+ case R.id.about_leap:
+ intent = new Intent(this, AboutActivity.class);
+ startActivity(intent);
+ return true;
+ case R.id.switch_provider:
+ if (Provider.getInstance().hasEIP()){
+ if (getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getBoolean(EIP.AUTHED_EIP, false)){
+ logOut();
+ }
+ eipStop();
+ }
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().remove(Provider.KEY).commit();
+ startActivityForResult(new Intent(this,ConfigurationWizard.class), SWITCH_PROVIDER);
+ return true;
+ case R.id.login_button:
+ View view = ((ViewGroup)findViewById(android.R.id.content)).getChildAt(0);
+ logInDialog(view, Bundle.EMPTY);
+ return true;
+ case R.id.logout_button:
+ logOut();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+
+ }
+
+ @Override
+ public void authenticate(String username, String password) {
+ mProgressBar = (ProgressBar) findViewById(R.id.eipProgress);
+ eipStatus = (TextView) findViewById(R.id.eipStatus);
+
+ providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler());
+ providerAPI_result_receiver.setReceiver(this);
+
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ Bundle parameters = new Bundle();
+ parameters.putString(LogInDialog.USERNAME, username);
+ parameters.putString(LogInDialog.PASSWORD, password);
+
+ JSONObject provider_json;
+ try {
+ provider_json = new JSONObject(preferences.getString(Provider.KEY, ""));
+ parameters.putString(Provider.API_URL, provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION));
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ provider_API_command.setAction(ProviderAPI.SRP_AUTH);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ eipStatus.setText(R.string.authenticating_message);
+ //mProgressBar.setMax(4);
+ startService(provider_API_command);
+ }
+
+ public void cancelAuthedEipOn() {
+ EipServiceFragment eipFragment = (EipServiceFragment) getFragmentManager().findFragmentByTag(EipServiceFragment.TAG);
+ eipFragment.checkEipSwitch(false);
+ }
+
+ /**
+ * Asks ProviderAPI to log out.
+ */
+ public void logOut() {
+ providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler());
+ providerAPI_result_receiver.setReceiver(this);
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ Bundle parameters = new Bundle();
+
+ JSONObject provider_json;
+ try {
+ provider_json = new JSONObject(preferences.getString(Provider.KEY, ""));
+ parameters.putString(Provider.API_URL, provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION));
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ provider_API_command.setAction(ProviderAPI.LOG_OUT);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ if(mProgressBar == null) mProgressBar = (ProgressBar) findViewById(R.id.eipProgress);
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ if(eipStatus == null) eipStatus = (TextView) findViewById(R.id.eipStatus);
+ eipStatus.setText(R.string.logout_message);
+ // eipStatus.setText("Starting to logout");
+
+ startService(provider_API_command);
+ //mProgressBar.setMax(1);
+
+ }
+
+ /**
+ * Shows the log in dialog.
+ * @param view from which the dialog is created.
+ */
+ public void logInDialog(View view, Bundle resultData) {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_log_in_dialog = getFragmentManager().findFragmentByTag(LogInDialog.TAG);
+ if (previous_log_in_dialog != null) {
+ fragment_transaction.remove(previous_log_in_dialog);
+ }
+ fragment_transaction.addToBackStack(null);
+
+ DialogFragment newFragment = LogInDialog.newInstance();
+ if(resultData != null && !resultData.isEmpty()) {
+ newFragment.setArguments(resultData);
+ }
+ newFragment.show(fragment_transaction, LogInDialog.TAG);
+ }
+
+ /**
+ * Asks ProviderAPI to download an authenticated OpenVPN certificate.
+ * @param session_id cookie for the server to allow us to download the certificate.
+ */
+ private void downloadAuthedUserCertificate(/*Cookie session_id*/) {
+ providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler());
+ providerAPI_result_receiver.setReceiver(this);
+
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ Bundle parameters = new Bundle();
+ parameters.putString(ConfigurationWizard.TYPE_OF_CERTIFICATE, ConfigurationWizard.AUTHED_CERTIFICATE);
+ /*parameters.putString(ConfigHelper.SESSION_ID_COOKIE_KEY, session_id.getName());
+ parameters.putString(ConfigHelper.SESSION_ID_KEY, session_id.getValue());*/
+
+ provider_API_command.setAction(ProviderAPI.DOWNLOAD_CERTIFICATE);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ startService(provider_API_command);
+ }
+
+ @Override
+ public void onReceiveResult(int resultCode, Bundle resultData) {
+ if(resultCode == ProviderAPI.SRP_AUTHENTICATION_SUCCESSFUL){
+ String session_id_cookie_key = resultData.getString(ProviderAPI.SESSION_ID_COOKIE_KEY);
+ String session_id_string = resultData.getString(ProviderAPI.SESSION_ID_KEY);
+ setResult(RESULT_OK);
+
+ authed_eip = true;
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(EIP.AUTHED_EIP, authed_eip).commit();
+
+ invalidateOptionsMenu();
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ changeStatusMessage(resultCode);
+
+ //Cookie session_id = new BasicClientCookie(session_id_cookie_key, session_id_string);
+ downloadAuthedUserCertificate(/*session_id*/);
+ } else if(resultCode == ProviderAPI.SRP_AUTHENTICATION_FAILED) {
+ logInDialog(getCurrentFocus(), resultData);
+ } else if(resultCode == ProviderAPI.LOGOUT_SUCCESSFUL) {
+ authed_eip = false;
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(EIP.AUTHED_EIP, authed_eip).commit();
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ mProgressBar.setProgress(0);
+ invalidateOptionsMenu();
+ setResult(RESULT_OK);
+ changeStatusMessage(resultCode);
+
+ } else if(resultCode == ProviderAPI.LOGOUT_FAILED) {
+ setResult(RESULT_CANCELED);
+ changeStatusMessage(resultCode);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ } else if(resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE) {
+ setResult(RESULT_OK);
+ changeStatusMessage(resultCode);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ if(EipServiceFragment.isEipSwitchChecked())
+ eipStart();
+ } else if(resultCode == ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE) {
+ setResult(RESULT_CANCELED);
+ changeStatusMessage(resultCode);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ }
+ }
+
+ private void changeStatusMessage(final int previous_result_code) {
+ // TODO Auto-generated method stub
+ ResultReceiver eip_status_receiver = new ResultReceiver(new Handler()){
+ protected void onReceiveResult(int resultCode, Bundle resultData){
+ super.onReceiveResult(resultCode, resultData);
+ String request = resultData.getString(EIP.REQUEST_TAG);
+ if (request.equalsIgnoreCase(EIP.ACTION_IS_EIP_RUNNING)){
+ if (resultCode == Activity.RESULT_OK){
+
+ switch(previous_result_code){
+ case ProviderAPI.SRP_AUTHENTICATION_SUCCESSFUL: eipStatus.setText(R.string.succesful_authentication_message); break;
+ case ProviderAPI.SRP_AUTHENTICATION_FAILED: eipStatus.setText(R.string.authentication_failed_message); break;
+ case ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE: eipStatus.setText(R.string.authed_secured_status); break;
+ case ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE: eipStatus.setText(R.string.incorrectly_downloaded_certificate_message); break;
+ case ProviderAPI.LOGOUT_SUCCESSFUL: eipStatus.setText(R.string.anonymous_secured_status); break;
+ case ProviderAPI.LOGOUT_FAILED: eipStatus.setText(R.string.log_out_failed_message); break;
+
+ }
+ }
+ else if(resultCode == Activity.RESULT_CANCELED){
+
+ switch(previous_result_code){
+
+ case ProviderAPI.SRP_AUTHENTICATION_SUCCESSFUL: eipStatus.setText(R.string.succesful_authentication_message); break;
+ case ProviderAPI.SRP_AUTHENTICATION_FAILED: eipStatus.setText(R.string.authentication_failed_message); break;
+ case ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE: eipStatus.setText(R.string.future_authed_secured_status); break;
+ case ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE: eipStatus.setText(R.string.incorrectly_downloaded_certificate_message); break;
+ case ProviderAPI.LOGOUT_SUCCESSFUL: eipStatus.setText(R.string.future_anonymous_secured_status); break;
+ case ProviderAPI.LOGOUT_FAILED: eipStatus.setText(R.string.log_out_failed_message); break;
+ }
+ }
+ }
+
+ }
+ };
+ eipIsRunning(eip_status_receiver);
+ }
+
+ /**
+ * For retrieving the base application Context in classes that don't extend
+ * Android's Activity class
+ *
+ * @return Application Context as defined by <code>this</code> for Dashboard instance
+ */
+ public static Context getAppContext() {
+ return app;
+ }
+
+
+ @Override
+ public void startActivityForResult(Intent intent, int requestCode) {
+ intent.putExtra(Dashboard.REQUEST_CODE, requestCode);
+ super.startActivityForResult(intent, requestCode);
+ }
+ /**
+ * Send a command to EIP
+ *
+ * @param action A valid String constant from EIP class representing an Intent
+ * filter for the EIP class
+ */
+ private void eipIsRunning(ResultReceiver eip_receiver){
+ // TODO validate "action"...how do we get the list of intent-filters for a class via Android API?
+ Intent eip_intent = new Intent(this, EIP.class);
+ eip_intent.setAction(EIP.ACTION_IS_EIP_RUNNING);
+ eip_intent.putExtra(EIP.RECEIVER_TAG, eip_receiver);
+ startService(eip_intent);
+ }
+
+ /**
+ * Send a command to EIP
+ *
+ */
+ private void eipStop(){
+ // TODO validate "action"...how do we get the list of intent-filters for a class via Android API?
+ Intent eip_intent = new Intent(this, EIP.class);
+ eip_intent.setAction(EIP.ACTION_STOP_EIP);
+ // eip_intent.putExtra(EIP.RECEIVER_TAG, eip_receiver);
+ startService(eip_intent);
+
+ }
+
+ private void eipStart(){
+ Intent eip_intent = new Intent(this, EIP.class);
+ eip_intent.setAction(EIP.ACTION_START_EIP);
+ eip_intent.putExtra(EIP.RECEIVER_TAG, EipServiceFragment.getReceiver());
+ startService(eip_intent);
+
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
new file mode 100644
index 00000000..f78002b0
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.NewProviderDialog.NewProviderDialogInterface;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+/**
+ * Implements a dialog to show why a download failed.
+ *
+ * @author parmegv
+ *
+ */
+public class DownloadFailedDialog extends DialogFragment {
+
+ public static String TAG = "downloaded_failed_dialog";
+ private String reason_to_fail;
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance(String reason_to_fail) {
+ DownloadFailedDialog dialog_fragment = new DownloadFailedDialog();
+ dialog_fragment.reason_to_fail = reason_to_fail;
+ return dialog_fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+
+ builder.setMessage(reason_to_fail)
+ .setPositiveButton(R.string.retry, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ interface_with_ConfigurationWizard.retrySetUpProvider();
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ interface_with_ConfigurationWizard.cancelSettingUpProvider();
+ dialog.dismiss();
+ }
+ });
+
+ // Create the AlertDialog object and return it
+ return builder.create();
+ }
+
+ public interface DownloadFailedDialogInterface {
+ public void retrySetUpProvider();
+ public void cancelSettingUpProvider();
+ }
+
+ DownloadFailedDialogInterface interface_with_ConfigurationWizard;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ interface_with_ConfigurationWizard = (DownloadFailedDialogInterface) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString()
+ + " must implement NoticeDialogListener");
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ interface_with_ConfigurationWizard.cancelSettingUpProvider();
+ dialog.dismiss();
+ }
+
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/EIP.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/EIP.java
new file mode 100644
index 00000000..e773e3b9
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/EIP.java
@@ -0,0 +1,623 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.Vector;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.R;
+import se.leap.openvpn.ConfigParser;
+import se.leap.openvpn.ConfigParser.ConfigParseError;
+import se.leap.openvpn.LaunchVPN;
+import se.leap.openvpn.OpenVpnManagementThread;
+import se.leap.openvpn.OpenVpnService;
+import se.leap.openvpn.OpenVpnService.LocalBinder;
+import se.leap.openvpn.ProfileManager;
+import se.leap.openvpn.VpnProfile;
+import android.app.Activity;
+import android.app.IntentService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.drm.DrmStore.Action;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+/**
+ * EIP is the abstract base class for interacting with and managing the Encrypted
+ * Internet Proxy connection. Connections are started, stopped, and queried through
+ * this IntentService.
+ * Contains logic for parsing eip-service.json from the provider, configuring and selecting
+ * gateways, and controlling {@link .openvpn.OpenVpnService} connections.
+ *
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ */
+public final class EIP extends IntentService {
+
+ public final static String AUTHED_EIP = "authed eip";
+ public final static String ACTION_START_EIP = "se.leap.bitmaskclient.START_EIP";
+ public final static String ACTION_STOP_EIP = "se.leap.bitmaskclient.STOP_EIP";
+ public final static String ACTION_UPDATE_EIP_SERVICE = "se.leap.bitmaskclient.UPDATE_EIP_SERVICE";
+ public final static String ACTION_IS_EIP_RUNNING = "se.leap.bitmaskclient.IS_RUNNING";
+ public final static String EIP_NOTIFICATION = "EIP_NOTIFICATION";
+ public final static String ALLOWED_ANON = "allow_anonymous";
+ public final static String CERTIFICATE = "cert";
+ public final static String PRIVATE_KEY = "private_key";
+ public final static String KEY = "eip";
+ public final static String PARSED_SERIAL = "eip_parsed_serial";
+ public final static String SERVICE_API_PATH = "config/eip-service.json";
+ public final static String RECEIVER_TAG = "receiverTag";
+ public final static String REQUEST_TAG = "requestTag";
+ public final static String TAG = "se.leap.bitmaskclient.EIP";
+
+
+ private static Context context;
+ private static ResultReceiver mReceiver;
+ private static OpenVpnService mVpnService;
+ private static boolean mBound = false;
+ // Used to store actions to "resume" onServiceConnection
+ private static String mPending = null;
+
+ private static int parsedEipSerial;
+ private static JSONObject eipDefinition = null;
+
+ private static OVPNGateway activeGateway = null;
+
+ public EIP(){
+ super("LEAPEIP");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ context = getApplicationContext();
+
+ updateEIPService();
+
+ this.retreiveVpnService();
+ }
+
+ @Override
+ public void onDestroy() {
+ unbindService(mVpnServiceConn);
+ mBound = false;
+
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ String action = intent.getAction();
+ mReceiver = intent.getParcelableExtra(RECEIVER_TAG);
+
+ if ( action == ACTION_IS_EIP_RUNNING )
+ this.isRunning();
+ if ( action == ACTION_UPDATE_EIP_SERVICE )
+ this.updateEIPService();
+ else if ( action == ACTION_START_EIP )
+ this.startEIP();
+ else if ( action == ACTION_STOP_EIP )
+ this.stopEIP();
+ }
+
+ /**
+ * Sends an Intent to bind OpenVpnService.
+ * Used when OpenVpnService isn't bound but might be running.
+ */
+ private boolean retreiveVpnService() {
+ Intent bindIntent = new Intent(this,OpenVpnService.class);
+ bindIntent.setAction(OpenVpnService.RETRIEVE_SERVICE);
+ return bindService(bindIntent, mVpnServiceConn, BIND_AUTO_CREATE);
+ }
+
+ private static ServiceConnection mVpnServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ LocalBinder binder = (LocalBinder) service;
+ mVpnService = binder.getService();
+ mBound = true;
+
+ if (mReceiver != null && mPending != null) {
+
+ boolean running = mVpnService.isRunning();
+
+ int resultCode = Activity.RESULT_CANCELED;
+
+ if (mPending.equals(ACTION_IS_EIP_RUNNING)){
+ resultCode = (running) ? Activity.RESULT_OK : Activity.RESULT_CANCELED;
+
+ }
+ else if (mPending.equals(ACTION_START_EIP)){
+ resultCode = (running) ? Activity.RESULT_OK : Activity.RESULT_CANCELED;
+ }
+ else if (mPending.equals(ACTION_STOP_EIP)){
+ resultCode = (running) ? Activity.RESULT_CANCELED
+ : Activity.RESULT_OK;
+ }
+ Bundle resultData = new Bundle();
+ resultData.putString(REQUEST_TAG, ACTION_IS_EIP_RUNNING);
+ mReceiver.send(resultCode, resultData);
+
+ mPending = null;
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mBound = false;
+
+ if (mReceiver != null){
+ Bundle resultData = new Bundle();
+ resultData.putString(REQUEST_TAG, EIP_NOTIFICATION);
+ mReceiver.send(Activity.RESULT_CANCELED, resultData);
+ }
+ }
+
+
+ };
+
+ /**
+ * Attempts to determine if OpenVpnService has an established VPN connection
+ * through the bound ServiceConnection. If there is no bound service, this
+ * method will attempt to bind a running OpenVpnService and send
+ * <code>Activity.RESULT_CANCELED</code> to the ResultReceiver that made the
+ * request.
+ * Note: If the request to bind OpenVpnService is successful, the ResultReceiver
+ * will be notified in {@link onServiceConnected()}
+ */
+
+ private void isRunning() {
+ Bundle resultData = new Bundle();
+ resultData.putString(REQUEST_TAG, ACTION_IS_EIP_RUNNING);
+ int resultCode = Activity.RESULT_CANCELED;
+ if (mBound) {
+ resultCode = (mVpnService.isRunning()) ? Activity.RESULT_OK : Activity.RESULT_CANCELED;
+
+ if (mReceiver != null){
+ mReceiver.send(resultCode, resultData);
+ }
+ } else {
+ mPending = ACTION_IS_EIP_RUNNING;
+ boolean retrieved_vpn_service = retreiveVpnService();
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ boolean running = false;
+ try {
+ running = mVpnService.isRunning();
+ } catch (NullPointerException e){
+ e.printStackTrace();
+ }
+
+ if (retrieved_vpn_service && running && mReceiver != null){
+ mReceiver.send(Activity.RESULT_OK, resultData);
+ }
+ else{
+ mReceiver.send(Activity.RESULT_CANCELED, resultData);
+ }
+ }
+ }
+
+ /**
+ * Initiates an EIP connection by selecting a gateway and preparing and sending an
+ * Intent to {@link se.leap.openvpn.LaunchVPN}
+ */
+ private void startEIP() {
+ activeGateway = selectGateway();
+
+ Intent intent = new Intent(this,LaunchVPN.class);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(LaunchVPN.EXTRA_KEY, activeGateway.mVpnProfile.getUUID().toString() );
+ intent.putExtra(LaunchVPN.EXTRA_NAME, activeGateway.mVpnProfile.getName() );
+ intent.putExtra(RECEIVER_TAG, mReceiver);
+ startActivity(intent);
+ mPending = ACTION_START_EIP;
+ }
+
+ /**
+ * Disconnects the EIP connection gracefully through the bound service or forcefully
+ * if there is no bound service. Sends a message to the requesting ResultReceiver.
+ */
+ private void stopEIP() {
+ if (mBound)
+ mVpnService.onRevoke();
+ else
+ OpenVpnManagementThread.stopOpenVPN();
+
+ if (mReceiver != null){
+ Bundle resultData = new Bundle();
+ resultData.putString(REQUEST_TAG, ACTION_STOP_EIP);
+ mReceiver.send(Activity.RESULT_OK, resultData);
+ }
+ }
+
+ /**
+ * Loads eip-service.json from SharedPreferences and calls {@link updateGateways()}
+ * to parse gateway definitions.
+ * TODO Implement API call to refresh eip-service.json from the provider
+ */
+ private void updateEIPService() {
+ try {
+ eipDefinition = new JSONObject(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(KEY, ""));
+ parsedEipSerial = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getInt(PARSED_SERIAL, 0);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ if(parsedEipSerial == 0) {
+ // Delete all vpn profiles
+ ProfileManager vpl = ProfileManager.getInstance(context);
+ VpnProfile[] profiles = (VpnProfile[]) vpl.getProfiles().toArray(new VpnProfile[vpl.getProfiles().size()]);
+ for (int current_profile = 0; current_profile < profiles.length; current_profile++){
+ vpl.removeProfile(context, profiles[current_profile]);
+ }
+ }
+ if (eipDefinition.optInt("serial") > parsedEipSerial)
+ updateGateways();
+ }
+
+ /**
+ * Choose a gateway to connect to based on timezone from system locale data
+ *
+ * @return The gateway to connect to
+ */
+ private OVPNGateway selectGateway() {
+ // TODO Remove String arg constructor in favor of findGatewayByName(String)
+
+ Calendar cal = Calendar.getInstance();
+ int localOffset = cal.get(Calendar.ZONE_OFFSET) / 3600000;
+ TreeMap<Integer, Set<String>> offsets = new TreeMap<Integer, Set<String>>();
+ JSONObject locationsObjects = null;
+ Iterator<String> locations = null;
+ try {
+ locationsObjects = eipDefinition.getJSONObject("locations");
+ locations = locationsObjects.keys();
+ } catch (JSONException e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+
+ while (locations.hasNext()) {
+ String locationName = locations.next();
+ JSONObject location = null;
+ try {
+ location = locationsObjects.getJSONObject(locationName);
+
+ // Distance along the numberline of Prime Meridian centric, assumes UTC-11 through UTC+12
+ int dist = Math.abs(localOffset - location.optInt("timezone"));
+ // Farther than 12 timezones and it's shorter around the "back"
+ if (dist > 12)
+ dist = 12 - (dist -12); // Well i'll be. Absolute values make equations do funny things.
+
+ Set<String> set = offsets.get(dist);
+ if (set == null) set = new HashSet<String>();
+ set.add(locationName);
+ offsets.put(dist, set);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+
+ String closestLocation = offsets.isEmpty() ? "" : offsets.firstEntry().getValue().iterator().next();
+ JSONArray gateways = null;
+ String chosenHost = null;
+ try {
+ gateways = eipDefinition.getJSONArray("gateways");
+ for (int i = 0; i < gateways.length(); i++) {
+ JSONObject gw = gateways.getJSONObject(i);
+ if ( gw.getString("location").equalsIgnoreCase(closestLocation) || closestLocation.isEmpty()){
+ chosenHost = gw.getString("host");
+ break;
+ }
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ return new OVPNGateway(chosenHost);
+ }
+
+ /**
+ * Walk the list of gateways defined in eip-service.json and parse them into
+ * OVPNGateway objects.
+ * TODO Store the OVPNGateways (as Serializable) in SharedPreferences
+ */
+ private void updateGateways(){
+ JSONArray gatewaysDefined = null;
+
+ try {
+ gatewaysDefined = eipDefinition.getJSONArray("gateways");
+ } catch (JSONException e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+
+ for ( int i=0 ; i < gatewaysDefined.length(); i++ ){
+
+ JSONObject gw = null;
+
+ try {
+ gw = gatewaysDefined.getJSONObject(i);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ try {
+ if ( gw.getJSONObject("capabilities").getJSONArray("transport").toString().contains("openvpn") ){
+ new OVPNGateway(gw);
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putInt(PARSED_SERIAL, eipDefinition.optInt(Provider.API_RETURN_SERIAL)).commit();
+ }
+
+ /**
+ * OVPNGateway provides objects defining gateways and their options and metadata.
+ * Each instance contains a VpnProfile for OpenVPN specific data and member
+ * variables describing capabilities and location
+ *
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ */
+ private class OVPNGateway {
+
+ private String TAG = "OVPNGateway";
+
+ private String mName;
+ private VpnProfile mVpnProfile;
+ private JSONObject mGateway;
+ private HashMap<String,Vector<Vector<String>>> options = new HashMap<String, Vector<Vector<String>>>();
+
+
+ /**
+ * Attempts to retrieve a VpnProfile by name and build an OVPNGateway around it.
+ * FIXME This needs to become a findGatewayByName() method
+ *
+ * @param name The hostname of the gateway to inflate
+ */
+ private OVPNGateway(String name){
+ mName = name;
+
+ this.loadVpnProfile();
+ }
+
+ private void loadVpnProfile() {
+ ProfileManager vpl = ProfileManager.getInstance(context);
+
+ try {
+ if ( mName == null )
+ mVpnProfile = vpl.getProfiles().iterator().next();
+ else
+ mVpnProfile = vpl.getProfileByName(mName);
+ } catch (NoSuchElementException e) {
+ updateEIPService();
+ this.loadVpnProfile(); // FIXME catch infinite loops
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Build a gateway object from a JSON OpenVPN gateway definition in eip-service.json
+ * and create a VpnProfile belonging to it.
+ *
+ * @param gateway The JSON OpenVPN gateway definition to parse
+ */
+ protected OVPNGateway(JSONObject gateway){
+
+ mGateway = gateway;
+
+ // Currently deletes VpnProfile for host, if there already is one, and builds new
+ ProfileManager vpl = ProfileManager.getInstance(context);
+ Collection<VpnProfile> profiles = vpl.getProfiles();
+ for (Iterator<VpnProfile> it = profiles.iterator(); it.hasNext(); ){
+ VpnProfile p = it.next();
+ try {
+ if ( p.mName.equalsIgnoreCase( gateway.getString("host") ) ){
+ it.remove();
+ vpl.removeProfile(context, p);
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ this.parseOptions();
+ this.createVPNProfile();
+
+ setUniqueProfileName(vpl);
+ vpl.addProfile(mVpnProfile);
+ vpl.saveProfile(context, mVpnProfile);
+ vpl.saveProfileList(context);
+ }
+
+ /**
+ * Attempts to create a unique profile name from the hostname of the gateway
+ *
+ * @param profileManager
+ */
+ private void setUniqueProfileName(ProfileManager profileManager) {
+ int i=0;
+
+ String newname;
+ try {
+ newname = mGateway.getString("host");
+ while(profileManager.getProfileByName(newname)!=null) {
+ i++;
+ if(i==1)
+ newname = getString(R.string.converted_profile);
+ else
+ newname = getString(R.string.converted_profile_i,i);
+ }
+
+ mVpnProfile.mName=newname;
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ Log.v(TAG,"Couldn't read gateway name for profile creation!");
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * FIXME This method is really the outline of the refactoring needed in se.leap.openvpn.ConfigParser
+ */
+ private void parseOptions(){
+
+ // FIXME move these to a common API (& version) definition place, like ProviderAPI or ConfigHelper
+ String common_options = "openvpn_configuration";
+ String remote = "ip_address";
+ String ports = "ports";
+ String protos = "protocols";
+ String capabilities = "capabilities";
+ String location_key = "location";
+ String locations = "locations";
+
+ Vector<String> arg = new Vector<String>();
+ Vector<Vector<String>> args = new Vector<Vector<String>>();
+
+ try {
+ JSONObject def = (JSONObject) eipDefinition.get(common_options);
+ Iterator keys = def.keys();
+ Vector<Vector<String>> value = new Vector<Vector<String>>();
+ while ( keys.hasNext() ){
+ String key = keys.next().toString();
+
+ arg.add(key);
+ for ( String word : def.getString(key).split(" ") )
+ arg.add(word);
+ value.add( (Vector<String>) arg.clone() );
+ options.put(key, (Vector<Vector<String>>) value.clone());
+ value.clear();
+ arg.clear();
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ try {
+ arg.add(remote);
+ arg.add(mGateway.getString(remote));
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ args.add((Vector<String>) arg.clone());
+ options.put("remote", (Vector<Vector<String>>) args.clone() );
+ arg.clear();
+ args.clear();
+
+
+
+ try {
+
+ arg.add(location_key);
+ String locationText = "";
+ locationText = eipDefinition.getJSONObject(locations).getJSONObject(mGateway.getString(location_key)).getString("name");
+ arg.add(locationText);
+
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ args.add((Vector<String>) arg.clone());
+ options.put("location", (Vector<Vector<String>>) args.clone() );
+
+ arg.clear();
+ args.clear();
+ JSONArray protocolsJSON = null;
+ arg.add("proto");
+ try {
+ protocolsJSON = mGateway.getJSONObject(capabilities).getJSONArray(protos);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ Vector<String> protocols = new Vector<String>();
+ for ( int i=0; i<protocolsJSON.length(); i++ )
+ protocols.add(protocolsJSON.optString(i));
+ if ( protocols.contains("udp"))
+ arg.add("udp");
+ else if ( protocols.contains("tcp"))
+ arg.add("tcp");
+ args.add((Vector<String>) arg.clone());
+ options.put("proto", (Vector<Vector<String>>) args.clone());
+ arg.clear();
+ args.clear();
+
+
+ String port = null;
+ arg.add("port");
+ try {
+ port = mGateway.getJSONObject(capabilities).getJSONArray(ports).optString(0);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ arg.add(port);
+ args.add((Vector<String>) arg.clone());
+ options.put("port", (Vector<Vector<String>>) args.clone());
+ args.clear();
+ arg.clear();
+ }
+
+ /**
+ * Create and attach the VpnProfile to our gateway object
+ */
+ protected void createVPNProfile(){
+ try {
+ ConfigParser cp = new ConfigParser();
+ cp.setDefinition(options);
+ VpnProfile vp = cp.convertProfile();
+ mVpnProfile = vp;
+ Log.v(TAG,"Created VPNProfile");
+ } catch (ConfigParseError e) {
+ // FIXME We didn't get a VpnProfile! Error handling! and log level
+ Log.v(TAG,"Error createing VPNProfile");
+ e.printStackTrace();
+ }
+ }
+ }
+
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/EipServiceFragment.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/EipServiceFragment.java
new file mode 100644
index 00000000..b4cb541a
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/EipServiceFragment.java
@@ -0,0 +1,306 @@
+package se.leap.bitmaskclient;
+
+import se.leap.bitmaskclient.R;
+import se.leap.openvpn.LogWindow;
+import se.leap.openvpn.OpenVPN;
+import se.leap.openvpn.OpenVPN.StateListener;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.CompoundButton;
+import android.widget.RelativeLayout;
+import android.widget.Switch;
+import android.widget.TextView;
+
+public class EipServiceFragment extends Fragment implements StateListener, OnCheckedChangeListener {
+
+ protected static final String IS_EIP_PENDING = "is_eip_pending";
+
+ private View eipFragment;
+ private static Switch eipSwitch;
+ private View eipDetail;
+ private TextView eipStatus;
+
+ private boolean eipAutoSwitched = true;
+
+ private boolean mEipStartPending = false;
+
+ private boolean set_switch_off = false;
+
+ private static EIPReceiver mEIPReceiver;
+
+
+ public static String TAG = "se.leap.bitmask.EipServiceFragment";
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ eipFragment = inflater.inflate(R.layout.eip_service_fragment, container, false);
+
+ eipDetail = ((RelativeLayout) eipFragment.findViewById(R.id.eipDetail));
+ eipDetail.setVisibility(View.VISIBLE);
+
+ View eipSettings = eipFragment.findViewById(R.id.eipSettings);
+ eipSettings.setVisibility(View.GONE); // FIXME too!
+
+ if (mEipStartPending)
+ eipFragment.findViewById(R.id.eipProgress).setVisibility(View.VISIBLE);
+
+ eipStatus = (TextView) eipFragment.findViewById(R.id.eipStatus);
+
+ eipSwitch = (Switch) eipFragment.findViewById(R.id.eipSwitch);
+
+
+ eipSwitch.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ eipAutoSwitched = false;
+ return false;
+ }
+ });
+ eipSwitch.setOnCheckedChangeListener(this);
+
+
+ return eipFragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mEIPReceiver = new EIPReceiver(new Handler());
+
+ if (savedInstanceState != null)
+ mEipStartPending = savedInstanceState.getBoolean(IS_EIP_PENDING);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ OpenVPN.addStateListener(this);
+ if(set_switch_off) {
+ eipSwitch.setChecked(false);
+ set_switch_off = false;
+ }
+ }
+
+ protected void setSwitchOff(boolean value) {
+ set_switch_off = value;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ OpenVPN.removeStateListener(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(IS_EIP_PENDING, mEipStartPending);
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (buttonView.equals(eipSwitch) && !eipAutoSwitched){
+ boolean allowed_anon = getActivity().getSharedPreferences(Dashboard.SHARED_PREFERENCES, Activity.MODE_PRIVATE).getBoolean(EIP.ALLOWED_ANON, false);
+ String certificate = getActivity().getSharedPreferences(Dashboard.SHARED_PREFERENCES, Activity.MODE_PRIVATE).getString(EIP.CERTIFICATE, "");
+ if(allowed_anon || !certificate.isEmpty()) {
+ if (isChecked){
+ mEipStartPending = true;
+ eipFragment.findViewById(R.id.eipProgress).setVisibility(View.VISIBLE);
+ ((TextView) eipFragment.findViewById(R.id.eipStatus)).setText(R.string.eip_status_start_pending);
+ eipCommand(EIP.ACTION_START_EIP);
+ } else {
+ if (mEipStartPending){
+ AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity());
+ alertBuilder.setTitle(getResources().getString(R.string.eip_cancel_connect_title));
+ alertBuilder
+ .setMessage(getResources().getString(R.string.eip_cancel_connect_text))
+ .setPositiveButton(getResources().getString(R.string.eip_cancel_connect_cancel), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ eipCommand(EIP.ACTION_STOP_EIP);
+ mEipStartPending = false;
+ }
+ })
+ .setNegativeButton(getResources().getString(R.string.eip_cancel_connect_false), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ eipAutoSwitched = true;
+ eipSwitch.setChecked(true);
+ eipAutoSwitched = false;
+ }
+ })
+ .show();
+ } else {
+ eipCommand(EIP.ACTION_STOP_EIP);
+ }
+ }
+ }
+ else {
+ Dashboard dashboard = (Dashboard)getActivity();
+ Bundle waiting_on_login = new Bundle();
+ waiting_on_login.putBoolean(IS_EIP_PENDING, true);
+ dashboard.logInDialog(getActivity().getCurrentFocus(), waiting_on_login);
+ }
+ }
+ else {
+ if(!eipSwitch.isChecked())
+ eipStatus.setText(R.string.state_noprocess);
+ }
+ eipAutoSwitched = true;
+ }
+
+
+
+ /**
+ * Send a command to EIP
+ *
+ * @param action A valid String constant from EIP class representing an Intent
+ * filter for the EIP class
+ */
+ private void eipCommand(String action){
+ // TODO validate "action"...how do we get the list of intent-filters for a class via Android API?
+ Intent vpnIntent = new Intent(action);
+ vpnIntent.putExtra(EIP.RECEIVER_TAG, mEIPReceiver);
+ getActivity().startService(vpnIntent);
+ }
+
+ @Override
+ public void updateState(final String state, final String logmessage, final int localizedResId) {
+ // Note: "states" are not organized anywhere...collected state strings:
+ // NOPROCESS,NONETWORK,BYTECOUNT,AUTH_FAILED + some parsing thing ( WAIT(?),AUTH,GET_CONFIG,ASSIGN_IP,CONNECTED,SIGINT )
+ getActivity().runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (eipStatus != null) {
+ boolean switchState = true;
+ String statusMessage = "";
+ String prefix = getString(localizedResId);
+ if (state.equals("CONNECTED")){
+
+ statusMessage = getString(R.string.eip_state_connected);
+ getActivity().findViewById(R.id.eipProgress).setVisibility(View.GONE);
+ mEipStartPending = false;
+ } else if (state.equals("BYTECOUNT")) {
+ statusMessage = getString(R.string.eip_state_connected); getActivity().findViewById(R.id.eipProgress).setVisibility(View.GONE);
+ mEipStartPending = false;
+
+ } else if ( (state.equals("NOPROCESS") && !mEipStartPending ) || state.equals("EXITING") && !mEipStartPending || state.equals("FATAL")) {
+ statusMessage = getString(R.string.eip_state_not_connected);
+ getActivity().findViewById(R.id.eipProgress).setVisibility(View.GONE);
+ mEipStartPending = false;
+ switchState = false;
+ } else if (state.equals("NOPROCESS")){
+ statusMessage = logmessage;
+ } else if (state.equals("ASSIGN_IP")){ //don't show assigning message in eipStatus
+ statusMessage = (String) eipStatus.getText();
+ }
+ else {
+ statusMessage = prefix + " " + logmessage;
+ }
+
+ eipAutoSwitched = true;
+ eipSwitch.setChecked(switchState);
+ eipAutoSwitched = false;
+ eipStatus.setText(statusMessage);
+ }
+ }
+ });
+ }
+
+
+ /**
+ * Inner class for handling messages related to EIP status and control requests
+ *
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ */
+ protected class EIPReceiver extends ResultReceiver {
+
+ protected EIPReceiver(Handler handler){
+ super(handler);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ super.onReceiveResult(resultCode, resultData);
+
+ String request = resultData.getString(EIP.REQUEST_TAG);
+ boolean checked = false;
+
+ if (request == EIP.ACTION_IS_EIP_RUNNING) {
+ switch (resultCode){
+ case Activity.RESULT_OK:
+ checked = true;
+ break;
+ case Activity.RESULT_CANCELED:
+ checked = false;
+ break;
+ }
+ } else if (request == EIP.ACTION_START_EIP) {
+ switch (resultCode){
+ case Activity.RESULT_OK:
+ checked = true;
+ break;
+ case Activity.RESULT_CANCELED:
+ checked = false;
+ eipFragment.findViewById(R.id.eipProgress).setVisibility(View.GONE);
+ break;
+ }
+ } else if (request == EIP.ACTION_STOP_EIP) {
+ switch (resultCode){
+ case Activity.RESULT_OK:
+ checked = false;
+ break;
+ case Activity.RESULT_CANCELED:
+ checked = true;
+ break;
+ }
+ } else if (request == EIP.EIP_NOTIFICATION) {
+ switch (resultCode){
+ case Activity.RESULT_OK:
+ checked = true;
+ break;
+ case Activity.RESULT_CANCELED:
+ checked = false;
+ break;
+ }
+ }
+
+ eipAutoSwitched = true;
+ eipSwitch.setChecked(checked);
+ eipAutoSwitched = false;
+ }
+ }
+
+
+ public static EIPReceiver getReceiver() {
+ return mEIPReceiver;
+ }
+
+ public static boolean isEipSwitchChecked() {
+ return eipSwitch.isChecked();
+ }
+
+ public void checkEipSwitch(boolean checked) {
+ eipSwitch.setChecked(checked);
+ onCheckedChanged(eipSwitch, checked);
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/LeapHttpClient.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/LeapHttpClient.java
new file mode 100644
index 00000000..885b5105
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/LeapHttpClient.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import java.security.KeyStore;
+
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.SingleClientConnManager;
+import android.content.Context;
+
+/**
+ * Implements an HTTP client, enabling LEAP Android app to manage its own runtime keystore or bypass default Android security measures.
+ *
+ * @author rafa
+ *
+ */
+public class LeapHttpClient extends DefaultHttpClient {
+
+ private static LeapHttpClient client;
+
+ /**
+ * If the class scope client is null, it creates one and imports, if existing, the main certificate from Shared Preferences.
+ * @param context
+ * @return the new client.
+ */
+ public static LeapHttpClient getInstance(String cert_string) {
+ if(client == null) {
+ if(cert_string != null) {
+ ConfigHelper.addTrustedCertificate("provider_ca_certificate", cert_string);
+ }
+ }
+ return client;
+ }
+
+ @Override
+ protected ClientConnectionManager createClientConnectionManager() {
+ SchemeRegistry registry = new SchemeRegistry();
+ registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
+ registry.register(new Scheme("https", newSslSocketFactory(), 443));
+
+ return new SingleClientConnManager(getParams(), registry);
+ }
+
+ /**
+ * Uses keystore from ConfigHelper for the SSLSocketFactory.
+ * @return
+ */
+ private SSLSocketFactory newSslSocketFactory() {
+ try {
+ KeyStore trusted = ConfigHelper.getKeystore();
+ SSLSocketFactory sf = new SSLSocketFactory(trusted);
+
+ return sf;
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java
new file mode 100644
index 00000000..a317d95e
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java
@@ -0,0 +1,341 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import org.jboss.security.srp.SRPParameters;
+
+/**
+ * Implements all SRP algorithm logic.
+ *
+ * It's derived from JBoss implementation, with adjustments to make it work with LEAP platform.
+ *
+ * @author parmegv
+ *
+ */
+public class LeapSRPSession {
+
+ private static String token = "";
+
+ final public static String SALT = "salt";
+ final public static String M1 = "M1";
+ final public static String M2 = "M2";
+ final public static String TOKEN = "token";
+ final public static String AUTHORIZATION_HEADER= "Authorization";
+
+ private SRPParameters params;
+ private String username;
+ private String password;
+ private BigInteger N;
+ private byte[] N_bytes;
+ private BigInteger g;
+ private BigInteger x;
+ private BigInteger v;
+ private BigInteger a;
+ private BigInteger A;
+ private byte[] K;
+ private SecureRandom pseudoRng;
+ /** The M1 = H(H(N) xor H(g) | H(U) | s | A | B | K) hash */
+ private MessageDigest clientHash;
+ /** The M2 = H(A | M | K) hash */
+ private MessageDigest serverHash;
+
+ private static int A_LEN;
+
+ /** Creates a new SRP server session object from the username, password
+ verifier,
+ @param username, the user ID
+ @param password, the user clear text password
+ @param params, the SRP parameters for the session
+ */
+ public LeapSRPSession(String username, String password, SRPParameters params)
+ {
+ this(username, password, params, null);
+ }
+
+ /** Creates a new SRP server session object from the username, password
+ verifier,
+ @param username, the user ID
+ @param password, the user clear text password
+ @param params, the SRP parameters for the session
+ @param abytes, the random exponent used in the A public key
+ */
+ public LeapSRPSession(String username, String password, SRPParameters params,
+ byte[] abytes) {
+ this.params = params;
+ this.g = new BigInteger(1, params.g);
+ N_bytes = ConfigHelper.trim(params.N);
+ this.N = new BigInteger(1, N_bytes);
+ this.username = username;
+ this.password = password;
+
+ try {
+ pseudoRng = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ if( abytes != null ) {
+ A_LEN = 8*abytes.length;
+ /* TODO Why did they put this condition?
+ if( 8*abytes.length != A_LEN )
+ throw new IllegalArgumentException("The abytes param must be "
+ +(A_LEN/8)+" in length, abytes.length="+abytes.length);
+ */
+ this.a = new BigInteger(abytes);
+ }
+ else
+ A_LEN = 64;
+
+ serverHash = newDigest();
+ clientHash = newDigest();
+ }
+
+ /**
+ * Calculates the parameter x of the SRP-6a algorithm.
+ * @param username
+ * @param password
+ * @param salt the salt of the user
+ * @return x
+ */
+ public byte[] calculatePasswordHash(String username, String password, byte[] salt)
+ {
+ //password = password.replaceAll("\\\\", "\\\\\\\\");
+ // Calculate x = H(s | H(U | ':' | password))
+ MessageDigest x_digest = newDigest();
+ // Try to convert the username to a byte[] using ISO-8859-1
+ byte[] user = null;
+ byte[] password_bytes = null;
+ byte[] colon = {};
+ String encoding = "ISO-8859-1";
+ try {
+ user = ConfigHelper.trim(username.getBytes(encoding));
+ colon = ConfigHelper.trim(":".getBytes(encoding));
+ password_bytes = ConfigHelper.trim(password.getBytes(encoding));
+ }
+ catch(UnsupportedEncodingException e) {
+ // Use the default platform encoding
+ user = ConfigHelper.trim(username.getBytes());
+ colon = ConfigHelper.trim(":".getBytes());
+ password_bytes = ConfigHelper.trim(password.getBytes());
+ }
+
+ // Build the hash
+ x_digest.update(user);
+ x_digest.update(colon);
+ x_digest.update(password_bytes);
+ byte[] h = x_digest.digest();
+
+ x_digest.reset();
+ x_digest.update(salt);
+ x_digest.update(h);
+ byte[] x_digest_bytes = x_digest.digest();
+
+ return x_digest_bytes;
+ }
+
+ /**
+ * Calculates the parameter V of the SRP-6a algorithm.
+ * @param k_string constant k predefined by the SRP server implementation.
+ * @return the value of V
+ */
+ private BigInteger calculateV(String k_string) {
+ BigInteger k = new BigInteger(k_string, 16);
+ BigInteger v = k.multiply(g.modPow(x, N)); // g^x % N
+ return v;
+ }
+
+ /**
+ * Calculates the trimmed xor from two BigInteger numbers
+ * @param b1 the positive source to build first BigInteger
+ * @param b2 the positive source to build second BigInteger
+ * @param length
+ * @return
+ */
+ public byte[] xor(byte[] b1, byte[] b2)
+ {
+ //TODO Check if length matters in the order, when b2 is smaller than b1 or viceversa
+ byte[] xor_digest = new BigInteger(1, b1).xor(new BigInteger(1, b2)).toByteArray();
+ return ConfigHelper.trim(xor_digest);
+ }
+
+ /**
+ * @returns The exponential residue (parameter A) to be sent to the server.
+ */
+ public byte[] exponential() {
+ byte[] Abytes = null;
+ if(A == null) {
+ /* If the random component of A has not been specified use a random
+ number */
+ if( a == null ) {
+ BigInteger one = BigInteger.ONE;
+ do {
+ a = new BigInteger(A_LEN, pseudoRng);
+ } while(a.compareTo(one) <= 0);
+ }
+ A = g.modPow(a, N);
+ Abytes = ConfigHelper.trim(A.toByteArray());
+ }
+ return Abytes;
+ }
+
+ /**
+ * Calculates the parameter M1, to be sent to the SRP server.
+ * It also updates hashes of client and server for further calculations in other methods.
+ * It uses a predefined k.
+ * @param salt_bytes
+ * @param Bbytes the parameter received from the server, in bytes
+ * @return the parameter M1
+ * @throws NoSuchAlgorithmException
+ */
+ public byte[] response(byte[] salt_bytes, byte[] Bbytes) throws NoSuchAlgorithmException {
+ // Calculate x = H(s | H(U | ':' | password))
+ byte[] M1 = null;
+ if(new BigInteger(1, Bbytes).mod(new BigInteger(1, N_bytes)) != BigInteger.ZERO) {
+ byte[] xb = calculatePasswordHash(username, password, ConfigHelper.trim(salt_bytes));
+ this.x = new BigInteger(1, xb);
+
+ // Calculate v = kg^x mod N
+ String k_string = "bf66c44a428916cad64aa7c679f3fd897ad4c375e9bbb4cbf2f5de241d618ef0";
+ this.v = calculateV(k_string);
+
+ // H(N)
+ byte[] digest_of_n = newDigest().digest(N_bytes);
+
+ // H(g)
+ byte[] digest_of_g = newDigest().digest(params.g);
+
+ // clientHash = H(N) xor H(g)
+ byte[] xor_digest = xor(digest_of_n, digest_of_g);
+ clientHash.update(xor_digest);
+
+ // clientHash = H(N) xor H(g) | H(U)
+ byte[] username_digest = newDigest().digest(ConfigHelper.trim(username.getBytes()));
+ username_digest = ConfigHelper.trim(username_digest);
+ clientHash.update(username_digest);
+
+ // clientHash = H(N) xor H(g) | H(U) | s
+ clientHash.update(ConfigHelper.trim(salt_bytes));
+
+ K = null;
+
+ // clientHash = H(N) xor H(g) | H(U) | A
+ byte[] Abytes = ConfigHelper.trim(A.toByteArray());
+ clientHash.update(Abytes);
+
+ // clientHash = H(N) xor H(g) | H(U) | s | A | B
+ Bbytes = ConfigHelper.trim(Bbytes);
+ clientHash.update(Bbytes);
+
+ // Calculate S = (B - kg^x) ^ (a + u * x) % N
+ BigInteger S = calculateS(Bbytes);
+ byte[] S_bytes = ConfigHelper.trim(S.toByteArray());
+
+ // K = SessionHash(S)
+ String hash_algorithm = params.hashAlgorithm;
+ MessageDigest sessionDigest = MessageDigest.getInstance(hash_algorithm);
+ K = ConfigHelper.trim(sessionDigest.digest(S_bytes));
+
+ // clientHash = H(N) xor H(g) | H(U) | A | B | K
+ clientHash.update(K);
+
+ M1 = ConfigHelper.trim(clientHash.digest());
+
+ // serverHash = Astr + M + K
+ serverHash.update(Abytes);
+ serverHash.update(M1);
+ serverHash.update(K);
+
+ }
+ return M1;
+ }
+
+ /**
+ * It calculates the parameter S used by response() to obtain session hash K.
+ * @param Bbytes the parameter received from the server, in bytes
+ * @return the parameter S
+ */
+ private BigInteger calculateS(byte[] Bbytes) {
+ byte[] Abytes = ConfigHelper.trim(A.toByteArray());
+ Bbytes = ConfigHelper.trim(Bbytes);
+ byte[] u_bytes = getU(Abytes, Bbytes);
+
+ BigInteger B = new BigInteger(1, Bbytes);
+ BigInteger u = new BigInteger(1, u_bytes);
+
+ BigInteger B_minus_v = B.subtract(v);
+ BigInteger a_ux = a.add(u.multiply(x));
+ BigInteger S = B_minus_v.modPow(a_ux, N);
+ return S;
+ }
+
+ /**
+ * It calculates the parameter u used by calculateS to obtain S.
+ * @param Abytes the exponential residue sent to the server
+ * @param Bbytes the parameter received from the server, in bytes
+ * @return
+ */
+ public byte[] getU(byte[] Abytes, byte[] Bbytes) {
+ MessageDigest u_digest = newDigest();
+ u_digest.update(ConfigHelper.trim(Abytes));
+ u_digest.update(ConfigHelper.trim(Bbytes));
+ byte[] u_digest_bytes = u_digest.digest();
+ return ConfigHelper.trim(new BigInteger(1, u_digest_bytes).toByteArray());
+ }
+
+ /**
+ * @param M2 The server's response to the client's challenge
+ * @returns True if and only if the server's response was correct.
+ */
+ public boolean verify(byte[] M2)
+ {
+ // M2 = H(A | M1 | K)
+ M2 = ConfigHelper.trim(M2);
+ byte[] myM2 = ConfigHelper.trim(serverHash.digest());
+ boolean valid = Arrays.equals(M2, myM2);
+ return valid;
+ }
+
+ protected static void setToken(String token) {
+ LeapSRPSession.token = token;
+ }
+
+ protected static String getToken() {
+ return token;
+ }
+
+ /**
+ * @return a new SHA-256 digest.
+ */
+ public MessageDigest newDigest()
+ {
+ MessageDigest md = null;
+ try {
+ md = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ return md;
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/LogInDialog.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/LogInDialog.java
new file mode 100644
index 00000000..a28c9049
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/LogInDialog.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import se.leap.bitmaskclient.R;
+import android.R.color;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.res.ColorStateList;
+import android.os.Bundle;
+import android.provider.CalendarContract.Colors;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.BounceInterpolator;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * Implements the log in dialog, currently without progress dialog.
+ *
+ * It returns to the previous fragment when finished, and sends username and password to the authenticate method.
+ *
+ * It also notifies the user if the password is not valid.
+ *
+ * @author parmegv
+ *
+ */
+public class LogInDialog extends DialogFragment {
+
+
+ final public static String TAG = "logInDialog";
+ final public static String VERB = "log in";
+ final public static String USERNAME = "username";
+ final public static String PASSWORD = "password";
+ final public static String USERNAME_MISSING = "username missing";
+ final public static String PASSWORD_INVALID_LENGTH = "password_invalid_length";
+
+ private static boolean is_eip_pending = false;
+
+ public AlertDialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View log_in_dialog_view = inflater.inflate(R.layout.log_in_dialog, null);
+
+ final TextView user_message = (TextView)log_in_dialog_view.findViewById(R.id.user_message);
+ if(getArguments() != null && getArguments().containsKey(getResources().getString(R.string.user_message))) {
+ user_message.setText(getArguments().getString(getResources().getString(R.string.user_message)));
+ } else {
+ user_message.setVisibility(View.GONE);
+ }
+
+ final EditText username_field = (EditText)log_in_dialog_view.findViewById(R.id.username_entered);
+ if(getArguments() != null && getArguments().containsKey(USERNAME)) {
+ String username = getArguments().getString(USERNAME);
+ username_field.setText(username);
+ }
+ if (getArguments() != null && getArguments().containsKey(USERNAME_MISSING)) {
+ username_field.setError(getResources().getString(R.string.username_ask));
+ }
+
+ final EditText password_field = (EditText)log_in_dialog_view.findViewById(R.id.password_entered);
+ if(!username_field.getText().toString().isEmpty() && password_field.isFocusable()) {
+ password_field.requestFocus();
+ }
+ if (getArguments() != null && getArguments().containsKey(PASSWORD_INVALID_LENGTH)) {
+ password_field.setError(getResources().getString(R.string.error_not_valid_password_user_message));
+ }
+ if(getArguments() != null && getArguments().getBoolean(EipServiceFragment.IS_EIP_PENDING, false)) {
+ is_eip_pending = true;
+ }
+
+
+ builder.setView(log_in_dialog_view)
+ .setPositiveButton(R.string.login_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ String username = username_field.getText().toString();
+ String password = password_field.getText().toString();
+ dialog.dismiss();
+ interface_with_Dashboard.authenticate(username, password);
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+
+ return builder.create();
+ }
+
+ /**
+ * Interface used to communicate LogInDialog with Dashboard.
+ *
+ * @author parmegv
+ *
+ */
+ public interface LogInDialogInterface {
+ /**
+ * Starts authentication process.
+ * @param username
+ * @param password
+ */
+ public void authenticate(String username, String password);
+ public void cancelAuthedEipOn();
+ }
+
+ LogInDialogInterface interface_with_Dashboard;
+
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance() {
+ LogInDialog dialog_fragment = new LogInDialog();
+ return dialog_fragment;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ interface_with_Dashboard = (LogInDialogInterface) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString()
+ + " must implement LogInDialogListener");
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if(is_eip_pending)
+ interface_with_Dashboard.cancelAuthedEipOn();
+ super.onCancel(dialog);
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/NewProviderDialog.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/NewProviderDialog.java
new file mode 100644
index 00000000..cf09c64b
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/NewProviderDialog.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import se.leap.bitmaskclient.R;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.Toast;
+
+/**
+ * Implements the new custom provider dialog.
+ *
+ * @author parmegv
+ *
+ */
+public class NewProviderDialog extends DialogFragment {
+
+ final public static String TAG = "newProviderDialog";
+
+ public interface NewProviderDialogInterface {
+ public void showAndSelectProvider(String url_provider, boolean danger_on);
+ }
+
+ NewProviderDialogInterface interface_with_ConfigurationWizard;
+
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance() {
+ NewProviderDialog dialog_fragment = new NewProviderDialog();
+ return dialog_fragment;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ interface_with_ConfigurationWizard = (NewProviderDialogInterface) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString()
+ + " must implement NoticeDialogListener");
+ }
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View new_provider_dialog_view = inflater.inflate(R.layout.new_provider_dialog, null);
+ final EditText url_input_field = (EditText)new_provider_dialog_view.findViewById(R.id.new_provider_url);
+ if(getArguments() != null && getArguments().containsKey(Provider.MAIN_URL)) {
+ url_input_field.setText(getArguments().getString(Provider.MAIN_URL));
+ }
+ final CheckBox danger_checkbox = (CheckBox)new_provider_dialog_view.findViewById(R.id.danger_checkbox);
+ if(getArguments() != null && getArguments().containsKey(ProviderItem.DANGER_ON)) {
+ danger_checkbox.setActivated(getArguments().getBoolean(ProviderItem.DANGER_ON));
+ }
+
+ builder.setView(new_provider_dialog_view)
+ .setMessage(R.string.introduce_new_provider)
+ .setPositiveButton(R.string.save, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ String entered_url = url_input_field.getText().toString().trim();
+ if(!entered_url.startsWith("https://")) {
+ if (entered_url.startsWith("http://")){
+ entered_url = entered_url.substring("http://".length());
+ }
+ entered_url = "https://".concat(entered_url);
+ }
+ boolean danger_on = danger_checkbox.isChecked();
+ if(validURL(entered_url)) {
+ interface_with_ConfigurationWizard.showAndSelectProvider(entered_url, danger_on);
+ Toast.makeText(getActivity().getApplicationContext(), R.string.valid_url_entered, Toast.LENGTH_LONG).show();
+ } else {
+ url_input_field.setText("");
+ danger_checkbox.setChecked(false);
+ Toast.makeText(getActivity().getApplicationContext(), R.string.not_valid_url_entered, Toast.LENGTH_LONG).show();;
+ }
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ // Create the AlertDialog object and return it
+ return builder.create();
+ }
+
+ /**
+ * Checks if the entered url is valid or not.
+ * @param entered_url
+ * @return true if it's not empty nor contains only the protocol.
+ */
+ boolean validURL(String entered_url) {
+ //return !entered_url.isEmpty() && entered_url.matches("http[s]?://.+") && !entered_url.replaceFirst("http[s]?://", "").isEmpty();
+ return android.util.Patterns.WEB_URL.matcher(entered_url).matches();
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/PRNGFixes.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/PRNGFixes.java
new file mode 100644
index 00000000..a046f01f
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/PRNGFixes.java
@@ -0,0 +1,338 @@
+package se.leap.bitmaskclient;
+
+/*
+ * This software is provided 'as-is', without any express or implied
+ * warranty. In no event will Google be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, as long as the origin is not misrepresented.
+ *
+ * Source: http://android-developers.blogspot.de/2013/08/some-securerandom-thoughts.html
+ */
+
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.SecureRandomSpi;
+import java.security.Security;
+
+/**
+ * Fixes for the output of the default PRNG having low entropy.
+ *
+ * The fixes need to be applied via {@link #apply()} before any use of Java
+ * Cryptography Architecture primitives. A good place to invoke them is in the
+ * application's {@code onCreate}.
+ */
+public final class PRNGFixes {
+
+ private static final int VERSION_CODE_JELLY_BEAN = 16;
+ private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
+ private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
+ getBuildFingerprintAndDeviceSerial();
+
+ /** Hidden constructor to prevent instantiation. */
+ private PRNGFixes() {}
+
+ /**
+ * Applies all fixes.
+ *
+ * @throws SecurityException if a fix is needed but could not be applied.
+ */
+ public static void apply() {
+ applyOpenSSLFix();
+ installLinuxPRNGSecureRandom();
+ }
+
+ /**
+ * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
+ * fix is not needed.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void applyOpenSSLFix() throws SecurityException {
+ if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
+ || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
+ // No need to apply the fix
+ return;
+ }
+
+ try {
+ // Mix in the device- and invocation-specific seed.
+ Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_seed", byte[].class)
+ .invoke(null, generateSeed());
+
+ // Mix output of Linux PRNG into OpenSSL's PRNG
+ int bytesRead = (Integer) Class.forName(
+ "org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_load_file", String.class, long.class)
+ .invoke(null, "/dev/urandom", 1024);
+ if (bytesRead != 1024) {
+ throw new IOException(
+ "Unexpected number of bytes read from Linux PRNG: "
+ + bytesRead);
+ }
+ } catch (Exception e) {
+ throw new SecurityException("Failed to seed OpenSSL PRNG", e);
+ }
+ }
+
+ /**
+ * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
+ * default. Does nothing if the implementation is already the default or if
+ * there is not need to install the implementation.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void installLinuxPRNGSecureRandom()
+ throws SecurityException {
+ if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
+ // No need to apply the fix
+ return;
+ }
+
+ // Install a Linux PRNG-based SecureRandom implementation as the
+ // default, if not yet installed.
+ Provider[] secureRandomProviders =
+ Security.getProviders("SecureRandom.SHA1PRNG");
+ if ((secureRandomProviders == null)
+ || (secureRandomProviders.length < 1)
+ || (!LinuxPRNGSecureRandomProvider.class.equals(
+ secureRandomProviders[0].getClass()))) {
+ Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
+ }
+
+ // Assert that new SecureRandom() and
+ // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
+ // by the Linux PRNG-based SecureRandom implementation.
+ SecureRandom rng1 = new SecureRandom();
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng1.getProvider().getClass())) {
+ throw new SecurityException(
+ "new SecureRandom() backed by wrong Provider: "
+ + rng1.getProvider().getClass());
+ }
+
+ SecureRandom rng2;
+ try {
+ rng2 = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("SHA1PRNG not available", e);
+ }
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng2.getProvider().getClass())) {
+ throw new SecurityException(
+ "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ + " Provider: " + rng2.getProvider().getClass());
+ }
+ }
+
+ /**
+ * {@code Provider} of {@code SecureRandom} engines which pass through
+ * all requests to the Linux PRNG.
+ */
+ private static class LinuxPRNGSecureRandomProvider extends Provider {
+
+ public LinuxPRNGSecureRandomProvider() {
+ super("LinuxPRNG",
+ 1.0,
+ "A Linux-specific random number provider that uses"
+ + " /dev/urandom");
+ // Although /dev/urandom is not a SHA-1 PRNG, some apps
+ // explicitly request a SHA1PRNG SecureRandom and we thus need to
+ // prevent them from getting the default implementation whose output
+ // may have low entropy.
+ put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
+ put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
+ }
+ }
+
+ /**
+ * {@link SecureRandomSpi} which passes all requests to the Linux PRNG
+ * ({@code /dev/urandom}).
+ */
+ public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
+
+ /*
+ * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
+ * are passed through to the Linux PRNG (/dev/urandom). Instances of
+ * this class seed themselves by mixing in the current time, PID, UID,
+ * build fingerprint, and hardware serial number (where available) into
+ * Linux PRNG.
+ *
+ * Concurrency: Read requests to the underlying Linux PRNG are
+ * serialized (on sLock) to ensure that multiple threads do not get
+ * duplicated PRNG output.
+ */
+
+ private static final File URANDOM_FILE = new File("/dev/urandom");
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Input stream for reading from Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static DataInputStream sUrandomIn;
+
+ /**
+ * Output stream for writing to Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static OutputStream sUrandomOut;
+
+ /**
+ * Whether this engine instance has been seeded. This is needed because
+ * each instance needs to seed itself if the client does not explicitly
+ * seed it.
+ */
+ private boolean mSeeded;
+
+ @Override
+ protected void engineSetSeed(byte[] bytes) {
+ try {
+ OutputStream out;
+ synchronized (sLock) {
+ out = getUrandomOutputStream();
+ }
+ out.write(bytes);
+ out.flush();
+ } catch (IOException e) {
+ // On a small fraction of devices /dev/urandom is not writable.
+ // Log and ignore.
+ Log.w(PRNGFixes.class.getSimpleName(),
+ "Failed to mix seed into " + URANDOM_FILE);
+ } finally {
+ mSeeded = true;
+ }
+ }
+
+ @Override
+ protected void engineNextBytes(byte[] bytes) {
+ if (!mSeeded) {
+ // Mix in the device- and invocation-specific seed.
+ engineSetSeed(generateSeed());
+ }
+
+ try {
+ DataInputStream in;
+ synchronized (sLock) {
+ in = getUrandomInputStream();
+ }
+ synchronized (in) {
+ in.readFully(bytes);
+ }
+ } catch (IOException e) {
+ throw new SecurityException(
+ "Failed to read from " + URANDOM_FILE, e);
+ }
+ }
+
+ @Override
+ protected byte[] engineGenerateSeed(int size) {
+ byte[] seed = new byte[size];
+ engineNextBytes(seed);
+ return seed;
+ }
+
+ private DataInputStream getUrandomInputStream() {
+ synchronized (sLock) {
+ if (sUrandomIn == null) {
+ // NOTE: Consider inserting a BufferedInputStream between
+ // DataInputStream and FileInputStream if you need higher
+ // PRNG output performance and can live with future PRNG
+ // output being pulled into this process prematurely.
+ try {
+ sUrandomIn = new DataInputStream(
+ new FileInputStream(URANDOM_FILE));
+ } catch (IOException e) {
+ throw new SecurityException("Failed to open "
+ + URANDOM_FILE + " for reading", e);
+ }
+ }
+ return sUrandomIn;
+ }
+ }
+
+ private OutputStream getUrandomOutputStream() throws IOException {
+ synchronized (sLock) {
+ if (sUrandomOut == null) {
+ sUrandomOut = new FileOutputStream(URANDOM_FILE);
+ }
+ return sUrandomOut;
+ }
+ }
+ }
+
+ /**
+ * Generates a device- and invocation-specific seed to be mixed into the
+ * Linux PRNG.
+ */
+ private static byte[] generateSeed() {
+ try {
+ ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
+ DataOutputStream seedBufferOut =
+ new DataOutputStream(seedBuffer);
+ seedBufferOut.writeLong(System.currentTimeMillis());
+ seedBufferOut.writeLong(System.nanoTime());
+ seedBufferOut.writeInt(Process.myPid());
+ seedBufferOut.writeInt(Process.myUid());
+ seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
+ seedBufferOut.close();
+ return seedBuffer.toByteArray();
+ } catch (IOException e) {
+ throw new SecurityException("Failed to generate seed", e);
+ }
+ }
+
+ /**
+ * Gets the hardware serial number of this device.
+ *
+ * @return serial number or {@code null} if not available.
+ */
+ private static String getDeviceSerialNumber() {
+ // We're using the Reflection API because Build.SERIAL is only available
+ // since API Level 9 (Gingerbread, Android 2.3).
+ try {
+ return (String) Build.class.getField("SERIAL").get(null);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private static byte[] getBuildFingerprintAndDeviceSerial() {
+ StringBuilder result = new StringBuilder();
+ String fingerprint = Build.FINGERPRINT;
+ if (fingerprint != null) {
+ result.append(fingerprint);
+ }
+ String serial = getDeviceSerialNumber();
+ if (serial != null) {
+ result.append(serial);
+ }
+ try {
+ return result.toString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported");
+ }
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/Provider.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/Provider.java
new file mode 100644
index 00000000..216f4261
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/Provider.java
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Locale;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.app.Activity;
+import android.content.SharedPreferences;
+
+/**
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ *
+ */
+public final class Provider implements Serializable {
+
+ private static final long serialVersionUID = 6003835972151761353L;
+
+ private static Provider instance = null;
+
+ // We'll access our preferences here
+ private static SharedPreferences preferences = null;
+ // Represents our Provider's provider.json
+ private static JSONObject definition = null;
+
+ final public static String
+ API_URL = "api_uri",
+ API_VERSION = "api_version",
+ ALLOW_REGISTRATION = "allow_registration",
+ API_RETURN_SERIAL = "serial",
+ SERVICE = "service",
+ KEY = "provider",
+ CA_CERT = "ca_cert",
+ NAME = "name",
+ DESCRIPTION = "description",
+ DOMAIN = "domain",
+ MAIN_URL = "main_url",
+ DOT_JSON_URL = "provider_json_url"
+ ;
+
+ // Array of what API versions we understand
+ protected static final String[] API_VERSIONS = {"1"}; // I assume we might encounter arbitrary version "numbers"
+ // Some API pieces we want to know about
+ private static final String API_TERM_SERVICES = "services";
+ private static final String API_TERM_NAME = "name";
+ private static final String API_TERM_DOMAIN = "domain";
+ private static final String API_TERM_DEFAULT_LANGUAGE = "default_language";
+ protected static final String[] API_EIP_TYPES = {"openvpn"};
+
+ private static final String PREFS_EIP_NAME = null;
+
+
+
+ // What, no individual fields?! We're going to gamble on org.json.JSONObject and JSONArray
+ // Supporting multiple API versions will probably break this paradigm,
+ // Forcing me to write a real constructor and rewrite getters/setters
+ // Also will refactor if i'm instantiating the same local variables all the time
+
+ /**
+ *
+ */
+ private Provider() {}
+
+ protected static Provider getInstance(){
+ if(instance==null){
+ instance = new Provider();
+ }
+ return instance;
+ }
+
+ protected void init(Activity activity) {
+
+ // Load our preferences from SharedPreferences
+ // If there's nothing there, we will end up returning a rather empty object
+ // to whoever called getInstance() and they can run the First Run Wizard
+ //preferences = context.getgetPreferences(0); // 0 == MODE_PRIVATE, but we don't extend Android's classes...
+
+ // Load SharedPreferences
+ preferences = activity.getSharedPreferences(Dashboard.SHARED_PREFERENCES,Context.MODE_PRIVATE);
+ // Inflate our provider.json data
+ try {
+ definition = new JSONObject( preferences.getString(Provider.KEY, "") );
+ } catch (JSONException e) {
+ // TODO: handle exception
+
+ // FIXME!! We want "real" data!!
+ }
+ }
+
+ protected String getDomain(){
+ String domain = "Null";
+ try {
+ domain = definition.getString(API_TERM_DOMAIN);
+ } catch (JSONException e) {
+ domain = "Null";
+ e.printStackTrace();
+ }
+ return domain;
+ }
+
+ protected String getName(){
+ // Should we pass the locale in, or query the system here?
+ String lang = Locale.getDefault().getLanguage();
+ String name = "Null"; // Should it actually /be/ null, for error conditions?
+ try {
+ name = definition.getJSONObject(API_TERM_NAME).getString(lang);
+ } catch (JSONException e) {
+ // TODO: Nesting try/catch blocks? Crazy
+ // Maybe you should actually handle exception?
+ try {
+ name = definition.getJSONObject(API_TERM_NAME).getString( definition.getString(API_TERM_DEFAULT_LANGUAGE) );
+ } catch (JSONException e2) {
+ // TODO: Will you handle the exception already?
+ }
+ }
+
+ return name;
+ }
+
+ protected String getDescription(){
+ String lang = Locale.getDefault().getLanguage();
+ String desc = null;
+ try {
+ desc = definition.getJSONObject("description").getString(lang);
+ } catch (JSONException e) {
+ // TODO: handle exception!!
+ try {
+ desc = definition.getJSONObject("description").getString( definition.getString("default_language") );
+ } catch (JSONException e2) {
+ // TODO: i can't believe you're doing it again!
+ }
+ }
+
+ return desc;
+ }
+
+ protected boolean hasEIP() {
+ JSONArray services = null;
+ try {
+ services = definition.getJSONArray(API_TERM_SERVICES); // returns ["openvpn"]
+ } catch (Exception e) {
+ // TODO: handle exception
+ }
+ for (int i=0;i<API_EIP_TYPES.length+1;i++){
+ try {
+ // Walk the EIP types array looking for matches in provider's service definitions
+ if ( Arrays.asList(API_EIP_TYPES).contains( services.getString(i) ) )
+ return true;
+ } catch (NullPointerException e){
+ e.printStackTrace();
+ return false;
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ }
+ }
+ return false;
+ }
+
+ protected String getEIPType() {
+ // FIXME!!!!! We won't always be providing /only/ OpenVPN, will we?
+ // This will have to hook into some saved choice of EIP transport
+ if ( instance.hasEIP() )
+ return "OpenVPN";
+ else
+ return null;
+ }
+
+ protected JSONObject getEIP() {
+ // FIXME!!!!! We won't always be providing /only/ OpenVPN, will we?
+ // This will have to hook into some saved choice of EIP transport, cluster, gateway
+ // with possible "choose at random" preference
+ if ( instance.hasEIP() ){
+ // TODO Might need an EIP class, but we've only got OpenVPN type right now,
+ // and only one gateway for our only provider...
+ // TODO We'll try to load from preferences, have to call ProviderAPI if we've got nothin...
+ JSONObject eipObject = null;
+ try {
+ eipObject = new JSONObject( preferences.getString(PREFS_EIP_NAME, "") );
+ } catch (JSONException e) {
+ // TODO ConfigHelper.rescueJSON()
+ // Still nothing?
+ // TODO ProviderAPI.getEIP()
+ e.printStackTrace();
+ }
+
+ return eipObject;
+ } else
+ return null;
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderAPI.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
new file mode 100644
index 00000000..75ef511d
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
@@ -0,0 +1,875 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import java.io.DataOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+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.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Scanner;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.client.ClientProtocolException;
+import org.jboss.security.srp.SRPParameters;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import android.app.IntentService;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Base64;
+import android.util.Log;
+
+
+/**
+ * Implements HTTP api methods used to manage communications with the provider server.
+ *
+ * It's an IntentService because it downloads data from the Internet, so it operates in the background.
+ *
+ * @author parmegv
+ * @author MeanderingCode
+ *
+ */
+public class ProviderAPI extends IntentService {
+
+ private Handler mHandler;
+
+ final public static String
+ SET_UP_PROVIDER = "setUpProvider",
+ DOWNLOAD_NEW_PROVIDER_DOTJSON = "downloadNewProviderDotJSON",
+ SRP_REGISTER = "srpRegister",
+ SRP_AUTH = "srpAuth",
+ LOG_OUT = "logOut",
+ DOWNLOAD_CERTIFICATE = "downloadUserAuthedCertificate",
+ PARAMETERS = "parameters",
+ RESULT_KEY = "result",
+ RECEIVER_KEY = "receiver",
+ SESSION_ID_COOKIE_KEY = "session_id_cookie_key",
+ SESSION_ID_KEY = "session_id",
+ ERRORS = "errors",
+ UPDATE_PROGRESSBAR = "update_progressbar",
+ CURRENT_PROGRESS = "current_progress",
+ TAG = "provider_api_tag"
+ ;
+
+ final public static int
+ CUSTOM_PROVIDER_ADDED = 0,
+ SRP_AUTHENTICATION_SUCCESSFUL = 3,
+ SRP_AUTHENTICATION_FAILED = 4,
+ SRP_REGISTRATION_SUCCESSFUL = 5,
+ SRP_REGISTRATION_FAILED = 6,
+ LOGOUT_SUCCESSFUL = 7,
+ LOGOUT_FAILED = 8,
+ CORRECTLY_DOWNLOADED_CERTIFICATE = 9,
+ INCORRECTLY_DOWNLOADED_CERTIFICATE = 10,
+ PROVIDER_OK = 11,
+ PROVIDER_NOK = 12,
+ CORRECTLY_DOWNLOADED_ANON_CERTIFICATE = 13,
+ INCORRECTLY_DOWNLOADED_ANON_CERTIFICATE = 14
+ ;
+
+ private static boolean
+ CA_CERT_DOWNLOADED = false,
+ PROVIDER_JSON_DOWNLOADED = false,
+ EIP_SERVICE_JSON_DOWNLOADED = false
+ ;
+
+ private static String last_provider_main_url;
+ private static boolean last_danger_on = false;
+ private static boolean setting_up_provider = true;
+
+ public static void stop() {
+ setting_up_provider = false;
+ }
+
+ public ProviderAPI() {
+ super("ProviderAPI");
+ Log.v("ClassName", "Provider API");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler();
+ CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) );
+ }
+
+ public static String lastProviderMainUrl() {
+ return last_provider_main_url;
+ }
+
+ public static boolean lastDangerOn() {
+ return last_danger_on;
+ }
+
+ private String formatErrorMessage(final int toast_string_id) {
+ return "{ \"" + ERRORS + "\" : \""+getResources().getString(toast_string_id)+"\" }";
+ }
+
+ @Override
+ protected void onHandleIntent(Intent command) {
+ final ResultReceiver receiver = command.getParcelableExtra(RECEIVER_KEY);
+ String action = command.getAction();
+ Bundle parameters = command.getBundleExtra(PARAMETERS);
+ setting_up_provider = true;
+
+ if(action.equalsIgnoreCase(SET_UP_PROVIDER)) {
+ Bundle result = setUpProvider(parameters);
+ if(setting_up_provider) {
+ if(result.getBoolean(RESULT_KEY)) {
+ receiver.send(PROVIDER_OK, result);
+ } else {
+ receiver.send(PROVIDER_NOK, result);
+ }
+ }
+ } else if (action.equalsIgnoreCase(SRP_AUTH)) {
+ Bundle session_id_bundle = authenticateBySRP(parameters);
+ if(session_id_bundle.getBoolean(RESULT_KEY)) {
+ receiver.send(SRP_AUTHENTICATION_SUCCESSFUL, session_id_bundle);
+ } else {
+ receiver.send(SRP_AUTHENTICATION_FAILED, session_id_bundle);
+ }
+ } else if (action.equalsIgnoreCase(LOG_OUT)) {
+ if(logOut(parameters)) {
+ receiver.send(LOGOUT_SUCCESSFUL, Bundle.EMPTY);
+ } else {
+ receiver.send(LOGOUT_FAILED, Bundle.EMPTY);
+ }
+ } else if (action.equalsIgnoreCase(DOWNLOAD_CERTIFICATE)) {
+ if(getNewCert(parameters)) {
+ receiver.send(CORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY);
+ } else {
+ receiver.send(INCORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY);
+ }
+ }
+ }
+
+ /**
+ * Starts the authentication process using SRP protocol.
+ *
+ * @param task containing: username, password and api url.
+ * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if authentication was successful.
+ */
+ private Bundle authenticateBySRP(Bundle task) {
+ Bundle session_id_bundle = new Bundle();
+ int progress = 0;
+
+ String username = (String) task.get(LogInDialog.USERNAME);
+ String password = (String) task.get(LogInDialog.PASSWORD);
+ if(validUserLoginData(username, password)) {
+
+ String authentication_server = (String) task.get(Provider.API_URL);
+
+ SRPParameters params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), ConfigHelper.G.toByteArray(), BigInteger.ZERO.toByteArray(), "SHA-256");
+ LeapSRPSession client = new LeapSRPSession(username, password, params);
+ byte[] A = client.exponential();
+ broadcast_progress(progress++);
+ try {
+ JSONObject saltAndB = sendAToSRPServer(authentication_server, username, new BigInteger(1, A).toString(16));
+ if(saltAndB.length() > 0) {
+ String salt = saltAndB.getString(LeapSRPSession.SALT);
+ broadcast_progress(progress++);
+ byte[] Bbytes = new BigInteger(saltAndB.getString("B"), 16).toByteArray();
+ byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes);
+ if(M1 != null) {
+ broadcast_progress(progress++);
+ JSONObject session_idAndM2 = sendM1ToSRPServer(authentication_server, username, M1);
+ if(session_idAndM2.has(LeapSRPSession.M2) && client.verify((byte[])session_idAndM2.get(LeapSRPSession.M2))) {
+ session_id_bundle.putBoolean(RESULT_KEY, true);
+ broadcast_progress(progress++);
+ } else {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_bad_user_password_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ }
+ } else {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_srp_math_error_user_message));
+ }
+ broadcast_progress(progress++);
+ } else {
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_bad_user_password_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ }
+ } catch (ClientProtocolException e) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_client_http_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ } catch (IOException e) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_io_exception_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ } catch (JSONException e) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_json_exception_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ } catch (NoSuchAlgorithmException e) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_no_such_algorithm_exception_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ } catch (KeyManagementException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ } else {
+ if(!wellFormedPassword(password)) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ session_id_bundle.putBoolean(LogInDialog.PASSWORD_INVALID_LENGTH, true);
+ }
+ if(username.isEmpty()) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putBoolean(LogInDialog.USERNAME_MISSING, true);
+ }
+ }
+
+ return session_id_bundle;
+ }
+
+ /**
+ * Sets up an intent with the progress value passed as a parameter
+ * and sends it as a broadcast.
+ * @param progress
+ */
+ private void broadcast_progress(int progress) {
+ Intent intentUpdate = new Intent();
+ intentUpdate.setAction(UPDATE_PROGRESSBAR);
+ intentUpdate.addCategory(Intent.CATEGORY_DEFAULT);
+ intentUpdate.putExtra(CURRENT_PROGRESS, progress);
+ sendBroadcast(intentUpdate);
+ }
+
+ /**
+ * Validates parameters entered by the user to log in
+ * @param entered_username
+ * @param entered_password
+ * @return true if both parameters are present and the entered password length is greater or equal to eight (8).
+ */
+ private boolean validUserLoginData(String entered_username, String entered_password) {
+ return !(entered_username.isEmpty()) && wellFormedPassword(entered_password);
+ }
+
+ /**
+ * Validates a password
+ * @param entered_password
+ * @return true if the entered password length is greater or equal to eight (8).
+ */
+ private boolean wellFormedPassword(String entered_password) {
+ return entered_password.length() >= 8;
+ }
+
+ /**
+ * Sends an HTTP POST request to the authentication server with the SRP Parameter A.
+ * @param server_url
+ * @param username
+ * @param clientA First SRP parameter sent
+ * @return response from authentication server
+ * @throws ClientProtocolException
+ * @throws IOException
+ * @throws JSONException
+ * @throws CertificateException
+ * @throws NoSuchAlgorithmException
+ * @throws KeyStoreException
+ * @throws KeyManagementException
+ */
+ private JSONObject sendAToSRPServer(String server_url, String username, String clientA) throws ClientProtocolException, IOException, JSONException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
+ Map<String, String> parameters = new HashMap<String, String>();
+ parameters.put("login", username);
+ parameters.put("A", clientA);
+ return sendToServer(server_url + "/sessions.json", "POST", parameters);
+
+ /*HttpPost post = new HttpPost(server_url + "/sessions.json" + "?" + "login=" + username + "&&" + "A=" + clientA);
+ return sendToServer(post);*/
+ }
+
+ /**
+ * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M).
+ * @param server_url
+ * @param username
+ * @param m1 Second SRP parameter sent
+ * @return response from authentication server
+ * @throws ClientProtocolException
+ * @throws IOException
+ * @throws JSONException
+ * @throws CertificateException
+ * @throws NoSuchAlgorithmException
+ * @throws KeyStoreException
+ * @throws KeyManagementException
+ */
+ private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1) throws ClientProtocolException, IOException, JSONException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
+ Map<String, String> parameters = new HashMap<String, String>();
+ parameters.put("client_auth", new BigInteger(1, ConfigHelper.trim(m1)).toString(16));
+
+ //HttpPut put = new HttpPut(server_url + "/sessions/" + username +".json" + "?" + "client_auth" + "=" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16));
+ JSONObject json_response = sendToServer(server_url + "/sessions/" + username +".json", "PUT", parameters);
+
+ JSONObject session_idAndM2 = new JSONObject();
+ if(json_response.length() > 0) {
+ byte[] M2_not_trimmed = new BigInteger(json_response.getString(LeapSRPSession.M2), 16).toByteArray();
+ /*Cookie session_id_cookie = LeapHttpClient.getInstance(getApplicationContext()).getCookieStore().getCookies().get(0);
+ session_idAndM2.put(ConfigHelper.SESSION_ID_COOKIE_KEY, session_id_cookie.getName());
+ session_idAndM2.put(ConfigHelper.SESSION_ID_KEY, session_id_cookie.getValue());*/
+ session_idAndM2.put(LeapSRPSession.M2, ConfigHelper.trim(M2_not_trimmed));
+ CookieHandler.setDefault(null); // we don't need cookies anymore
+ String token = json_response.getString(LeapSRPSession.TOKEN);
+ LeapSRPSession.setToken(token);
+ }
+ return session_idAndM2;
+ }
+
+ /**
+ * Executes an HTTP request expecting a JSON response.
+ * @param url
+ * @param request_method
+ * @param parameters
+ * @return response from authentication server
+ * @throws IOException
+ * @throws JSONException
+ * @throws MalformedURLException
+ * @throws CertificateException
+ * @throws NoSuchAlgorithmException
+ * @throws KeyStoreException
+ * @throws KeyManagementException
+ */
+ private JSONObject sendToServer(String url, String request_method, Map<String, String> parameters) throws JSONException, MalformedURLException, IOException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
+ JSONObject json_response;
+ InputStream is = null;
+ HttpsURLConnection urlConnection = (HttpsURLConnection)new URL(url).openConnection();
+ urlConnection.setRequestMethod(request_method);
+ urlConnection.setChunkedStreamingMode(0);
+ urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory());
+ try {
+
+ DataOutputStream writer = new DataOutputStream(urlConnection.getOutputStream());
+ writer.writeBytes(formatHttpParameters(parameters));
+ writer.close();
+
+ is = urlConnection.getInputStream();
+ String plain_response = new Scanner(is).useDelimiter("\\A").next();
+ json_response = new JSONObject(plain_response);
+ } finally {
+ InputStream error_stream = urlConnection.getErrorStream();
+ if(error_stream != null) {
+ String error_response = new Scanner(error_stream).useDelimiter("\\A").next();
+ urlConnection.disconnect();
+ Log.d("Error", error_response);
+ json_response = new JSONObject(error_response);
+ if(!json_response.isNull(ERRORS) || json_response.has(ERRORS)) {
+ return new JSONObject();
+ }
+ }
+ }
+
+ return json_response;
+ }
+
+ private String formatHttpParameters(Map<String, String> parameters) throws UnsupportedEncodingException {
+ StringBuilder result = new StringBuilder();
+ boolean first = true;
+
+ Iterator<String> parameter_iterator = parameters.keySet().iterator();
+ while(parameter_iterator.hasNext()) {
+ if(first)
+ first = false;
+ else
+ result.append("&&");
+
+ String key = parameter_iterator.next();
+ String value = parameters.get(key);
+
+ result.append(URLEncoder.encode(key, "UTF-8"));
+ result.append("=");
+ result.append(URLEncoder.encode(value, "UTF-8"));
+ }
+
+ return result.toString();
+ }
+
+
+
+
+ /**
+ * Downloads a provider.json from a given URL, adding a new provider using the given name.
+ * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url.
+ * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful.
+ */
+ private Bundle setUpProvider(Bundle task) {
+ int progress = 0;
+ Bundle current_download = new Bundle();
+
+ if(task != null && task.containsKey(ProviderItem.DANGER_ON) && task.containsKey(Provider.MAIN_URL)) {
+ last_danger_on = task.getBoolean(ProviderItem.DANGER_ON);
+ last_provider_main_url = task.getString(Provider.MAIN_URL);
+ CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = EIP_SERVICE_JSON_DOWNLOADED = false;
+ }
+
+ if(!CA_CERT_DOWNLOADED)
+ current_download = downloadCACert(last_provider_main_url, last_danger_on);
+ if(CA_CERT_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) {
+ broadcast_progress(progress++);
+ CA_CERT_DOWNLOADED = true;
+ if(!PROVIDER_JSON_DOWNLOADED)
+ current_download = getAndSetProviderJson(last_provider_main_url);
+ if(PROVIDER_JSON_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) {
+ broadcast_progress(progress++);
+ PROVIDER_JSON_DOWNLOADED = true;
+ current_download = getAndSetEipServiceJson();
+ if(current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY)) {
+ broadcast_progress(progress++);
+ EIP_SERVICE_JSON_DOWNLOADED = true;
+ }
+ }
+ }
+
+ return current_download;
+ }
+
+ private Bundle downloadCACert(String provider_main_url, boolean danger_on) {
+ Bundle result = new Bundle();
+ String cert_string = downloadWithCommercialCA(provider_main_url + "/ca.crt", danger_on);
+
+ if(validCertificate(cert_string) && setting_up_provider) {
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(Provider.CA_CERT, cert_string).commit();
+ result.putBoolean(RESULT_KEY, true);
+ } else {
+ String reason_to_fail = pickErrorMessage(cert_string);
+ result.putString(ERRORS, reason_to_fail);
+ result.putBoolean(RESULT_KEY, false);
+ }
+
+ return result;
+ }
+
+
+ public static boolean caCertDownloaded() {
+ return CA_CERT_DOWNLOADED;
+ }
+
+ private boolean validCertificate(String cert_string) {
+ boolean result = false;
+ if(!ConfigHelper.checkErroneousDownload(cert_string)) {
+ X509Certificate certCert = ConfigHelper.parseX509CertificateFromString(cert_string);
+ try {
+ Base64.encodeToString( certCert.getEncoded(), Base64.DEFAULT);
+ result = true;
+ } catch (CertificateEncodingException e) {
+ Log.d(TAG, e.getLocalizedMessage());
+ }
+ }
+
+ return result;
+ }
+
+ private Bundle getAndSetProviderJson(String provider_main_url) {
+ Bundle result = new Bundle();
+
+ if(setting_up_provider) {
+ String provider_dot_json_string = downloadWithProviderCA(provider_main_url + "/provider.json", true);
+
+ try {
+ JSONObject provider_json = new JSONObject(provider_dot_json_string);
+ String name = provider_json.getString(Provider.NAME);
+ //TODO setProviderName(name);
+
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(Provider.KEY, provider_json.toString()).commit();
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(EIP.ALLOWED_ANON, provider_json.getJSONObject(Provider.SERVICE).getBoolean(EIP.ALLOWED_ANON)).commit();
+
+ result.putBoolean(RESULT_KEY, true);
+ } catch (JSONException e) {
+ //TODO Error message should be contained in that provider_dot_json_string
+ String reason_to_fail = pickErrorMessage(provider_dot_json_string);
+ result.putString(ERRORS, reason_to_fail);
+ result.putBoolean(RESULT_KEY, false);
+ }
+ }
+ return result;
+ }
+
+
+
+ public static boolean providerJsonDownloaded() {
+ return PROVIDER_JSON_DOWNLOADED;
+ }
+
+ private Bundle getAndSetEipServiceJson() {
+ Bundle result = new Bundle();
+ String eip_service_json_string = "";
+ if(setting_up_provider) {
+ try {
+ JSONObject provider_json = new JSONObject(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.KEY, ""));
+ String eip_service_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH;
+ eip_service_json_string = downloadWithProviderCA(eip_service_url, true);
+ JSONObject eip_service_json = new JSONObject(eip_service_json_string);
+ eip_service_json.getInt(Provider.API_RETURN_SERIAL);
+
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(EIP.KEY, eip_service_json.toString()).commit();
+
+ result.putBoolean(RESULT_KEY, true);
+ } catch (JSONException e) {
+ String reason_to_fail = pickErrorMessage(eip_service_json_string);
+ result.putString(ERRORS, reason_to_fail);
+ result.putBoolean(RESULT_KEY, false);
+ }
+ }
+ return result;
+ }
+
+ public static boolean eipServiceDownloaded() {
+ return EIP_SERVICE_JSON_DOWNLOADED;
+ }
+
+ /**
+ * Interprets the error message as a JSON object and extract the "errors" keyword pair.
+ * If the error message is not a JSON object, then it is returned untouched.
+ * @param string_json_error_message
+ * @return final error message
+ */
+ private String pickErrorMessage(String string_json_error_message) {
+ String error_message = "";
+ try {
+ JSONObject json_error_message = new JSONObject(string_json_error_message);
+ error_message = json_error_message.getString(ERRORS);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ error_message = string_json_error_message;
+ }
+
+ return error_message;
+ }
+
+ /**
+ * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider.
+ *
+ * If danger_on flag is true, SSL exceptions will be managed by futher methods that will try to use some bypass methods.
+ * @param string_url
+ * @param danger_on if the user completely trusts this provider
+ * @return
+ */
+ private String downloadWithCommercialCA(String string_url, boolean danger_on) {
+
+ String json_file_content = "";
+
+ URL provider_url = null;
+ int seconds_of_timeout = 1;
+ try {
+ provider_url = new URL(string_url);
+ URLConnection url_connection = provider_url.openConnection();
+ url_connection.setConnectTimeout(seconds_of_timeout*1000);
+ if(!LeapSRPSession.getToken().isEmpty())
+ url_connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token = " + LeapSRPSession.getToken());
+ json_file_content = new Scanner(url_connection.getInputStream()).useDelimiter("\\A").next();
+ } catch (MalformedURLException e) {
+ json_file_content = formatErrorMessage(R.string.malformed_url);
+ } catch(SocketTimeoutException e) {
+ json_file_content = formatErrorMessage(R.string.server_unreachable_message);
+ } catch (IOException e) {
+ if(provider_url != null) {
+ json_file_content = downloadWithProviderCA(string_url, danger_on);
+ } else {
+ json_file_content = formatErrorMessage(R.string.certificate_error);
+ }
+ } catch (Exception e) {
+ if(provider_url != null && danger_on) {
+ json_file_content = downloadWithProviderCA(string_url, danger_on);
+ }
+ }
+
+ return json_file_content;
+ }
+
+ /**
+ * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider.
+ * @param url as a string
+ * @param danger_on true to download CA certificate in case it has not been downloaded.
+ * @return an empty string if it fails, the url content if not.
+ */
+ private String downloadWithProviderCA(String url_string, boolean danger_on) {
+ String json_file_content = "";
+
+ try {
+ URL url = new URL(url_string);
+ // Tell the URLConnection to use a SocketFactory from our SSLContext
+ HttpsURLConnection urlConnection =
+ (HttpsURLConnection)url.openConnection();
+ urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory());
+ if(!LeapSRPSession.getToken().isEmpty())
+ urlConnection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken());
+ json_file_content = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next();
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (UnknownHostException e) {
+ json_file_content = formatErrorMessage(R.string.server_unreachable_message);
+ } catch (IOException e) {
+ // The downloaded certificate doesn't validate our https connection.
+ if(danger_on) {
+ json_file_content = downloadWithoutCA(url_string);
+ } else {
+ json_file_content = formatErrorMessage(R.string.certificate_error);
+ }
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyManagementException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return json_file_content;
+ }
+
+ private javax.net.ssl.SSLSocketFactory getProviderSSLSocketFactory() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException {
+ String provider_cert_string = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.CA_CERT,"");
+
+ java.security.cert.Certificate provider_certificate = ConfigHelper.parseX509CertificateFromString(provider_cert_string);
+
+ // Create a KeyStore containing our trusted CAs
+ String keyStoreType = KeyStore.getDefaultType();
+ KeyStore keyStore = KeyStore.getInstance(keyStoreType);
+ keyStore.load(null, null);
+ keyStore.setCertificateEntry("provider_ca_certificate", provider_certificate);
+
+ // Create a TrustManager that trusts the CAs in our KeyStore
+ String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
+ tmf.init(keyStore);
+
+ // Create an SSLContext that uses our TrustManager
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, tmf.getTrustManagers(), null);
+
+ return context.getSocketFactory();
+ }
+
+ /**
+ * Downloads the string that's in the url with any certificate.
+ */
+ private String downloadWithoutCA(String url_string) {
+ String string = "";
+ try {
+
+ HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ };
+
+ class DefaultTrustManager implements X509TrustManager {
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {}
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {}
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+ }
+
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(new KeyManager[0], new TrustManager[] {new DefaultTrustManager()}, new SecureRandom());
+
+ URL url = new URL(url_string);
+ HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
+ urlConnection.setSSLSocketFactory(context.getSocketFactory());
+ urlConnection.setHostnameVerifier(hostnameVerifier);
+ string = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next();
+ System.out.println("String ignoring certificate = " + string);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ string = formatErrorMessage(R.string.server_unreachable_message);
+ } catch (IOException e) {
+ // The downloaded certificate doesn't validate our https connection.
+ e.printStackTrace();
+ string = formatErrorMessage(R.string.certificate_error);
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyManagementException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return string;
+ }
+
+ /**
+ * Logs out from the api url retrieved from the task.
+ * @param task containing api url from which the user will log out
+ * @return true if there were no exceptions
+ */
+ private boolean logOut(Bundle task) {
+ try {
+ String delete_url = task.getString(Provider.API_URL) + "/logout";
+ int progress = 0;
+
+ HttpsURLConnection urlConnection = (HttpsURLConnection)new URL(delete_url).openConnection();
+ urlConnection.setRequestMethod("DELETE");
+ urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory());
+
+ int responseCode = urlConnection.getResponseCode();
+ broadcast_progress(progress++);
+ LeapSRPSession.setToken("");
+ Log.d(TAG, Integer.toString(responseCode));
+ } catch (ClientProtocolException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ } catch (IndexOutOfBoundsException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ } catch (KeyManagementException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return true;
+ }
+
+ /**
+ * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate.
+ *
+ * @param task containing the type of the certificate to be downloaded
+ * @return true if certificate was downloaded correctly, false if provider.json or danger_on flag are not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error.
+ */
+ private boolean getNewCert(Bundle task) {
+
+ try {
+ String type_of_certificate = task.getString(ConfigurationWizard.TYPE_OF_CERTIFICATE);
+ JSONObject provider_json = new JSONObject(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.KEY, ""));
+
+ String provider_main_url = provider_json.getString(Provider.API_URL);
+ URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.CERTIFICATE);
+
+ boolean danger_on = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getBoolean(ProviderItem.DANGER_ON, false);
+
+ String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), danger_on);
+
+ if(!cert_string.isEmpty()) {
+ if(ConfigHelper.checkErroneousDownload(cert_string)) {
+ String reason_to_fail = provider_json.getString(ERRORS);
+ //result.putString(ConfigHelper.ERRORS_KEY, reason_to_fail);
+ //result.putBoolean(ConfigHelper.RESULT_KEY, false);
+ return false;
+ } else {
+
+ // API returns concatenated cert & key. Split them for OpenVPN options
+ String certificateString = null, keyString = null;
+ String[] certAndKey = cert_string.split("(?<=-\n)");
+ for (int i=0; i < certAndKey.length-1; i++){
+ if ( certAndKey[i].contains("KEY") ) {
+ keyString = certAndKey[i++] + certAndKey[i];
+ }
+ else if ( certAndKey[i].contains("CERTIFICATE") ) {
+ certificateString = certAndKey[i++] + certAndKey[i];
+ }
+ }
+ try {
+ RSAPrivateKey keyCert = ConfigHelper.parseRsaKeyFromString(keyString);
+ keyString = Base64.encodeToString( keyCert.getEncoded(), Base64.DEFAULT );
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(EIP.PRIVATE_KEY, "-----BEGIN RSA PRIVATE KEY-----\n"+keyString+"-----END RSA PRIVATE KEY-----").commit();
+
+ X509Certificate certCert = ConfigHelper.parseX509CertificateFromString(certificateString);
+ certificateString = Base64.encodeToString( certCert.getEncoded(), Base64.DEFAULT);
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(EIP.CERTIFICATE, "-----BEGIN CERTIFICATE-----\n"+certificateString+"-----END CERTIFICATE-----").commit();
+
+ return true;
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ }
+ }
+ } else {
+ return false;
+ }
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ } /*catch (URISyntaxException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ }*/
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java
new file mode 100644
index 00000000..7b256124
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+
+/**
+ * Implements the ResultReceiver needed by Activities using ProviderAPI to receive the results of its operations.
+ * @author parmegv
+ *
+ */
+public class ProviderAPIResultReceiver extends ResultReceiver {
+ private Receiver mReceiver;
+
+ public ProviderAPIResultReceiver(Handler handler) {
+ super(handler);
+ // TODO Auto-generated constructor stub
+ }
+
+ public void setReceiver(Receiver receiver) {
+ mReceiver = receiver;
+ }
+
+ /**
+ * Interface to enable ProviderAPIResultReceiver to receive results from the ProviderAPI IntentService.
+ * @author parmegv
+ *
+ */
+ public interface Receiver {
+ public void onReceiveResult(int resultCode, Bundle resultData);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (mReceiver != null) {
+ mReceiver.onReceiveResult(resultCode, resultData);
+ }
+ }
+
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderDetailFragment.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderDetailFragment.java
new file mode 100644
index 00000000..c067ce2b
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderDetailFragment.java
@@ -0,0 +1,115 @@
+package se.leap.bitmaskclient;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+public class ProviderDetailFragment extends DialogFragment {
+
+ final public static String TAG = "providerDetailFragment";
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ try {
+
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View provider_detail_view = inflater.inflate(R.layout.provider_detail_fragment, null);
+
+ JSONObject provider_json = new JSONObject(getActivity().getSharedPreferences(Dashboard.SHARED_PREFERENCES, getActivity().MODE_PRIVATE).getString(Provider.KEY, ""));
+
+ final TextView domain = (TextView)provider_detail_view.findViewById(R.id.provider_detail_domain);
+ domain.setText(provider_json.getString(Provider.DOMAIN));
+ final TextView name = (TextView)provider_detail_view.findViewById(R.id.provider_detail_name);
+ name.setText(provider_json.getJSONObject(Provider.NAME).getString("en"));
+ final TextView description = (TextView)provider_detail_view.findViewById(R.id.provider_detail_description);
+ description.setText(provider_json.getJSONObject(Provider.DESCRIPTION).getString("en"));
+
+ builder.setView(provider_detail_view);
+ builder.setTitle(R.string.provider_details_fragment_title);
+
+ if(anon_allowed(provider_json)) {
+ builder.setPositiveButton(R.string.use_anonymously_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ interface_with_configuration_wizard.use_anonymously();
+ }
+ });
+ }
+
+ if(registration_allowed(provider_json)) {
+ builder.setNegativeButton(R.string.login_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ interface_with_configuration_wizard.login();
+ }
+ });
+ }
+
+ return builder.create();
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ private boolean anon_allowed(JSONObject provider_json) {
+ try {
+ JSONObject service_description = provider_json.getJSONObject(Provider.SERVICE);
+ return service_description.has(EIP.ALLOWED_ANON) && service_description.getBoolean(EIP.ALLOWED_ANON);
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ private boolean registration_allowed(JSONObject provider_json) {
+ try {
+ JSONObject service_description = provider_json.getJSONObject(Provider.SERVICE);
+ return service_description.has(Provider.ALLOW_REGISTRATION) && service_description.getBoolean(Provider.ALLOW_REGISTRATION);
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ super.onCancel(dialog);
+ SharedPreferences.Editor editor = getActivity().getSharedPreferences(Dashboard.SHARED_PREFERENCES, Activity.MODE_PRIVATE).edit();
+ editor.remove(Provider.KEY).remove(ProviderItem.DANGER_ON).remove(EIP.ALLOWED_ANON).remove(EIP.KEY).commit();
+ interface_with_configuration_wizard.showAllProviders();
+ }
+
+ public static DialogFragment newInstance() {
+ ProviderDetailFragment provider_detail_fragment = new ProviderDetailFragment();
+ return provider_detail_fragment;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ interface_with_configuration_wizard = (ProviderDetailFragmentInterface) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString()
+ + " must implement LogInDialogListener");
+ }
+ }
+
+ public interface ProviderDetailFragmentInterface {
+ public void login();
+ public void use_anonymously();
+ public void showAllProviders();
+ }
+
+ ProviderDetailFragmentInterface interface_with_configuration_wizard;
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java
new file mode 100644
index 00000000..43bba085
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java
@@ -0,0 +1,114 @@
+package se.leap.bitmaskclient;
+
+import java.util.List;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TwoLineListItem;
+
+public class ProviderListAdapter<T> extends ArrayAdapter<T> {
+ private static boolean[] hidden = null;
+
+ public void hide(int position) {
+ hidden[getRealPosition(position)] = true;
+ notifyDataSetChanged();
+ notifyDataSetInvalidated();
+ }
+
+ public void unHide(int position) {
+ hidden[getRealPosition(position)] = false;
+ notifyDataSetChanged();
+ notifyDataSetInvalidated();
+ }
+
+ public void unHideAll() {
+ for (int provider_index = 0; provider_index < hidden.length; provider_index++)
+ hidden[provider_index] = false;
+ }
+
+ private int getRealPosition(int position) {
+ int hElements = getHiddenCountUpTo(position);
+ int diff = 0;
+ for(int i=0;i<hElements;i++) {
+ diff++;
+ if(hidden[position+diff])
+ i--;
+ }
+ return (position + diff);
+ }
+ private int getHiddenCount() {
+ int count = 0;
+ for(int i=0;i<hidden.length;i++)
+ if(hidden[i])
+ count++;
+ return count;
+ }
+ private int getHiddenCountUpTo(int location) {
+ int count = 0;
+ for(int i=0;i<=location;i++) {
+ if(hidden[i])
+ count++;
+ }
+ return count;
+ }
+
+ @Override
+ public int getCount() {
+ return (hidden.length - getHiddenCount());
+ }
+
+ public ProviderListAdapter(Context mContext, int layout, List<T> objects) {
+ super(mContext, layout, objects);
+ if(hidden == null) {
+ hidden = new boolean[objects.size()];
+ for (int i = 0; i < objects.size(); i++)
+ hidden[i] = false;
+ }
+ }
+
+ public ProviderListAdapter(Context mContext, int layout, List<T> objects, boolean show_all_providers) {
+ super(mContext, layout, objects);
+ if(show_all_providers) {
+ hidden = new boolean[objects.size()];
+ for (int i = 0; i < objects.size(); i++)
+ hidden[i] = false;
+ }
+ }
+
+ @Override
+ public void add(T item) {
+ super.add(item);
+ boolean[] new_hidden = new boolean[hidden.length+1];
+ System.arraycopy(hidden, 0, new_hidden, 0, hidden.length);
+ new_hidden[hidden.length] = false;
+ hidden = new_hidden;
+ }
+
+ @Override
+ public void remove(T item) {
+ super.remove(item);
+ boolean[] new_hidden = new boolean[hidden.length-1];
+ System.arraycopy(hidden, 0, new_hidden, 0, hidden.length-1);
+ hidden = new_hidden;
+ }
+
+ @Override
+ public View getView(int index, View convertView, ViewGroup parent) {
+ TwoLineListItem row;
+ int position = getRealPosition(index);
+ if (convertView == null) {
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ row = (TwoLineListItem)inflater.inflate(R.layout.provider_list_item, null);
+ } else {
+ row = (TwoLineListItem)convertView;
+ }
+ ProviderListContent.ProviderItem data = ProviderListContent.ITEMS.get(position);
+ row.getText1().setText(data.domain());
+ row.getText2().setText(data.name());
+
+ return row;
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListContent.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListContent.java
new file mode 100644
index 00000000..e1ca4f9a
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListContent.java
@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.net.URL;
+import java.net.MalformedURLException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Models the provider list shown in the ConfigurationWizard.
+ *
+ * @author parmegv
+ *
+ */
+public class ProviderListContent {
+
+ public static List<ProviderItem> ITEMS = new ArrayList<ProviderItem>();
+
+ public static Map<String, ProviderItem> ITEM_MAP = new HashMap<String, ProviderItem>();
+
+ /**
+ * Adds a new provider item to the end of the items map, and to the items list.
+ * @param item
+ */
+ public static void addItem(ProviderItem item) {
+ ITEMS.add(item);
+ ITEM_MAP.put(String.valueOf(ITEMS.size()), item);
+ }
+ public static void removeItem(ProviderItem item) {
+ ITEMS.remove(item);
+ ITEM_MAP.remove(item);
+ }
+
+ /**
+ * A provider item.
+ */
+ public static class ProviderItem {
+ final public static String CUSTOM = "custom";
+ final public static String DANGER_ON = "danger_on";
+ private String provider_main_url;
+ private String name;
+
+ /**
+ * @param name of the provider
+ * @param urls_file_input_stream file input stream linking with the assets url file
+ * @param custom if it's a new provider entered by the user or not
+ * @param danger_on if the user trusts completely the new provider
+ */
+ public ProviderItem(String name, InputStream urls_file_input_stream) {
+
+ try {
+ byte[] urls_file_bytes = new byte[urls_file_input_stream.available()];
+ urls_file_input_stream.read(urls_file_bytes);
+ String urls_file_content = new String(urls_file_bytes);
+ JSONObject file_contents = new JSONObject(urls_file_content);
+ provider_main_url = file_contents.getString(Provider.MAIN_URL);
+ this.name = name;
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * @param name of the provider
+ * @param provider_main_url used to download provider.json file of the provider
+ * @param provider_json already downloaded
+ * @param custom if it's a new provider entered by the user or not
+ */
+ public ProviderItem(String name, String provider_main_url) {
+ this.name = name;
+ this.provider_main_url = provider_main_url;
+ }
+
+ public String name() { return name; }
+
+ public String providerMainUrl() { return provider_main_url; }
+
+ public String domain() {
+ try {
+ return new URL(provider_main_url).getHost();
+ } catch (MalformedURLException e) {
+ return provider_main_url.replaceFirst("http[s]?://", "").replaceFirst("/.*", "");
+ }
+ }
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListFragment.java b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListFragment.java
new file mode 100644
index 00000000..db414d87
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/bitmaskclient/ProviderListFragment.java
@@ -0,0 +1,234 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import android.app.Activity;
+import android.app.ListFragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+/**
+ * A list fragment representing a list of Providers. This fragment
+ * also supports tablet devices by allowing list items to be given an
+ * 'activated' state upon selection. This helps indicate which item is
+ * currently being viewed in a {@link DashboardFragment}.
+ * <p>
+ * Activities containing this fragment MUST implement the {@link Callbacks}
+ * interface.
+ */
+public class ProviderListFragment extends ListFragment {
+
+ public static String TAG = "provider_list_fragment";
+ public static String SHOW_ALL_PROVIDERS = "show_all_providers";
+ public static String TOP_PADDING = "top padding from providerlistfragment";
+ private ProviderListAdapter<ProviderItem> content_adapter;
+
+ /**
+ * The serialization (saved instance state) Bundle key representing the
+ * activated item position. Only used on tablets.
+ */
+ private static final String STATE_ACTIVATED_POSITION = "activated_position";
+
+ /**
+ * The fragment's current callback object, which is notified of list item
+ * clicks.
+ */
+ private Callbacks mCallbacks = sDummyCallbacks;
+
+ /**
+ * The current activated item position. Only used on tablets.
+ */
+ private int mActivatedPosition = ListView.INVALID_POSITION;
+
+ /**
+ * A callback interface that all activities containing this fragment must
+ * implement. This mechanism allows activities to be notified of item
+ * selections.
+ */
+ public interface Callbacks {
+ /**
+ * Callback for when an item has been selected.
+ */
+ public void onItemSelected(String id);
+ }
+
+ /**
+ * A dummy implementation of the {@link Callbacks} interface that does
+ * nothing. Used only when this fragment is not attached to an activity.
+ */
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+ @Override
+ public void onItemSelected(String id) {
+ }
+ };
+
+ /**
+ * Mandatory empty constructor for the fragment manager to instantiate the
+ * fragment (e.g. upon screen orientation changes).
+ */
+ public ProviderListFragment() {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if(getArguments().containsKey(SHOW_ALL_PROVIDERS))
+ content_adapter = new ProviderListAdapter<ProviderListContent.ProviderItem>(
+ getActivity(),
+ R.layout.provider_list_item,
+ ProviderListContent.ITEMS, getArguments().getBoolean(SHOW_ALL_PROVIDERS));
+ else
+ content_adapter = new ProviderListAdapter<ProviderListContent.ProviderItem>(
+ getActivity(),
+ R.layout.provider_list_item,
+ ProviderListContent.ITEMS);
+
+
+ setListAdapter(content_adapter);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ return inflater.inflate(R.layout.provider_list_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ // Restore the previously serialized activated item position.
+ if (savedInstanceState != null
+ && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
+ setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
+ }
+ if(getArguments() != null && getArguments().containsKey(TOP_PADDING)) {
+ int topPadding = getArguments().getInt(TOP_PADDING);
+ View current_view = getView();
+ getView().setPadding(current_view.getPaddingLeft(), topPadding, current_view.getPaddingRight(), current_view.getPaddingBottom());
+ }
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ // Activities containing this fragment must implement its callbacks.
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+
+ // Reset the active callbacks interface to the dummy implementation.
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onListItemClick(ListView listView, View view, int position, long id) {
+ super.onListItemClick(listView, view, position, id);
+
+ // Notify the active callbacks interface (the activity, if the
+ // fragment is attached to one) that an item has been selected.
+ mCallbacks.onItemSelected(ProviderListContent.ITEMS.get(position).name());
+
+ for(int item_position = 0; item_position < listView.getCount(); item_position++) {
+ if(item_position != position)
+ content_adapter.hide(item_position);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (mActivatedPosition != ListView.INVALID_POSITION) {
+ // Serialize and persist the activated item position.
+ outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
+ }
+ }
+
+ public void notifyAdapter() {
+ content_adapter.notifyDataSetChanged();
+ }
+ /**
+ * Turns on activate-on-click mode. When this mode is on, list items will be
+ * given the 'activated' state when touched.
+ */
+ public void setActivateOnItemClick(boolean activateOnItemClick) {
+ // When setting CHOICE_MODE_SINGLE, ListView will automatically
+ // give items the 'activated' state when touched.
+ getListView().setChoiceMode(activateOnItemClick
+ ? ListView.CHOICE_MODE_SINGLE
+ : ListView.CHOICE_MODE_NONE);
+ }
+
+ private void setActivatedPosition(int position) {
+ if (position == ListView.INVALID_POSITION) {
+ getListView().setItemChecked(mActivatedPosition, false);
+ } else {
+ getListView().setItemChecked(position, true);
+ }
+
+ mActivatedPosition = position;
+ }
+
+ public void removeLastItem() {
+ unhideAll();
+ content_adapter.remove(content_adapter.getItem(content_adapter.getCount()-1));
+ content_adapter.notifyDataSetChanged();
+ }
+
+ public void addItem(ProviderItem provider) {
+ content_adapter.add(provider);
+ content_adapter.notifyDataSetChanged();
+ }
+
+ public void hideAllBut(int position) {
+ int real_count = content_adapter.getCount();
+ for(int i = 0; i < real_count;)
+ if(i != position) {
+ content_adapter.hide(i);
+ position--;
+ real_count--;
+ } else {
+ i++;
+ }
+ }
+
+ public void unhideAll() {
+ if(content_adapter != null) {
+ content_adapter.unHideAll();
+ content_adapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * @return a new instance of this ListFragment.
+ */
+ public static ProviderListFragment newInstance() {
+ return new ProviderListFragment();
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/CIDRIP.java b/bitmask_android/src/main/java/se/leap/openvpn/CIDRIP.java
new file mode 100644
index 00000000..8c4b6709
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/CIDRIP.java
@@ -0,0 +1,58 @@
+package se.leap.openvpn;
+
+class CIDRIP{
+ String mIp;
+ int len;
+ public CIDRIP(String ip, String mask){
+ mIp=ip;
+ long netmask=getInt(mask);
+
+ // Add 33. bit to ensure the loop terminates
+ netmask += 1l << 32;
+
+ int lenZeros = 0;
+ while((netmask & 0x1) == 0) {
+ lenZeros++;
+ netmask = netmask >> 1;
+ }
+ // Check if rest of netmask is only 1s
+ if(netmask != (0x1ffffffffl >> lenZeros)) {
+ // Asume no CIDR, set /32
+ len=32;
+ } else {
+ len =32 -lenZeros;
+ }
+
+ }
+ @Override
+ public String toString() {
+ return String.format("%s/%d",mIp,len);
+ }
+
+ public boolean normalise(){
+ long ip=getInt(mIp);
+
+ long newip = ip & (0xffffffffl << (32 -len));
+ if (newip != ip){
+ mIp = String.format("%d.%d.%d.%d", (newip & 0xff000000) >> 24,(newip & 0xff0000) >> 16, (newip & 0xff00) >> 8 ,newip & 0xff);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ static long getInt(String ipaddr) {
+ String[] ipt = ipaddr.split("\\.");
+ long ip=0;
+
+ ip += Long.parseLong(ipt[0])<< 24;
+ ip += Integer.parseInt(ipt[1])<< 16;
+ ip += Integer.parseInt(ipt[2])<< 8;
+ ip += Integer.parseInt(ipt[3]);
+
+ return ip;
+ }
+ public long getInt() {
+ return getInt(mIp);
+ }
+
+} \ No newline at end of file
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/ConfigParser.java b/bitmask_android/src/main/java/se/leap/openvpn/ConfigParser.java
new file mode 100644
index 00000000..df4eae1b
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/ConfigParser.java
@@ -0,0 +1,569 @@
+package se.leap.openvpn;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Vector;
+
+//! Openvpn Config FIle Parser, probably not 100% accurate but close enough
+
+// And rember, this is valid :)
+// --<foo>
+// bar
+// </foo>
+public class ConfigParser {
+
+
+ private HashMap<String, Vector<Vector<String>>> options = new HashMap<String, Vector<Vector<String>>>();
+ public void parseConfig(Reader reader) throws IOException, ConfigParseError {
+
+
+ BufferedReader br =new BufferedReader(reader);
+
+ @SuppressWarnings("unused")
+ int lineno=0;
+
+ while (true){
+ String line = br.readLine();
+ if(line==null)
+ break;
+ lineno++;
+ Vector<String> args = parseline(line);
+ if(args.size() ==0)
+ continue;
+
+
+ if(args.get(0).startsWith("--"))
+ args.set(0, args.get(0).substring(2));
+
+ checkinlinefile(args,br);
+
+ String optionname = args.get(0);
+ if(!options.containsKey(optionname)) {
+ options.put(optionname, new Vector<Vector<String>>());
+ }
+ options.get(optionname).add(args);
+ }
+ }
+ public void setDefinition(HashMap<String,Vector<Vector<String>>> args) {
+ options = args;
+ }
+
+ private void checkinlinefile(Vector<String> args, BufferedReader br) throws IOException, ConfigParseError {
+ String arg0 = args.get(0);
+ // CHeck for <foo>
+ if(arg0.startsWith("<") && arg0.endsWith(">")) {
+ String argname = arg0.substring(1, arg0.length()-1);
+ String inlinefile = VpnProfile.INLINE_TAG;
+
+ String endtag = String.format("</%s>",argname);
+ do {
+ String line = br.readLine();
+ if(line==null){
+ throw new ConfigParseError(String.format("No endtag </%s> for starttag <%s> found",argname,argname));
+ }
+ if(line.equals(endtag))
+ break;
+ else {
+ inlinefile+=line;
+ inlinefile+= "\n";
+ }
+ } while(true);
+
+ args.clear();
+ args.add(argname);
+ args.add(inlinefile);
+ }
+
+ }
+
+ enum linestate {
+ initial,
+ readin_single_quote
+ , reading_quoted, reading_unquoted, done}
+
+ private boolean space(char c) {
+ // I really hope nobody is using zero bytes inside his/her config file
+ // to sperate parameter but here we go:
+ return Character.isWhitespace(c) || c == '\0';
+
+ }
+
+ public class ConfigParseError extends Exception {
+ private static final long serialVersionUID = -60L;
+
+ public ConfigParseError(String msg) {
+ super(msg);
+ }
+ }
+
+
+ // adapted openvpn's parse function to java
+ private Vector<String> parseline(String line) throws ConfigParseError {
+ Vector<String> parameters = new Vector<String>();
+
+ if (line.length()==0)
+ return parameters;
+
+
+ linestate state = linestate.initial;
+ boolean backslash = false;
+ char out=0;
+
+ int pos=0;
+ String currentarg="";
+
+ do {
+ // Emulate the c parsing ...
+ char in;
+ if(pos < line.length())
+ in = line.charAt(pos);
+ else
+ in = '\0';
+
+ if (!backslash && in == '\\' && state != linestate.readin_single_quote)
+ {
+ backslash = true;
+ }
+ else
+ {
+ if (state == linestate.initial)
+ {
+ if (!space (in))
+ {
+ if (in == ';' || in == '#') /* comment */
+ break;
+ if (!backslash && in == '\"')
+ state = linestate.reading_quoted;
+ else if (!backslash && in == '\'')
+ state = linestate.readin_single_quote;
+ else
+ {
+ out = in;
+ state = linestate.reading_unquoted;
+ }
+ }
+ }
+ else if (state == linestate.reading_unquoted)
+ {
+ if (!backslash && space (in))
+ state = linestate.done;
+ else
+ out = in;
+ }
+ else if (state == linestate.reading_quoted)
+ {
+ if (!backslash && in == '\"')
+ state = linestate.done;
+ else
+ out = in;
+ }
+ else if (state == linestate.readin_single_quote)
+ {
+ if (in == '\'')
+ state = linestate.done;
+ else
+ out = in;
+ }
+
+ if (state == linestate.done)
+ {
+ /* ASSERT (parm_len > 0); */
+ state = linestate.initial;
+ parameters.add(currentarg);
+ currentarg = "";
+ out =0;
+ }
+
+ if (backslash && out!=0)
+ {
+ if (!(out == '\\' || out == '\"' || space (out)))
+ {
+ throw new ConfigParseError("Options warning: Bad backslash ('\\') usage");
+ }
+ }
+ backslash = false;
+ }
+
+ /* store parameter character */
+ if (out!=0)
+ {
+ currentarg+=out;
+ }
+ } while (pos++ < line.length());
+
+ return parameters;
+ }
+
+
+ final String[] unsupportedOptions = { "config",
+ "connection",
+ "proto-force",
+ "remote-random",
+ "tls-server"
+
+ };
+
+ // Ignore all scripts
+ // in most cases these won't work and user who wish to execute scripts will
+ // figure out themselves
+ final String[] ignoreOptions = { "tls-client",
+ "askpass",
+ "auth-nocache",
+ "up",
+ "down",
+ "route-up",
+ "ipchange",
+ "route-up",
+ "route-pre-down",
+ "auth-user-pass-verify",
+ "dhcp-release",
+ "dhcp-renew",
+ "dh",
+ "management-hold",
+ "management",
+ "management-query-passwords",
+ "pause-exit",
+ "persist-key",
+ "register-dns",
+ "route-delay",
+ "route-gateway",
+ "route-metric",
+ "route-method",
+ "status",
+ "script-security",
+ "show-net-up",
+ "suppress-timestamps",
+ "tmp-dir",
+ "tun-ipv6",
+ "topology",
+ "win-sys",
+ };
+
+
+ // This method is far too long
+ public VpnProfile convertProfile() throws ConfigParseError{
+ boolean noauthtypeset=true;
+ VpnProfile np = new VpnProfile("converted Profile");
+ // Pull, client, tls-client
+ np.clearDefaults();
+
+ // XXX we are always client
+ if(/*options.containsKey("client") || options.containsKey("pull")*/ true) {
+ np.mUsePull=true;
+ options.remove("pull");
+ options.remove("client");
+ }
+
+ Vector<String> secret = getOption("secret", 1, 2);
+ if(secret!=null)
+ {
+ np.mAuthenticationType=VpnProfile.TYPE_STATICKEYS;
+ noauthtypeset=false;
+ np.mUseTLSAuth=true;
+ np.mTLSAuthFilename=secret.get(1);
+ if(secret.size()==3)
+ np.mTLSAuthDirection=secret.get(2);
+
+ }
+
+ Vector<Vector<String>> routes = getAllOption("route", 1, 4);
+ if(routes!=null) {
+ String routeopt = "";
+ for(Vector<String> route:routes){
+ String netmask = "255.255.255.255";
+ if(route.size() >= 3)
+ netmask = route.get(2);
+ String net = route.get(1);
+ try {
+ CIDRIP cidr = new CIDRIP(net, netmask);
+ routeopt+=cidr.toString() + " ";
+ } catch (ArrayIndexOutOfBoundsException aioob) {
+ throw new ConfigParseError("Could not parse netmask of route " + netmask);
+ } catch (NumberFormatException ne) {
+ throw new ConfigParseError("Could not parse netmask of route " + netmask);
+ }
+
+ }
+ np.mCustomRoutes=routeopt;
+ }
+
+ // Also recognize tls-auth [inline] direction ...
+ Vector<Vector<String>> tlsauthoptions = getAllOption("tls-auth", 1, 2);
+ if(tlsauthoptions!=null) {
+ for(Vector<String> tlsauth:tlsauthoptions) {
+ if(tlsauth!=null)
+ {
+ if(!tlsauth.get(1).equals("[inline]")) {
+ np.mTLSAuthFilename=tlsauth.get(1);
+ np.mUseTLSAuth=true;
+ }
+ if(tlsauth.size()==3)
+ np.mTLSAuthDirection=tlsauth.get(2);
+ }
+ }
+ }
+
+ Vector<String> direction = getOption("key-direction", 1, 1);
+ if(direction!=null)
+ np.mTLSAuthDirection=direction.get(1);
+
+
+ if(getAllOption("redirect-gateway", 0, 5) != null)
+ np.mUseDefaultRoute=true;
+
+ Vector<String> dev =getOption("dev",1,1);
+ Vector<String> devtype =getOption("dev-type",1,1);
+
+ if( (devtype !=null && devtype.get(1).equals("tun")) ||
+ (dev!=null && dev.get(1).startsWith("tun")) ||
+ (devtype ==null && dev == null) ) {
+ //everything okay
+ } else {
+ throw new ConfigParseError("Sorry. Only tun mode is supported. See the FAQ for more detail");
+ }
+
+
+
+ Vector<String> mode =getOption("mode",1,1);
+ if (mode != null){
+ if(!mode.get(1).equals("p2p"))
+ throw new ConfigParseError("Invalid mode for --mode specified, need p2p");
+ }
+
+ Vector<String> port = getOption("port", 1,1);
+ if(port!=null){
+ np.mServerPort = port.get(1);
+ }
+
+ Vector<String> proto = getOption("proto", 1,1);
+ if(proto!=null){
+ np.mUseUdp=isUdpProto(proto.get(1));;
+ }
+
+ // Parse remote config
+ Vector<String> remote = getOption("remote",1,3);
+ if(remote != null){
+ switch (remote.size()) {
+ case 4:
+ np.mUseUdp=isUdpProto(remote.get(3));
+ case 3:
+ np.mServerPort = remote.get(2);
+ case 2:
+ np.mServerName = remote.get(1);
+ }
+ }
+
+ // Parse remote config
+ Vector<String> location = getOption("location",0,2);
+ if(location != null && location.size() == 2){
+ np.mLocation = location.get(1).replace("__", ", ");
+ }
+
+ Vector<Vector<String>> dhcpoptions = getAllOption("dhcp-option", 2, 2);
+ if(dhcpoptions!=null) {
+ for(Vector<String> dhcpoption:dhcpoptions) {
+ String type=dhcpoption.get(1);
+ String arg = dhcpoption.get(2);
+ if(type.equals("DOMAIN")) {
+ np.mSearchDomain=dhcpoption.get(2);
+ } else if(type.equals("DNS")) {
+ np.mOverrideDNS=true;
+ if(np.mDNS1.equals(VpnProfile.DEFAULT_DNS1))
+ np.mDNS1=arg;
+ else
+ np.mDNS2=arg;
+ }
+ }
+ }
+
+ Vector<String> ifconfig = getOption("ifconfig", 2, 2);
+ if(ifconfig!=null) {
+ CIDRIP cidr = new CIDRIP(ifconfig.get(1), ifconfig.get(2));
+ np.mIPv4Address=cidr.toString();
+ }
+
+ if(getOption("remote-random-hostname", 0, 0)!=null)
+ np.mUseRandomHostname=true;
+
+ if(getOption("float", 0, 0)!=null)
+ np.mUseFloat=true;
+
+ if(getOption("comp-lzo", 0, 1)!=null)
+ np.mUseLzo=true;
+
+ Vector<String> cipher = getOption("cipher", 1, 1);
+ if(cipher!=null)
+ np.mCipher= cipher.get(1);
+
+ Vector<String> ca = getOption("ca",1,1);
+ if(ca!=null){
+ np.mCaFilename = ca.get(1);
+ }
+
+ Vector<String> cert = getOption("cert",1,1);
+ if(cert!=null){
+ np.mClientCertFilename = cert.get(1);
+ np.mAuthenticationType = VpnProfile.TYPE_CERTIFICATES;
+ noauthtypeset=false;
+ }
+ Vector<String> key= getOption("key",1,1);
+ if(key!=null)
+ np.mClientKeyFilename=key.get(1);
+
+ Vector<String> pkcs12 = getOption("pkcs12",1,1);
+ if(pkcs12!=null) {
+ np.mPKCS12Filename = pkcs12.get(1);
+ np.mAuthenticationType = VpnProfile.TYPE_KEYSTORE;
+ noauthtypeset=false;
+ }
+
+ Vector<String> tlsremote = getOption("tls-remote",1,1);
+ if(tlsremote!=null){
+ np.mRemoteCN = tlsremote.get(1);
+ np.mCheckRemoteCN=true;
+ }
+
+ Vector<String> verb = getOption("verb",1,1);
+ if(verb!=null){
+ np.mVerb=verb.get(1);
+ }
+
+
+ if(getOption("nobind", 0, 0) != null)
+ np.mNobind=true;
+
+ if(getOption("persist-tun", 0,0) != null)
+ np.mPersistTun=true;
+
+ Vector<String> connectretry = getOption("connect-retry", 1, 1);
+ if(connectretry!=null)
+ np.mConnectRetry =connectretry.get(1);
+
+ Vector<String> connectretrymax = getOption("connect-retry-max", 1, 1);
+ if(connectretrymax!=null)
+ np.mConnectRetryMax =connectretrymax.get(1);
+
+ Vector<Vector<String>> remotetls = getAllOption("remote-cert-tls", 1, 1);
+ if(remotetls!=null)
+ if(remotetls.get(0).get(1).equals("server"))
+ np.mExpectTLSCert=true;
+ else
+ options.put("remotetls",remotetls);
+
+ Vector<String> authuser = getOption("auth-user-pass",0,1);
+ if(authuser !=null){
+ if(noauthtypeset) {
+ np.mAuthenticationType=VpnProfile.TYPE_USERPASS;
+ } else if(np.mAuthenticationType==VpnProfile.TYPE_CERTIFICATES) {
+ np.mAuthenticationType=VpnProfile.TYPE_USERPASS_CERTIFICATES;
+ } else if(np.mAuthenticationType==VpnProfile.TYPE_KEYSTORE) {
+ np.mAuthenticationType=VpnProfile.TYPE_USERPASS_KEYSTORE;
+ }
+ if(authuser.size()>1) {
+ // Set option value to password get to get canche to embed later.
+ np.mUsername=null;
+ np.mPassword=authuser.get(1);
+ useEmbbedUserAuth(np,authuser.get(1));
+ }
+
+ }
+
+
+ // Check the other options
+
+ checkIgnoreAndInvalidOptions(np);
+ fixup(np);
+
+ return np;
+ }
+
+ private boolean isUdpProto(String proto) throws ConfigParseError {
+ boolean isudp;
+ if(proto.equals("udp") || proto.equals("udp6"))
+ isudp=true;
+ else if (proto.equals("tcp-client") ||
+ proto.equals("tcp") ||
+ proto.equals("tcp6") ||
+ proto.endsWith("tcp6-client"))
+ isudp =false;
+ else
+ throw new ConfigParseError("Unsupported option to --proto " + proto);
+ return isudp;
+ }
+
+ static public void useEmbbedUserAuth(VpnProfile np,String inlinedata)
+ {
+ String data = inlinedata.replace(VpnProfile.INLINE_TAG, "");
+ String[] parts = data.split("\n");
+ if(parts.length >= 2) {
+ np.mUsername=parts[0];
+ np.mPassword=parts[1];
+ }
+ }
+
+ private void checkIgnoreAndInvalidOptions(VpnProfile np) throws ConfigParseError {
+ for(String option:unsupportedOptions)
+ if(options.containsKey(option))
+ throw new ConfigParseError(String.format("Unsupported Option %s encountered in config file. Aborting",option));
+
+ for(String option:ignoreOptions)
+ // removing an item which is not in the map is no error
+ options.remove(option);
+
+ if(options.size()> 0) {
+ String custom = "# These Options were found in the config file do not map to config settings:\n";
+
+ for(Vector<Vector<String>> option:options.values()) {
+ for(Vector<String> optionsline: option) {
+ for (String arg : optionsline)
+ custom+= VpnProfile.openVpnEscape(arg) + " ";
+ }
+ custom+="\n";
+
+ }
+ np.mCustomConfigOptions = custom;
+ np.mUseCustomConfig=true;
+
+ }
+ }
+
+
+ private void fixup(VpnProfile np) {
+ if(np.mRemoteCN.equals(np.mServerName)) {
+ np.mRemoteCN="";
+ }
+ }
+
+ private Vector<String> getOption(String option, int minarg, int maxarg) throws ConfigParseError {
+ Vector<Vector<String>> alloptions = getAllOption(option, minarg, maxarg);
+ if(alloptions==null)
+ return null;
+ else
+ return alloptions.lastElement();
+ }
+
+
+ private Vector<Vector<String>> getAllOption(String option, int minarg, int maxarg) throws ConfigParseError {
+ Vector<Vector<String>> args = options.get(option);
+ if(args==null)
+ return null;
+
+ for(Vector<String> optionline:args)
+
+ if(optionline.size()< (minarg+1) || optionline.size() > maxarg+1) {
+ String err = String.format(Locale.getDefault(),"Option %s has %d parameters, expected between %d and %d",
+ option,optionline.size()-1,minarg,maxarg );
+ throw new ConfigParseError(err);
+ }
+ options.remove(option);
+ return args;
+ }
+
+}
+
+
+
+
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/LICENSE.txt b/bitmask_android/src/main/java/se/leap/openvpn/LICENSE.txt
new file mode 100644
index 00000000..d897edea
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/LICENSE.txt
@@ -0,0 +1,24 @@
+License for OpenVPN for Android. Please note that the thirdparty libraries/executables may have other license (OpenVPN, lzo, OpenSSL, Google Breakpad)
+
+Copyright (c) 2012-2013, Arne Schwabe
+ All rights reserved.
+
+If you need a non GPLv2 license of the source please contact me.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+In addition, as a special exception, the copyright holders give
+permission to link the code of portions of this program with the
+OpenSSL library.
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/LaunchVPN.java b/bitmask_android/src/main/java/se/leap/openvpn/LaunchVPN.java
new file mode 100644
index 00000000..89f2d372
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/LaunchVPN.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * 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 se.leap.openvpn;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Vector;
+
+import se.leap.bitmaskclient.ConfigHelper;
+import se.leap.bitmaskclient.EIP;
+import se.leap.bitmaskclient.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.VpnService;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+import android.preference.PreferenceManager;
+import android.text.InputType;
+import android.text.method.PasswordTransformationMethod;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * This Activity actually handles two stages of a launcher shortcut's life cycle.
+ *
+ * 1. Your application offers to provide shortcuts to the launcher. When
+ * the user installs a shortcut, an activity within your application
+ * generates the actual shortcut and returns it to the launcher, where it
+ * is shown to the user as an icon.
+ *
+ * 2. Any time the user clicks on an installed shortcut, an intent is sent.
+ * Typically this would then be handled as necessary by an activity within
+ * your application.
+ *
+ * We handle stage 1 (creating a shortcut) by simply sending back the information (in the form
+ * of an {@link android.content.Intent} that the launcher will use to create the shortcut.
+ *
+ * You can also implement this in an interactive way, by having your activity actually present
+ * UI for the user to select the specific nature of the shortcut, such as a contact, picture, URL,
+ * media item, or action.
+ *
+ * We handle stage 2 (responding to a shortcut) in this sample by simply displaying the contents
+ * of the incoming {@link android.content.Intent}.
+ *
+ * In a real application, you would probably use the shortcut intent to display specific content
+ * or start a particular operation.
+ */
+public class LaunchVPN extends ListActivity implements OnItemClickListener {
+
+ public static final String EXTRA_KEY = "se.leap.openvpn.shortcutProfileUUID";
+ public static final String EXTRA_NAME = "se.leap.openvpn.shortcutProfileName";
+ public static final String EXTRA_HIDELOG = "se.leap.openvpn.showNoLogWindow";;
+
+ public static final int START_VPN_PROFILE= 70;
+
+ // Dashboard, maybe more, want to know!
+ private ResultReceiver mReceiver;
+
+ private ProfileManager mPM;
+ private VpnProfile mSelectedProfile;
+ private boolean mhideLog=false;
+
+ private boolean mCmfixed=false;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mPM =ProfileManager.getInstance(this);
+
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ // Resolve the intent
+
+ final Intent intent = getIntent();
+ final String action = intent.getAction();
+
+ // If something wants feedback, they sent us a Receiver
+ mReceiver = intent.getParcelableExtra(EIP.RECEIVER_TAG);
+
+ // If the intent is a request to create a shortcut, we'll do that and exit
+
+
+ if(Intent.ACTION_MAIN.equals(action)) {
+ // we got called to be the starting point, most likely a shortcut
+ String shortcutUUID = intent.getStringExtra( EXTRA_KEY);
+ String shortcutName = intent.getStringExtra( EXTRA_NAME);
+ mhideLog = intent.getBooleanExtra(EXTRA_HIDELOG, false);
+
+ VpnProfile profileToConnect = ProfileManager.get(shortcutUUID);
+ if(shortcutName != null && profileToConnect ==null)
+ profileToConnect = ProfileManager.getInstance(this).getProfileByName(shortcutName);
+
+ if(profileToConnect ==null) {
+ OpenVPN.logError(R.string.shortcut_profile_notfound);
+ // show Log window to display error
+ showLogWindow();
+ finish();
+ return;
+ }
+
+ mSelectedProfile = profileToConnect;
+ launchVPN();
+
+ } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
+ createListView();
+ }
+ }
+
+ private void createListView() {
+ ListView lv = getListView();
+ //lv.setTextFilterEnabled(true);
+
+ Collection<VpnProfile> vpnlist = mPM.getProfiles();
+
+ Vector<String> vpnnames=new Vector<String>();
+ for (VpnProfile vpnProfile : vpnlist) {
+ vpnnames.add(vpnProfile.mName);
+ }
+
+
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,vpnnames);
+ lv.setAdapter(adapter);
+
+ lv.setOnItemClickListener(this);
+ }
+
+ /**
+ * This function creates a shortcut and returns it to the caller. There are actually two
+ * intents that you will send back.
+ *
+ * The first intent serves as a container for the shortcut and is returned to the launcher by
+ * setResult(). This intent must contain three fields:
+ *
+ * <ul>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_INTENT} The shortcut intent.</li>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_NAME} The text that will be displayed with
+ * the shortcut.</li>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_ICON} The shortcut's icon, if provided as a
+ * bitmap, <i>or</i> {@link android.content.Intent#EXTRA_SHORTCUT_ICON_RESOURCE} if provided as
+ * a drawable resource.</li>
+ * </ul>
+ *
+ * If you use a simple drawable resource, note that you must wrapper it using
+ * {@link android.content.Intent.ShortcutIconResource}, as shown below. This is required so
+ * that the launcher can access resources that are stored in your application's .apk file. If
+ * you return a bitmap, such as a thumbnail, you can simply put the bitmap into the extras
+ * bundle using {@link android.content.Intent#EXTRA_SHORTCUT_ICON}.
+ *
+ * The shortcut intent can be any intent that you wish the launcher to send, when the user
+ * clicks on the shortcut. Typically this will be {@link android.content.Intent#ACTION_VIEW}
+ * with an appropriate Uri for your content, but any Intent will work here as long as it
+ * triggers the desired action within your Activity.
+ * @param profile
+ */
+ private void setupShortcut(VpnProfile profile) {
+ // First, set up the shortcut intent. For this example, we simply create an intent that
+ // will bring us directly back to this activity. A more typical implementation would use a
+ // data Uri in order to display a more specific result, or a custom action in order to
+ // launch a specific operation.
+
+ Intent shortcutIntent = new Intent(Intent.ACTION_MAIN);
+ shortcutIntent.setClass(this, LaunchVPN.class);
+ shortcutIntent.putExtra(EXTRA_KEY,profile.getUUID().toString());
+
+ // Then, set up the container intent (the response to the caller)
+
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, profile.getName());
+ Parcelable iconResource = Intent.ShortcutIconResource.fromContext(
+ this, R.drawable.icon);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
+
+ // Now, return the result to the launcher
+
+ setResult(RESULT_OK, intent);
+ }
+
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position,
+ long id) {
+ String profilename = ((TextView) view).getText().toString();
+
+ VpnProfile profile = mPM.getProfileByName(profilename);
+
+ // if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
+ setupShortcut(profile);
+ finish();
+ return;
+ // }
+
+ }
+
+
+
+ private void askForPW(final int type) {
+
+ final EditText entry = new EditText(this);
+ entry.setSingleLine();
+ entry.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ entry.setTransformationMethod(new PasswordTransformationMethod());
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle("Need " + getString(type));
+ dialog.setMessage("Enter the password for profile " + mSelectedProfile.mName);
+ dialog.setView(entry);
+
+ dialog.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String pw = entry.getText().toString();
+ if(type == R.string.password) {
+ mSelectedProfile.mTransientPW = pw;
+ } else {
+ mSelectedProfile.mTransientPCKS12PW = pw;
+ }
+ onActivityResult(START_VPN_PROFILE, Activity.RESULT_OK, null);
+
+ }
+
+ });
+ dialog.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+
+ dialog.create().show();
+
+ }
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if(requestCode==START_VPN_PROFILE) {
+ if(resultCode == Activity.RESULT_OK) {
+ int needpw = mSelectedProfile.needUserPWInput();
+ if(needpw !=0) {
+ askForPW(needpw);
+ } else {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean showlogwindow = prefs.getBoolean("showlogwindow", false);
+
+ if(!mhideLog && showlogwindow)
+ showLogWindow();
+ new startOpenVpnThread().start();
+ }
+ } else if (resultCode == Activity.RESULT_CANCELED) {
+ // User does not want us to start, so we just vanish (well, now we tell our receiver, then vanish)
+ Bundle resultData = new Bundle();
+ // For now, nothing else is calling, so this "request" string is good enough
+ resultData.putString(EIP.REQUEST_TAG, EIP.ACTION_START_EIP);
+ mReceiver.send(RESULT_CANCELED, resultData);
+ finish();
+ }
+ }
+ }
+ void showLogWindow() {
+
+ Intent startLW = new Intent(getBaseContext(),LogWindow.class);
+ startLW.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ startActivity(startLW);
+
+ }
+
+ void showConfigErrorDialog(int vpnok) {
+ AlertDialog.Builder d = new AlertDialog.Builder(this);
+ d.setTitle(R.string.config_error_found);
+ d.setMessage(vpnok);
+ d.setPositiveButton(android.R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+
+ }
+ });
+ d.show();
+ }
+
+ void launchVPN () {
+ int vpnok = mSelectedProfile.checkProfile(this);
+ if(vpnok!= R.string.no_error_found) {
+ showConfigErrorDialog(vpnok);
+ return;
+ }
+
+ Intent intent = VpnService.prepare(this);
+ // Check if we want to fix /dev/tun
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean usecm9fix = prefs.getBoolean("useCM9Fix", false);
+ boolean loadTunModule = prefs.getBoolean("loadTunModule", false);
+
+ if(loadTunModule)
+ execeuteSUcmd("insmod /system/lib/modules/tun.ko");
+
+ if(usecm9fix && !mCmfixed ) {
+ execeuteSUcmd("chown system /dev/tun");
+ }
+
+
+ if (intent != null) {
+ // Start the query
+ try {
+ startActivityForResult(intent, START_VPN_PROFILE);
+ } catch (ActivityNotFoundException ane) {
+ // Shame on you Sony! At least one user reported that
+ // an official Sony Xperia Arc S image triggers this exception
+ OpenVPN.logError(R.string.no_vpn_support_image);
+ showLogWindow();
+ }
+ } else {
+ onActivityResult(START_VPN_PROFILE, Activity.RESULT_OK, null);
+ }
+
+ }
+
+ private void execeuteSUcmd(String command) {
+ ProcessBuilder pb = new ProcessBuilder(new String[] {"su","-c",command});
+ try {
+ Process p = pb.start();
+ int ret = p.waitFor();
+ if(ret ==0)
+ mCmfixed=true;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private class startOpenVpnThread extends Thread {
+
+ @Override
+ public void run() {
+ VPNLaunchHelper.startOpenVpn(mSelectedProfile, getBaseContext());
+ // Tell whom-it-may-concern that we started VPN
+ Bundle resultData = new Bundle();
+ // For now, nothing else is calling, so this "request" string is good enough
+ resultData.putString(EIP.REQUEST_TAG, EIP.ACTION_START_EIP);
+ mReceiver.send(RESULT_OK, resultData);
+ finish();
+
+ }
+
+ }
+
+
+}
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/LogWindow.java b/bitmask_android/src/main/java/se/leap/openvpn/LogWindow.java
new file mode 100644
index 00000000..b87c4999
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/LogWindow.java
@@ -0,0 +1,340 @@
+package se.leap.openvpn;
+
+import java.util.Vector;
+
+import se.leap.bitmaskclient.R;
+
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.ListActivity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import se.leap.openvpn.OpenVPN.LogItem;
+import se.leap.openvpn.OpenVPN.LogListener;
+import se.leap.openvpn.OpenVPN.StateListener;
+
+public class LogWindow extends ListActivity implements StateListener {
+ private static final int START_VPN_CONFIG = 0;
+ private String[] mBconfig=null;
+
+
+ class LogWindowListAdapter implements ListAdapter, LogListener, Callback {
+
+ private static final int MESSAGE_NEWLOG = 0;
+
+ private static final int MESSAGE_CLEARLOG = 1;
+
+ private Vector<String> myEntries=new Vector<String>();
+
+ private Handler mHandler;
+
+ private Vector<DataSetObserver> observers=new Vector<DataSetObserver>();
+
+
+ public LogWindowListAdapter() {
+ initLogBuffer();
+
+ if (mHandler == null) {
+ mHandler = new Handler(this);
+ }
+
+ OpenVPN.addLogListener(this);
+ }
+
+
+
+ private void initLogBuffer() {
+ myEntries.clear();
+ for (LogItem litem : OpenVPN.getlogbuffer()) {
+ myEntries.add(litem.getString(getContext()));
+ }
+ }
+
+ String getLogStr() {
+ String str = "";
+ for(String entry:myEntries) {
+ str+=entry + '\n';
+ }
+ return str;
+ }
+
+
+ private void shareLog() {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, getLogStr());
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.bitmask_openvpn_log_file));
+ shareIntent.setType("text/plain");
+ startActivity(Intent.createChooser(shareIntent, "Send Logfile"));
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ observers.add(observer);
+
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ observers.remove(observer);
+ }
+
+ @Override
+ public int getCount() {
+ return myEntries.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return myEntries.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView v;
+ if(convertView==null)
+ v = new TextView(getBaseContext());
+ else
+ v = (TextView) convertView;
+ v.setText(myEntries.get(position));
+ return v;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return myEntries.isEmpty();
+
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ @Override
+ public void newLog(LogItem logmessage) {
+ Message msg = Message.obtain();
+ msg.what=MESSAGE_NEWLOG;
+ Bundle mbundle=new Bundle();
+ mbundle.putString("logmessage", logmessage.getString(getBaseContext()));
+ msg.setData(mbundle);
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ // We have been called
+ if(msg.what==MESSAGE_NEWLOG) {
+
+ String logmessage = msg.getData().getString("logmessage");
+ myEntries.add(logmessage);
+
+ for (DataSetObserver observer : observers) {
+ observer.onChanged();
+ }
+ } else if (msg.what == MESSAGE_CLEARLOG) {
+ initLogBuffer();
+ for (DataSetObserver observer : observers) {
+ observer.onInvalidated();
+ }
+ }
+
+ return true;
+ }
+
+ void clearLog() {
+ // Actually is probably called from GUI Thread as result of the user
+ // pressing a button. But better safe than sorry
+ OpenVPN.clearLog();
+ OpenVPN.logMessage(0,"","Log cleared.");
+ mHandler.sendEmptyMessage(MESSAGE_CLEARLOG);
+ }
+ }
+
+
+
+ private LogWindowListAdapter ladapter;
+ private TextView mSpeedView;
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId()==R.id.clearlog) {
+ ladapter.clearLog();
+ return true;
+ } else if(item.getItemId()==R.id.cancel){
+ Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.title_cancel);
+ builder.setMessage(R.string.cancel_connection_query);
+ builder.setNegativeButton(android.R.string.no, null);
+ builder.setPositiveButton(android.R.string.yes, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ProfileManager.setConntectedVpnProfileDisconnected(getApplicationContext());
+ OpenVpnManagementThread.stopOpenVPN();
+ }
+ });
+
+ builder.show();
+ return true;
+ } else if(item.getItemId()==R.id.info) {
+ if(mBconfig==null)
+ OpenVPN.triggerLogBuilderConfig();
+
+ } else if(item.getItemId()==R.id.send) {
+ ladapter.shareLog();
+ }
+
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ protected Context getContext() {
+ return this;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.logmenu, menu);
+ return true;
+ }
+
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ OpenVPN.addStateListener(this);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == START_VPN_CONFIG && resultCode==RESULT_OK) {
+ String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+
+ final VpnProfile profile = ProfileManager.get(configuredVPN);
+ ProfileManager.getInstance(this).saveProfile(this, profile);
+ // Name could be modified, reset List adapter
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle(R.string.configuration_changed);
+ dialog.setMessage(R.string.restart_vpn_after_change);
+
+
+ dialog.setPositiveButton(R.string.restart,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(getBaseContext(), LaunchVPN.class);
+ intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUIDString());
+ intent.setAction(Intent.ACTION_MAIN);
+ startActivity(intent);
+ }
+
+
+ });
+ dialog.setNegativeButton(R.string.ignore, null);
+ dialog.create().show();
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ OpenVPN.removeStateListener(this);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.logwindow);
+ ListView lv = getListView();
+
+ lv.setOnItemLongClickListener(new OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view,
+ int position, long id) {
+ ClipboardManager clipboard = (ClipboardManager)
+ getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("Log Entry",((TextView) view).getText());
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(getBaseContext(), R.string.copied_entry, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ });
+
+ ladapter = new LogWindowListAdapter();
+ lv.setAdapter(ladapter);
+
+ mSpeedView = (TextView) findViewById(R.id.speed);
+ }
+
+ @Override
+ public void updateState(final String status,final String logmessage, final int resid) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ String prefix=getString(resid) + ":";
+ if (status.equals("BYTECOUNT") || status.equals("NOPROCESS") )
+ prefix="";
+ mSpeedView.setText(prefix + logmessage);
+ }
+ });
+
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ OpenVPN.removeLogListener(ladapter);
+ }
+
+}
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/NetworkSateReceiver.java b/bitmask_android/src/main/java/se/leap/openvpn/NetworkSateReceiver.java
new file mode 100644
index 00000000..777402b4
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/NetworkSateReceiver.java
@@ -0,0 +1,86 @@
+package se.leap.openvpn;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.State;
+import android.preference.PreferenceManager;
+import se.leap.bitmaskclient.R;
+
+public class NetworkSateReceiver extends BroadcastReceiver {
+ private int lastNetwork=-1;
+ private OpenVpnManagementThread mManangement;
+
+ private String lastStateMsg=null;
+
+ public NetworkSateReceiver(OpenVpnManagementThread managementThread) {
+ super();
+ mManangement = managementThread;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ NetworkInfo networkInfo = getCurrentNetworkInfo(context);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean sendusr1 = prefs.getBoolean("netchangereconnect", true);
+
+ String netstatestring;
+ if(networkInfo==null)
+ netstatestring = "not connected";
+ else {
+ String subtype = networkInfo.getSubtypeName();
+ if(subtype==null)
+ subtype = "";
+ String extrainfo = networkInfo.getExtraInfo();
+ if(extrainfo==null)
+ extrainfo="";
+
+ /*
+ if(networkInfo.getType()==android.net.ConnectivityManager.TYPE_WIFI) {
+ WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiinfo = wifiMgr.getConnectionInfo();
+ extrainfo+=wifiinfo.getBSSID();
+
+ subtype += wifiinfo.getNetworkId();
+ }*/
+
+
+
+ netstatestring = String.format("%2$s %4$s to %1$s %3$s",networkInfo.getTypeName(),
+ networkInfo.getDetailedState(),extrainfo,subtype );
+ }
+
+
+
+ if(networkInfo!=null && networkInfo.getState() == State.CONNECTED) {
+ int newnet = networkInfo.getType();
+
+ if(sendusr1 && lastNetwork!=newnet)
+ mManangement.reconnect();
+
+ lastNetwork = newnet;
+ } else if (networkInfo==null) {
+ // Not connected, stop openvpn, set last connected network to no network
+ lastNetwork=-1;
+ if(sendusr1)
+ mManangement.signalusr1();
+ }
+
+ if(!netstatestring.equals(lastStateMsg))
+ OpenVPN.logInfo(R.string.netstatus, netstatestring);
+ lastStateMsg=netstatestring;
+
+ }
+
+ private NetworkInfo getCurrentNetworkInfo(Context context) {
+ ConnectivityManager conn = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ NetworkInfo networkInfo = conn.getActiveNetworkInfo();
+ return networkInfo;
+ }
+
+}
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/OpenVPN.java b/bitmask_android/src/main/java/se/leap/openvpn/OpenVPN.java
new file mode 100644
index 00000000..8acdc423
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/OpenVPN.java
@@ -0,0 +1,250 @@
+package se.leap.openvpn;
+
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Vector;
+
+import se.leap.bitmaskclient.R;
+
+
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+public class OpenVPN {
+
+
+ public static LinkedList<LogItem> logbuffer;
+
+ private static Vector<LogListener> logListener;
+ private static Vector<StateListener> stateListener;
+ private static String[] mBconfig;
+
+ private static String mLaststatemsg;
+
+ private static String mLaststate;
+
+ private static int mLastStateresid=R.string.state_noprocess;
+ public static String TAG="se.leap.openvpn.OpenVPN";
+
+ static {
+ logbuffer = new LinkedList<LogItem>();
+ logListener = new Vector<OpenVPN.LogListener>();
+ stateListener = new Vector<OpenVPN.StateListener>();
+ logInformation();
+ }
+
+ public static class LogItem {
+ public static final int ERROR = 1;
+ public static final int INFO = 2;
+ public static final int VERBOSE = 3;
+
+ private Object [] mArgs = null;
+ private String mMessage = null;
+ private int mRessourceId;
+ // Default log priority
+ int mLevel = INFO;
+
+ public LogItem(int ressourceId, Object[] args) {
+ mRessourceId = ressourceId;
+ mArgs = args;
+ }
+
+
+ public LogItem(int loglevel,int ressourceId, Object[] args) {
+ mRessourceId = ressourceId;
+ mArgs = args;
+ mLevel = loglevel;
+ }
+
+
+ public LogItem(String message) {
+
+ mMessage = message;
+ }
+
+ public LogItem(int loglevel, String msg) {
+ mLevel = loglevel;
+ mMessage = msg;
+ }
+
+
+ public LogItem(int loglevel, int ressourceId) {
+ mRessourceId =ressourceId;
+ mLevel = loglevel;
+ }
+
+
+ public String getString(Context c) {
+ if(mMessage !=null) {
+ return mMessage;
+ } else {
+ if(c!=null) {
+ if(mArgs == null)
+ return c.getString(mRessourceId);
+ else
+ return c.getString(mRessourceId,mArgs);
+ } else {
+ String str = String.format(Locale.ENGLISH,"Log (no context) resid %d", mRessourceId);
+ if(mArgs !=null)
+ for(Object o:mArgs)
+ str += "|" + o.toString();
+ return str;
+ }
+ }
+ }
+ }
+
+ private static final int MAXLOGENTRIES = 500;
+
+
+ public static final String MANAGMENT_PREFIX = "M:";
+
+
+
+
+
+
+ public interface LogListener {
+ void newLog(LogItem logItem);
+ }
+
+ public interface StateListener {
+ void updateState(String state, String logmessage, int localizedResId);
+ }
+
+ synchronized static void logMessage(int level,String prefix, String message)
+ {
+ newlogItem(new LogItem(prefix + message));
+ Log.d("OpenVPN log item", message);
+ }
+
+ synchronized static void clearLog() {
+ logbuffer.clear();
+ logInformation();
+ }
+
+ private static void logInformation() {
+
+ logInfo(R.string.mobile_info,Build.MODEL, Build.BOARD,Build.BRAND,Build.VERSION.SDK_INT);
+ }
+
+ public synchronized static void addLogListener(LogListener ll){
+ logListener.add(ll);
+ }
+
+ public synchronized static void removeLogListener(LogListener ll) {
+ logListener.remove(ll);
+ }
+
+
+ public synchronized static void addStateListener(StateListener sl){
+ stateListener.add(sl);
+ if(mLaststate!=null)
+ sl.updateState(mLaststate, mLaststatemsg, mLastStateresid);
+ }
+
+ private static int getLocalizedState(String state){
+ if (state.equals("CONNECTING"))
+ return R.string.state_connecting;
+ else if (state.equals("WAIT"))
+ return R.string.state_wait;
+ else if (state.equals("AUTH"))
+ return R.string.state_auth;
+ else if (state.equals("GET_CONFIG"))
+ return R.string.state_get_config;
+ else if (state.equals("ASSIGN_IP"))
+ return R.string.state_assign_ip;
+ else if (state.equals("ADD_ROUTES"))
+ return R.string.state_add_routes;
+ else if (state.equals("CONNECTED"))
+ return R.string.state_connected;
+ else if (state.equals("RECONNECTING"))
+ return R.string.state_reconnecting;
+ else if (state.equals("EXITING"))
+ return R.string.state_exiting;
+ else if (state.equals("RESOLVE"))
+ return R.string.state_resolve;
+ else if (state.equals("TCP_CONNECT"))
+ return R.string.state_tcp_connect;
+ else if (state.equals("FATAL"))
+ return R.string.eip_state_not_connected;
+ else
+ return R.string.unknown_state;
+
+ }
+
+ public synchronized static void removeStateListener(StateListener sl) {
+ stateListener.remove(sl);
+ }
+
+
+ synchronized public static LogItem[] getlogbuffer() {
+
+ // The stoned way of java to return an array from a vector
+ // brought to you by eclipse auto complete
+ return (LogItem[]) logbuffer.toArray(new LogItem[logbuffer.size()]);
+
+ }
+ public static void logBuilderConfig(String[] bconfig) {
+ mBconfig = bconfig;
+ }
+ public static void triggerLogBuilderConfig() {
+ if(mBconfig==null) {
+ logMessage(0, "", "No active interface");
+ } else {
+ for (String item : mBconfig) {
+ logMessage(0, "", item);
+ }
+ }
+
+ }
+
+ public static void updateStateString (String state, String msg) {
+ int rid = getLocalizedState(state);
+ updateStateString(state, msg,rid);
+ }
+
+ public synchronized static void updateStateString(String state, String msg, int resid) {
+ if (! "BYTECOUNT".equals(state)) {
+ mLaststate= state;
+ mLaststatemsg = msg;
+ mLastStateresid = resid;
+
+ for (StateListener sl : stateListener) {
+ sl.updateState(state,msg,resid);
+ }
+ }
+ }
+
+ public static void logInfo(String message) {
+ newlogItem(new LogItem(LogItem.INFO, message));
+ }
+
+ public static void logInfo(int ressourceId, Object... args) {
+ newlogItem(new LogItem(LogItem.INFO, ressourceId, args));
+ }
+
+ private static void newlogItem(LogItem logItem) {
+ logbuffer.addLast(logItem);
+ if(logbuffer.size()>MAXLOGENTRIES)
+ logbuffer.removeFirst();
+
+ for (LogListener ll : logListener) {
+ ll.newLog(logItem);
+ }
+ }
+
+ public static void logError(String msg) {
+ newlogItem(new LogItem(LogItem.ERROR, msg));
+
+ }
+
+ public static void logError(int ressourceId) {
+ newlogItem(new LogItem(LogItem.ERROR, ressourceId));
+ }
+ public static void logError(int ressourceId, Object... args) {
+ newlogItem(new LogItem(LogItem.ERROR, ressourceId,args));
+ }
+
+}
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/OpenVPNThread.java b/bitmask_android/src/main/java/se/leap/openvpn/OpenVPNThread.java
new file mode 100644
index 00000000..ffd21732
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/OpenVPNThread.java
@@ -0,0 +1,130 @@
+package se.leap.openvpn;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.LinkedList;
+
+import se.leap.bitmaskclient.R;
+
+import android.util.Log;
+import se.leap.openvpn.OpenVPN.LogItem;
+
+public class OpenVPNThread implements Runnable {
+ private static final String DUMP_PATH_STRING = "Dump path: ";
+ private static final String TAG = "OpenVPN";
+ private String[] mArgv;
+ private Process mProcess;
+ private String mNativeDir;
+ private OpenVpnService mService;
+ private String mDumpPath;
+
+ public OpenVPNThread(OpenVpnService service,String[] argv, String nativelibdir)
+ {
+ mArgv = argv;
+ mNativeDir = nativelibdir;
+ mService = service;
+ }
+
+ public void stopProcess() {
+ mProcess.destroy();
+ }
+
+
+
+ @Override
+ public void run() {
+ try {
+ Log.i(TAG, "Starting openvpn");
+ startOpenVPNThreadArgs(mArgv);
+ Log.i(TAG, "Giving up");
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(TAG, "OpenVPNThread Got " + e.toString());
+ } finally {
+ int exitvalue = 0;
+ try {
+ exitvalue = mProcess.waitFor();
+ } catch ( IllegalThreadStateException ite) {
+ OpenVPN.logError("Illegal Thread state: " + ite.getLocalizedMessage());
+ } catch (InterruptedException ie) {
+ OpenVPN.logError("InterruptedException: " + ie.getLocalizedMessage());
+ }
+ if( exitvalue != 0)
+ OpenVPN.logError("Process exited with exit value " + exitvalue);
+
+// OpenVPN.updateStateString("NOPROCESS","No process running.", R.string.state_noprocess); fixes bug #4565
+ if(mDumpPath!=null) {
+ try {
+ BufferedWriter logout = new BufferedWriter(new FileWriter(mDumpPath + ".log"));
+ for(LogItem li :OpenVPN.getlogbuffer()){
+ logout.write(li.getString(null) + "\n");
+ }
+ logout.close();
+ OpenVPN.logError(R.string.minidump_generated);
+ } catch (IOException e) {
+ OpenVPN.logError("Writing minidump log: " +e.getLocalizedMessage());
+ }
+ }
+
+ mService.processDied();
+ Log.i(TAG, "Exiting");
+ }
+ }
+
+ private void startOpenVPNThreadArgs(String[] argv) {
+ LinkedList<String> argvlist = new LinkedList<String>();
+
+ for(String arg:argv)
+ argvlist.add(arg);
+
+ ProcessBuilder pb = new ProcessBuilder(argvlist);
+ // Hack O rama
+
+ // Hack until I find a good way to get the real library path
+ String applibpath = argv[0].replace("/cache/" + VpnProfile.MINIVPN , "/lib");
+
+ String lbpath = pb.environment().get("LD_LIBRARY_PATH");
+ if(lbpath==null)
+ lbpath = applibpath;
+ else
+ lbpath = lbpath + ":" + applibpath;
+
+ if (!applibpath.equals(mNativeDir)) {
+ lbpath = lbpath + ":" + mNativeDir;
+ }
+
+ pb.environment().put("LD_LIBRARY_PATH", lbpath);
+ pb.redirectErrorStream(true);
+ try {
+ mProcess = pb.start();
+ // Close the output, since we don't need it
+ mProcess.getOutputStream().close();
+ InputStream in = mProcess.getInputStream();
+ BufferedReader br = new BufferedReader(new InputStreamReader(in));
+
+ while(true) {
+ String logline = br.readLine();
+ if(logline==null)
+ return;
+
+ if (logline.startsWith(DUMP_PATH_STRING))
+ mDumpPath = logline.substring(DUMP_PATH_STRING.length());
+
+
+ OpenVPN.logMessage(0, "P:", logline);
+ }
+
+
+ } catch (IOException e) {
+ OpenVPN.logMessage(0, "", "Error reading from output of OpenVPN process"+ e.getLocalizedMessage());
+ e.printStackTrace();
+ stopProcess();
+ }
+
+
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/OpenVpnManagementThread.java b/bitmask_android/src/main/java/se/leap/openvpn/OpenVpnManagementThread.java
new file mode 100644
index 00000000..27a3db65
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/OpenVpnManagementThread.java
@@ -0,0 +1,592 @@
+package se.leap.openvpn;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.util.LinkedList;
+import java.util.Vector;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+
+import se.leap.bitmaskclient.R;
+import android.content.SharedPreferences;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.preference.PreferenceManager;
+import android.util.Base64;
+import android.util.Log;
+
+public class OpenVpnManagementThread implements Runnable {
+
+ private static final String TAG = "openvpn";
+ private LocalSocket mSocket;
+ private VpnProfile mProfile;
+ private OpenVpnService mOpenVPNService;
+ private LinkedList<FileDescriptor> mFDList=new LinkedList<FileDescriptor>();
+ private int mBytecountinterval=2;
+ private long mLastIn=0;
+ private long mLastOut=0;
+ private LocalServerSocket mServerSocket;
+ private boolean mReleaseHold=true;
+ private boolean mWaitingForRelease=false;
+ private long mLastHoldRelease=0;
+
+ private static Vector<OpenVpnManagementThread> active=new Vector<OpenVpnManagementThread>();
+
+ static private native void jniclose(int fdint);
+ static private native byte[] rsasign(byte[] input,int pkey) throws InvalidKeyException;
+
+ public OpenVpnManagementThread(VpnProfile profile, LocalServerSocket mgmtsocket, OpenVpnService openVpnService) {
+ mProfile = profile;
+ mServerSocket = mgmtsocket;
+ mOpenVPNService = openVpnService;
+
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(openVpnService);
+ boolean managemeNetworkState = prefs.getBoolean("netchangereconnect", true);
+ if(managemeNetworkState)
+ mReleaseHold=false;
+
+ }
+
+ static {
+ System.loadLibrary("opvpnutil");
+ }
+
+ public void managmentCommand(String cmd) {
+ if(mSocket!=null) {
+ try {
+ mSocket.getOutputStream().write(cmd.getBytes());
+ mSocket.getOutputStream().flush();
+ } catch (IOException e) {
+ // Ignore socket stack traces
+ }
+ }
+ }
+
+
+ @Override
+ public void run() {
+ Log.i(TAG, "Managment Socket Thread started");
+ byte [] buffer =new byte[2048];
+ // mSocket.setSoTimeout(5); // Setting a timeout cannot be that bad
+
+ String pendingInput="";
+ active.add(this);
+
+ try {
+ // Wait for a client to connect
+ mSocket= mServerSocket.accept();
+ InputStream instream = mSocket.getInputStream();
+
+ while(true) {
+ int numbytesread = instream.read(buffer);
+ if(numbytesread==-1)
+ return;
+
+ FileDescriptor[] fds = null;
+ try {
+ fds = mSocket.getAncillaryFileDescriptors();
+ } catch (IOException e) {
+ OpenVPN.logMessage(0, "", "Error reading fds from socket" + e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ if(fds!=null){
+
+ for (FileDescriptor fd : fds) {
+
+ mFDList.add(fd);
+ }
+ }
+
+ String input = new String(buffer,0,numbytesread,"UTF-8");
+
+ pendingInput += input;
+
+ pendingInput=processInput(pendingInput);
+
+
+
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ active.remove(this);
+ }
+
+ //! Hack O Rama 2000!
+ private void protectFileDescriptor(FileDescriptor fd) {
+ Exception exp=null;
+ try {
+ Method getInt = FileDescriptor.class.getDeclaredMethod("getInt$");
+ int fdint = (Integer) getInt.invoke(fd);
+
+ // You can even get more evil by parsing toString() and extract the int from that :)
+
+ mOpenVPNService.protect(fdint);
+
+ //ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(fdint);
+ //pfd.close();
+ jniclose(fdint);
+ return;
+ } catch (NoSuchMethodException e) {
+ exp =e;
+ } catch (IllegalArgumentException e) {
+ exp =e;
+ } catch (IllegalAccessException e) {
+ exp =e;
+ } catch (InvocationTargetException e) {
+ exp =e;
+ } catch (NullPointerException e) {
+ exp =e;
+ }
+ if(exp!=null) {
+ exp.printStackTrace();
+ Log.d("Openvpn", "Failed to retrieve fd from socket: " + fd);
+ OpenVPN.logMessage(0, "", "Failed to retrieve fd from socket: " + exp.getLocalizedMessage());
+ }
+ }
+
+ private String processInput(String pendingInput) {
+
+
+ while(pendingInput.contains("\n")) {
+ String[] tokens = pendingInput.split("\\r?\\n", 2);
+ processCommand(tokens[0]);
+ if(tokens.length == 1)
+ // No second part, newline was at the end
+ pendingInput="";
+ else
+ pendingInput=tokens[1];
+ }
+ return pendingInput;
+ }
+
+
+ private void processCommand(String command) {
+ Log.d(TAG, "processCommand: " + command);
+
+ if (command.startsWith(">") && command.contains(":")) {
+ String[] parts = command.split(":",2);
+ String cmd = parts[0].substring(1);
+ String argument = parts[1];
+ if(cmd.equals("INFO")) {
+ // Ignore greeting from mgmt
+ //logStatusMessage(command);
+ }else if (cmd.equals("PASSWORD")) {
+ processPWCommand(argument);
+ } else if (cmd.equals("HOLD")) {
+ handleHold();
+ } else if (cmd.equals("NEED-OK")) {
+ processNeedCommand(argument);
+ } else if (cmd.equals("BYTECOUNT")){
+ processByteCount(argument);
+ } else if (cmd.equals("STATE")) {
+ processState(argument);
+ } else if (cmd.equals("FATAL")){
+ processState(","+cmd+","); //handles FATAL as state
+ } else if (cmd.equals("PROXY")) {
+ processProxyCMD(argument);
+ } else if (cmd.equals("LOG")) {
+ String[] args = argument.split(",",3);
+ // 0 unix time stamp
+ // 1 log level N,I,E etc.
+ // 2 log message
+ OpenVPN.logMessage(0, "", args[2]);
+ } else if (cmd.equals("RSA_SIGN")) {
+ processSignCommand(argument);
+ } else {
+ OpenVPN.logMessage(0, "MGMT:", "Got unrecognized command" + command);
+ Log.i(TAG, "Got unrecognized command" + command);
+ }
+ } else if (command.startsWith("SUCCESS:")) { //Fixes bug LEAP #4565
+ if (command.equals("SUCCESS: signal SIGINT thrown")){
+ Log.d(TAG, "SUCCESS: signal SIGINT thrown");
+ processState(",EXITING,SIGINT,,");
+ }
+ } else {
+ Log.i(TAG, "Got unrecognized line from managment" + command);
+ OpenVPN.logMessage(0, "MGMT:", "Got unrecognized line from management:" + command);
+ }
+ }
+ private void handleHold() {
+ if(mReleaseHold) {
+ releaseHoldCmd();
+ } else {
+ mWaitingForRelease=true;
+ OpenVPN.updateStateString("NONETWORK", "",R.string.state_nonetwork);
+ }
+ }
+ private void releaseHoldCmd() {
+ if ((System.currentTimeMillis()- mLastHoldRelease) < 5000) {
+ try {
+ Thread.sleep(3000);
+ } catch (InterruptedException e) {}
+
+ }
+ mWaitingForRelease=false;
+ mLastHoldRelease = System.currentTimeMillis();
+ managmentCommand("hold release\n");
+ managmentCommand("bytecount " + mBytecountinterval + "\n");
+ managmentCommand("state on\n");
+ }
+
+ public void releaseHold() {
+ mReleaseHold=true;
+ if(mWaitingForRelease)
+ releaseHoldCmd();
+
+ }
+
+ private void processProxyCMD(String argument) {
+ String[] args = argument.split(",",3);
+ SocketAddress proxyaddr = ProxyDetection.detectProxy(mProfile);
+
+
+ if(args.length >= 2) {
+ String proto = args[1];
+ if(proto.equals("UDP")) {
+ proxyaddr=null;
+ }
+ }
+
+ if(proxyaddr instanceof InetSocketAddress ){
+ InetSocketAddress isa = (InetSocketAddress) proxyaddr;
+
+ OpenVPN.logInfo(R.string.using_proxy, isa.getHostName(),isa.getPort());
+
+ String proxycmd = String.format("proxy HTTP %s %d\n", isa.getHostName(),isa.getPort());
+ managmentCommand(proxycmd);
+ } else {
+ managmentCommand("proxy NONE\n");
+ }
+
+ }
+ private void processState(String argument) {
+ String[] args = argument.split(",",3);
+ String currentstate = args[1];
+ if(args[2].equals(",,")){
+ OpenVPN.updateStateString(currentstate,"");
+ }
+ else if (args[2].endsWith(",,")){ //fixes LEAP Bug #4546
+ args[2] = (String) args[2].subSequence(0, args[2].length()-2);
+ Log.d(TAG, "processState() STATE: "+ currentstate + " msg: " + args[2]);
+ OpenVPN.updateStateString(currentstate,args[2]);
+ }
+ else{
+ OpenVPN.updateStateString(currentstate,args[2]);
+ }
+ }
+
+ private static int repeated_byte_counts = 0;
+ private void processByteCount(String argument) {
+ // >BYTECOUNT:{BYTES_IN},{BYTES_OUT}
+ int comma = argument.indexOf(',');
+ long in = Long.parseLong(argument.substring(0, comma));
+ long out = Long.parseLong(argument.substring(comma+1));
+
+ long diffin = in - mLastIn;
+ long diffout = out - mLastOut;
+ if(diffin == 0 && diffout == 0)
+ repeated_byte_counts++;
+ if(repeated_byte_counts > 3)
+ Log.d("OpenVPN log", "Repeated byte count = " + repeated_byte_counts);
+ mLastIn=in;
+ mLastOut=out;
+
+ String netstat = String.format("In: %8s, %8s/s Out %8s, %8s/s",
+ humanReadableByteCount(in, false),
+ humanReadableByteCount(diffin, false),
+ humanReadableByteCount(out, false),
+ humanReadableByteCount(diffout, false));
+ OpenVPN.updateStateString("BYTECOUNT",netstat);
+
+
+ }
+
+ // From: http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java
+ public static String humanReadableByteCount(long bytes, boolean si) {
+ int unit = si ? 1000 : 1024;
+ if (bytes < unit) return bytes + " B";
+ int exp = (int) (Math.log(bytes) / Math.log(unit));
+ String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
+ return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
+ }
+
+ private void processNeedCommand(String argument) {
+ int p1 =argument.indexOf('\'');
+ int p2 = argument.indexOf('\'',p1+1);
+
+ String needed = argument.substring(p1+1, p2);
+ String extra = argument.split(":",2)[1];
+
+ String status = "ok";
+
+
+ if (needed.equals("PROTECTFD")) {
+ FileDescriptor fdtoprotect = mFDList.pollFirst();
+ protectFileDescriptor(fdtoprotect);
+ } else if (needed.equals("DNSSERVER")) {
+ mOpenVPNService.addDNS(extra);
+ }else if (needed.equals("DNSDOMAIN")){
+ mOpenVPNService.setDomain(extra);
+ } else if (needed.equals("ROUTE")) {
+ String[] routeparts = extra.split(" ");
+ mOpenVPNService.addRoute(routeparts[0], routeparts[1]);
+ } else if (needed.equals("ROUTE6")) {
+ mOpenVPNService.addRoutev6(extra);
+ } else if (needed.equals("IFCONFIG")) {
+ String[] ifconfigparts = extra.split(" ");
+ int mtu = Integer.parseInt(ifconfigparts[2]);
+ mOpenVPNService.setLocalIP(ifconfigparts[0], ifconfigparts[1],mtu,ifconfigparts[3]);
+ } else if (needed.equals("IFCONFIG6")) {
+ mOpenVPNService.setLocalIPv6(extra);
+
+ } else if (needed.equals("OPENTUN")) {
+ if(sendTunFD(needed,extra))
+ return;
+ else
+ status="cancel";
+ // This not nice or anything but setFileDescriptors accepts only FilDescriptor class :(
+
+ } else {
+ Log.e(TAG,"Unkown needok command " + argument);
+ return;
+ }
+
+ String cmd = String.format("needok '%s' %s\n", needed, status);
+ managmentCommand(cmd);
+ }
+
+ private boolean sendTunFD (String needed, String extra) {
+ Exception exp = null;
+ if(!extra.equals("tun")) {
+ // We only support tun
+ String errmsg = String.format("Devicetype %s requested, but only tun is possible with the Android API, sorry!",extra);
+ OpenVPN.logMessage(0, "", errmsg );
+
+ return false;
+ }
+ ParcelFileDescriptor pfd = mOpenVPNService.openTun();
+ if(pfd==null)
+ return false;
+
+ Method setInt;
+ int fdint = pfd.getFd();
+ try {
+ setInt = FileDescriptor.class.getDeclaredMethod("setInt$",int.class);
+ FileDescriptor fdtosend = new FileDescriptor();
+
+ setInt.invoke(fdtosend,fdint);
+
+ FileDescriptor[] fds = {fdtosend};
+ mSocket.setFileDescriptorsForSend(fds);
+
+ Log.d("Openvpn", "Sending FD tosocket: " + fdtosend + " " + fdint + " " + pfd);
+ // Trigger a send so we can close the fd on our side of the channel
+ // The API documentation fails to mention that it will not reset the file descriptor to
+ // be send and will happily send the file descriptor on every write ...
+ String cmd = String.format("needok '%s' %s\n", needed, "ok");
+ managmentCommand(cmd);
+
+ // Set the FileDescriptor to null to stop this mad behavior
+ mSocket.setFileDescriptorsForSend(null);
+
+ pfd.close();
+
+ return true;
+ } catch (NoSuchMethodException e) {
+ exp =e;
+ } catch (IllegalArgumentException e) {
+ exp =e;
+ } catch (IllegalAccessException e) {
+ exp =e;
+ } catch (InvocationTargetException e) {
+ exp =e;
+ } catch (IOException e) {
+ exp =e;
+ }
+ if(exp!=null) {
+ OpenVPN.logMessage(0,"", "Could not send fd over socket:" + exp.getLocalizedMessage());
+ exp.printStackTrace();
+ }
+ return false;
+ }
+
+ private void processPWCommand(String argument) {
+ //argument has the form Need 'Private Key' password
+ // or ">PASSWORD:Verification Failed: '%s' ['%s']"
+ String needed;
+
+
+
+ try{
+
+ int p1 = argument.indexOf('\'');
+ int p2 = argument.indexOf('\'',p1+1);
+ needed = argument.substring(p1+1, p2);
+ if (argument.startsWith("Verification Failed")) {
+ proccessPWFailed(needed, argument.substring(p2+1));
+ return;
+ }
+ } catch (StringIndexOutOfBoundsException sioob) {
+ OpenVPN.logMessage(0, "", "Could not parse management Password command: " + argument);
+ return;
+ }
+
+ String pw=null;
+
+ if(needed.equals("Private Key")) {
+ pw = mProfile.getPasswordPrivateKey();
+ } else if (needed.equals("Auth")) {
+ String usercmd = String.format("username '%s' %s\n",
+ needed, VpnProfile.openVpnEscape(mProfile.mUsername));
+ managmentCommand(usercmd);
+ pw = mProfile.getPasswordAuth();
+ }
+ if(pw!=null) {
+ String cmd = String.format("password '%s' %s\n", needed, VpnProfile.openVpnEscape(pw));
+ managmentCommand(cmd);
+ } else {
+ OpenVPN.logMessage(0, OpenVPN.MANAGMENT_PREFIX, String.format("Openvpn requires Authentication type '%s' but no password/key information available", needed));
+ }
+
+ }
+
+
+
+
+ private void proccessPWFailed(String needed, String args) {
+ OpenVPN.updateStateString("AUTH_FAILED", needed + args,R.string.state_auth_failed);
+ }
+ private void logStatusMessage(String command) {
+ OpenVPN.logMessage(0,"MGMT:", command);
+ }
+
+
+ public static boolean stopOpenVPN() {
+ boolean sendCMD=false;
+ for (OpenVpnManagementThread mt: active){
+ mt.managmentCommand("signal SIGINT\n");
+ sendCMD=true;
+ try {
+ if(mt.mSocket !=null)
+ mt.mSocket.close();
+ } catch (IOException e) {
+ // Ignore close error on already closed socket
+ }
+ }
+ return sendCMD;
+ }
+
+ public void signalusr1() {
+ mReleaseHold=false;
+ if(!mWaitingForRelease)
+ managmentCommand("signal SIGUSR1\n");
+ }
+
+ public void reconnect() {
+ signalusr1();
+ releaseHold();
+ }
+
+ private void processSignCommand(String b64data) {
+
+ PrivateKey privkey = mProfile.getKeystoreKey();
+ Exception err =null;
+
+ byte[] data = Base64.decode(b64data, Base64.DEFAULT);
+
+ // The Jelly Bean *evil* Hack
+ // 4.2 implements the RSA/ECB/PKCS1PADDING in the OpenSSLprovider
+ if(Build.VERSION.SDK_INT==16){
+ processSignJellyBeans(privkey,data);
+ return;
+ }
+
+
+ try{
+
+
+ Cipher rsasinger = Cipher.getInstance("RSA/ECB/PKCS1PADDING");
+
+ rsasinger.init(Cipher.ENCRYPT_MODE, privkey);
+
+ byte[] signed_bytes = rsasinger.doFinal(data);
+ String signed_string = Base64.encodeToString(signed_bytes, Base64.NO_WRAP);
+ managmentCommand("rsa-sig\n");
+ managmentCommand(signed_string);
+ managmentCommand("\nEND\n");
+ } catch (NoSuchAlgorithmException e){
+ err =e;
+ } catch (InvalidKeyException e) {
+ err =e;
+ } catch (NoSuchPaddingException e) {
+ err =e;
+ } catch (IllegalBlockSizeException e) {
+ err =e;
+ } catch (BadPaddingException e) {
+ err =e;
+ }
+ if(err !=null) {
+ OpenVPN.logError(R.string.error_rsa_sign,err.getClass().toString(),err.getLocalizedMessage());
+ }
+
+ }
+
+
+ private void processSignJellyBeans(PrivateKey privkey, byte[] data) {
+ Exception err =null;
+ try {
+ Method[] allm = privkey.getClass().getSuperclass().getDeclaredMethods();
+ System.out.println(allm);
+ Method getKey = privkey.getClass().getSuperclass().getDeclaredMethod("getOpenSSLKey");
+ getKey.setAccessible(true);
+
+ // Real object type is OpenSSLKey
+ Object opensslkey = getKey.invoke(privkey);
+
+ getKey.setAccessible(false);
+
+ Method getPkeyContext = opensslkey.getClass().getDeclaredMethod("getPkeyContext");
+
+ // integer pointer to EVP_pkey
+ getPkeyContext.setAccessible(true);
+ int pkey = (Integer) getPkeyContext.invoke(opensslkey);
+ getPkeyContext.setAccessible(false);
+
+ byte[] signed_bytes = rsasign(data, pkey);
+ String signed_string = Base64.encodeToString(signed_bytes, Base64.NO_WRAP);
+ managmentCommand("rsa-sig\n");
+ managmentCommand(signed_string);
+ managmentCommand("\nEND\n");
+
+ } catch (NoSuchMethodException e) {
+ err=e;
+ } catch (IllegalArgumentException e) {
+ err=e;
+ } catch (IllegalAccessException e) {
+ err=e;
+ } catch (InvocationTargetException e) {
+ err=e;
+ } catch (InvalidKeyException e) {
+ err=e;
+ }
+ if(err !=null) {
+ OpenVPN.logError(R.string.error_rsa_sign,err.getClass().toString(),err.getLocalizedMessage());
+ }
+
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/OpenVpnService.java b/bitmask_android/src/main/java/se/leap/openvpn/OpenVpnService.java
new file mode 100644
index 00000000..b5c9c798
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/OpenVpnService.java
@@ -0,0 +1,504 @@
+package se.leap.openvpn;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Vector;
+
+import se.leap.bitmaskclient.Dashboard;
+import se.leap.bitmaskclient.R;
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.net.VpnService;
+import android.os.Binder;
+import android.os.Handler.Callback;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import se.leap.openvpn.OpenVPN.StateListener;
+
+public class OpenVpnService extends VpnService implements StateListener, Callback {
+ public static final String START_SERVICE = "se.leap.openvpn.START_SERVICE";
+ public static final String RETRIEVE_SERVICE = "se.leap.openvpn.RETRIEVE_SERVICE";
+
+ private Thread mProcessThread=null;
+
+ private Vector<String> mDnslist=new Vector<String>();
+
+ private VpnProfile mProfile;
+
+ private String mDomain=null;
+
+ private Vector<CIDRIP> mRoutes=new Vector<CIDRIP>();
+ private Vector<String> mRoutesv6=new Vector<String>();
+
+ private CIDRIP mLocalIP=null;
+
+ private OpenVpnManagementThread mSocketManager;
+
+ private Thread mSocketManagerThread;
+ private int mMtu;
+ private String mLocalIPv6=null;
+ private NetworkSateReceiver mNetworkStateReceiver;
+ private NotificationManager mNotificationManager;
+
+ private boolean mDisplayBytecount=false;
+
+ private boolean mStarting=false;
+
+ private long mConnecttime;
+
+
+ private static final int OPENVPN_STATUS = 1;
+
+ public static final int PROTECT_FD = 0;
+
+ private final IBinder mBinder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ public OpenVpnService getService() {
+ // Return this instance of LocalService so clients can call public methods
+ return OpenVpnService.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ String action = intent.getAction();
+ if( action !=null && (action.equals(START_SERVICE) || action.equals(RETRIEVE_SERVICE)) )
+ return mBinder;
+ else
+ return super.onBind(intent);
+ }
+
+ @Override
+ public void onRevoke() {
+ OpenVpnManagementThread.stopOpenVPN();
+ endVpnService();
+ }
+
+ // Similar to revoke but do not try to stop process
+ public void processDied() {
+ endVpnService();
+ }
+
+ private void endVpnService() {
+ mProcessThread=null;
+ OpenVPN.logBuilderConfig(null);
+ ProfileManager.setConntectedVpnProfileDisconnected(this);
+ if(!mStarting) {
+ stopSelf();
+ stopForeground(true);
+ }
+ }
+
+ private void showNotification(String state, String msg, String tickerText, boolean lowpriority, long when, boolean persistant) {
+ String ns = Context.NOTIFICATION_SERVICE;
+ mNotificationManager = (NotificationManager) getSystemService(ns);
+ int icon;
+ if (state.equals("NOPROCESS") || state.equals("AUTH_FAILED") || state.equals("NONETWORK") || state.equals("EXITING")){
+ icon = R.drawable.ic_vpn_disconnected;
+ }else{
+ icon = R.drawable.ic_stat_vpn;
+ }
+
+ android.app.Notification.Builder nbuilder = new Notification.Builder(this);
+
+ nbuilder.setContentTitle(getString(R.string.notifcation_title,mProfile.mLocation));
+ nbuilder.setContentText(msg);
+ nbuilder.setOnlyAlertOnce(true);
+ nbuilder.setOngoing(persistant);
+ nbuilder.setContentIntent(getLogPendingIntent());
+ nbuilder.setSmallIcon(icon);
+ if(when !=0)
+ nbuilder.setWhen(when);
+
+
+ // Try to set the priority available since API 16 (Jellybean)
+ jbNotificationExtras(lowpriority, nbuilder);
+ if(tickerText!=null)
+ nbuilder.setTicker(tickerText);
+
+ @SuppressWarnings("deprecation")
+ Notification notification = nbuilder.getNotification();
+
+
+ mNotificationManager.notify(OPENVPN_STATUS, notification);
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void jbNotificationExtras(boolean lowpriority,
+ android.app.Notification.Builder nbuilder) {
+ try {
+ if(lowpriority) {
+ Method setpriority = nbuilder.getClass().getMethod("setPriority", int.class);
+ // PRIORITY_MIN == -2
+ setpriority.invoke(nbuilder, -2 );
+
+ nbuilder.setUsesChronometer(true);
+ /* PendingIntent cancelconnet=null;
+
+ nbuilder.addAction(android.R.drawable.ic_menu_close_clear_cancel,
+ getString(R.string.cancel_connection),cancelconnet); */
+ }
+
+ //ignore exception
+ } catch (NoSuchMethodException nsm) {
+ } catch (IllegalArgumentException e) {
+ } catch (IllegalAccessException e) {
+ } catch (InvocationTargetException e) {
+ }
+
+ }
+
+ PendingIntent getLogPendingIntent() {
+ // Let the configure Button show the Dashboard
+ Intent intent = new Intent(Dashboard.getAppContext(),Dashboard.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ PendingIntent startLW = PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ return startLW;
+
+ }
+
+
+ private LocalServerSocket openManagmentInterface(int tries) {
+ // Could take a while to open connection
+ String socketname = (getCacheDir().getAbsolutePath() + "/" + "mgmtsocket");
+ LocalSocket sock = new LocalSocket();
+
+ while(tries > 0 && !sock.isConnected()) {
+ try {
+ sock.bind(new LocalSocketAddress(socketname,
+ LocalSocketAddress.Namespace.FILESYSTEM));
+ } catch (IOException e) {
+ // wait 300 ms before retrying
+ try { Thread.sleep(300);
+ } catch (InterruptedException e1) {}
+
+ }
+ tries--;
+ }
+
+ try {
+ LocalServerSocket lss = new LocalServerSocket(sock.getFileDescriptor());
+ return lss;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+
+
+ }
+
+ void registerNetworkStateReceiver() {
+ // Registers BroadcastReceiver to track network connection changes.
+ IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+ mNetworkStateReceiver = new NetworkSateReceiver(mSocketManager);
+ this.registerReceiver(mNetworkStateReceiver, filter);
+ }
+
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+
+ if( intent != null && intent.getAction() !=null &&
+ (intent.getAction().equals(START_SERVICE) || intent.getAction().equals(RETRIEVE_SERVICE)) )
+ return START_NOT_STICKY;
+
+
+ // Extract information from the intent.
+ String prefix = getPackageName();
+ String[] argv = intent.getStringArrayExtra(prefix + ".ARGV");
+ String nativelibdir = intent.getStringExtra(prefix + ".nativelib");
+ String profileUUID = intent.getStringExtra(prefix + ".profileUUID");
+
+ mProfile = ProfileManager.get(profileUUID);
+
+ //showNotification("Starting VPN " + mProfile.mName,"Starting VPN " + mProfile.mName, false,0);
+
+
+ OpenVPN.addStateListener(this);
+
+ // Set a flag that we are starting a new VPN
+ mStarting=true;
+ // Stop the previous session by interrupting the thread.
+ if(OpenVpnManagementThread.stopOpenVPN()){
+ // an old was asked to exit, wait 2s
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ }
+ }
+
+ if (mProcessThread!=null) {
+ mProcessThread.interrupt();
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ }
+ }
+ // An old running VPN should now be exited
+ mStarting=false;
+
+
+ // Open the Management Interface
+ LocalServerSocket mgmtsocket = openManagmentInterface(8);
+
+ if(mgmtsocket!=null) {
+ // start a Thread that handles incoming messages of the managment socket
+ mSocketManager = new OpenVpnManagementThread(mProfile,mgmtsocket,this);
+ mSocketManagerThread = new Thread(mSocketManager,"OpenVPNMgmtThread");
+ mSocketManagerThread.start();
+ OpenVPN.logInfo("started Socket Thread");
+ registerNetworkStateReceiver();
+ }
+
+
+ // Start a new session by creating a new thread.
+ OpenVPNThread processThread = new OpenVPNThread(this, argv,nativelibdir);
+
+ mProcessThread = new Thread(processThread, "OpenVPNProcessThread");
+ mProcessThread.start();
+
+ ProfileManager.setConnectedVpnProfile(this, mProfile);
+
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mProcessThread != null) {
+ mSocketManager.managmentCommand("signal SIGINT\n");
+
+ mProcessThread.interrupt();
+ }
+ if (mNetworkStateReceiver!= null) {
+ this.unregisterReceiver(mNetworkStateReceiver);
+ }
+
+ }
+
+
+
+ public ParcelFileDescriptor openTun() {
+ Builder builder = new Builder();
+
+ if(mLocalIP==null && mLocalIPv6==null) {
+ OpenVPN.logMessage(0, "", getString(R.string.opentun_no_ipaddr));
+ return null;
+ }
+
+ if(mLocalIP!=null) {
+ builder.addAddress(mLocalIP.mIp, mLocalIP.len);
+ }
+
+ if(mLocalIPv6!=null) {
+ String[] ipv6parts = mLocalIPv6.split("/");
+ builder.addAddress(ipv6parts[0],Integer.parseInt(ipv6parts[1]));
+ }
+
+
+ for (String dns : mDnslist ) {
+ try {
+ builder.addDnsServer(dns);
+ } catch (IllegalArgumentException iae) {
+ OpenVPN.logError(R.string.dns_add_error, dns,iae.getLocalizedMessage());
+ }
+ }
+
+
+ builder.setMtu(mMtu);
+
+
+ for (CIDRIP route:mRoutes) {
+ try {
+ builder.addRoute(route.mIp, route.len);
+ } catch (IllegalArgumentException ia) {
+ OpenVPN.logMessage(0, "", getString(R.string.route_rejected) + route + " " + ia.getLocalizedMessage());
+ }
+ }
+
+ for(String v6route:mRoutesv6) {
+ try {
+ String[] v6parts = v6route.split("/");
+ builder.addRoute(v6parts[0],Integer.parseInt(v6parts[1]));
+ } catch (IllegalArgumentException ia) {
+ OpenVPN.logMessage(0, "", getString(R.string.route_rejected) + v6route + " " + ia.getLocalizedMessage());
+ }
+ }
+
+ if(mDomain!=null)
+ builder.addSearchDomain(mDomain);
+
+ String bconfig[] = new String[6];
+
+ bconfig[0]= getString(R.string.last_openvpn_tun_config);
+ bconfig[1] = getString(R.string.local_ip_info,mLocalIP.mIp,mLocalIP.len,mLocalIPv6, mMtu);
+ bconfig[2] = getString(R.string.dns_server_info, joinString(mDnslist));
+ bconfig[3] = getString(R.string.dns_domain_info, mDomain);
+ bconfig[4] = getString(R.string.routes_info, joinString(mRoutes));
+ bconfig[5] = getString(R.string.routes_info6, joinString(mRoutesv6));
+
+ String session = mProfile.mLocation;
+ /* we don't want the IP address in the notification bar
+ if(mLocalIP!=null && mLocalIPv6!=null)
+ session = getString(R.string.session_ipv6string,session, mLocalIP, mLocalIPv6);
+ else if (mLocalIP !=null)
+ session= getString(R.string.session_ipv4string, session, mLocalIP);
+ */
+ builder.setSession(session);
+
+
+ OpenVPN.logBuilderConfig(bconfig);
+
+ // No DNS Server, log a warning
+ if(mDnslist.size()==0)
+ OpenVPN.logInfo(R.string.warn_no_dns);
+
+ // Reset information
+ mDnslist.clear();
+ mRoutes.clear();
+ mRoutesv6.clear();
+ mLocalIP=null;
+ mLocalIPv6=null;
+ mDomain=null;
+
+ builder.setConfigureIntent(getLogPendingIntent());
+
+ try {
+ ParcelFileDescriptor pfd = builder.establish();
+ return pfd;
+ } catch (Exception e) {
+ OpenVPN.logMessage(0, "", getString(R.string.tun_open_error));
+ OpenVPN.logMessage(0, "", getString(R.string.error) + e.getLocalizedMessage());
+ OpenVPN.logMessage(0, "", getString(R.string.tun_error_helpful));
+ return null;
+ }
+
+ }
+
+
+ // Ugly, but java has no such method
+ private <T> String joinString(Vector<T> vec) {
+ String ret = "";
+ if(vec.size() > 0){
+ ret = vec.get(0).toString();
+ for(int i=1;i < vec.size();i++) {
+ ret = ret + ", " + vec.get(i).toString();
+ }
+ }
+ return ret;
+ }
+
+
+
+
+
+
+ public void addDNS(String dns) {
+ mDnslist.add(dns);
+ }
+
+
+ public void setDomain(String domain) {
+ if(mDomain==null) {
+ mDomain=domain;
+ }
+ }
+
+
+ public void addRoute(String dest, String mask) {
+ CIDRIP route = new CIDRIP(dest, mask);
+ if(route.len == 32 && !mask.equals("255.255.255.255")) {
+ OpenVPN.logMessage(0, "", getString(R.string.route_not_cidr,dest,mask));
+ }
+
+ if(route.normalise())
+ OpenVPN.logMessage(0, "", getString(R.string.route_not_netip,dest,route.len,route.mIp));
+
+ mRoutes.add(route);
+ }
+
+ public void addRoutev6(String extra) {
+ mRoutesv6.add(extra);
+ }
+
+
+ public void setLocalIP(String local, String netmask,int mtu, String mode) {
+ mLocalIP = new CIDRIP(local, netmask);
+ mMtu = mtu;
+
+ if(mLocalIP.len == 32 && !netmask.equals("255.255.255.255")) {
+ // get the netmask as IP
+ long netint = CIDRIP.getInt(netmask);
+ if(Math.abs(netint - mLocalIP.getInt()) ==1) {
+ if(mode.equals("net30"))
+ mLocalIP.len=30;
+ else
+ mLocalIP.len=31;
+ } else {
+ OpenVPN.logMessage(0, "", getString(R.string.ip_not_cidr, local,netmask,mode));
+ }
+ }
+ }
+
+ public void setLocalIPv6(String ipv6addr) {
+ mLocalIPv6 = ipv6addr;
+ }
+
+ public boolean isRunning() {
+ if (mStarting == true || mProcessThread != null)
+ return true;
+ else
+ return false;
+ }
+
+ @Override
+ public void updateState(String state,String logmessage, int resid) {
+ // If the process is not running, ignore any state,
+ // Notification should be invisible in this state
+ if(mProcessThread==null)
+ return;
+ if("CONNECTED".equals(state)) {
+ mNotificationManager.cancel(OPENVPN_STATUS);
+ } else if(!"BYTECOUNT".equals(state)) {
+
+ // Other notifications are shown,
+ // This also mean we are no longer connected, ignore bytecount messages until next
+ // CONNECTED
+ String ticker = getString(resid);
+ boolean persist = false;
+ if (("NOPROCESS".equals(state) ) || ("EXITING").equals(state)){
+ showNotification(state, getString(R.string.eip_state_not_connected), ticker, false, 0, persist);
+ }
+ else if (state.equals("GET_CONFIG") || state.equals("ASSIGN_IP")){ //don't show them in the notification message
+ }
+ else{
+ persist = true;
+ showNotification(state, getString(resid) +" " + logmessage,ticker,false,0,persist);
+ }
+ }
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ Runnable r = msg.getCallback();
+ if(r!=null){
+ r.run();
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/ProfileManager.java b/bitmask_android/src/main/java/se/leap/openvpn/ProfileManager.java
new file mode 100644
index 00000000..b9eb3ab6
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/ProfileManager.java
@@ -0,0 +1,220 @@
+package se.leap.openvpn;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.StreamCorruptedException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+
+public class ProfileManager {
+ private static final String PREFS_NAME = "VPNList";
+
+
+
+ private static final String ONBOOTPROFILE = "onBootProfile";
+
+
+
+ private static ProfileManager instance;
+
+
+
+ private static VpnProfile mLastConnectedVpn=null;
+ private HashMap<String,VpnProfile> profiles=new HashMap<String, VpnProfile>();
+ private static VpnProfile tmpprofile=null;
+
+
+ public static VpnProfile get(String key) {
+ if (tmpprofile!=null && tmpprofile.getUUIDString().equals(key))
+ return tmpprofile;
+
+ if(instance==null)
+ return null;
+ return instance.profiles.get(key);
+
+ }
+
+
+
+ private ProfileManager() { }
+
+ private static void checkInstance(Context context) {
+ if(instance == null) {
+ instance = new ProfileManager();
+ instance.loadVPNList(context);
+ }
+ }
+
+ synchronized public static ProfileManager getInstance(Context context) {
+ checkInstance(context);
+ return instance;
+ }
+
+ public static void setConntectedVpnProfileDisconnected(Context c) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c);
+ Editor prefsedit = prefs.edit();
+ prefsedit.putString(ONBOOTPROFILE, null);
+ prefsedit.apply();
+
+ }
+
+ public static void setConnectedVpnProfile(Context c, VpnProfile connectedrofile) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c);
+ Editor prefsedit = prefs.edit();
+
+ prefsedit.putString(ONBOOTPROFILE, connectedrofile.getUUIDString());
+ prefsedit.apply();
+ mLastConnectedVpn=connectedrofile;
+
+ }
+
+ public static VpnProfile getOnBootProfile(Context c) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c);
+
+ boolean useStartOnBoot = prefs.getBoolean("restartvpnonboot", false);
+
+
+ String mBootProfileUUID = prefs.getString(ONBOOTPROFILE,null);
+ if(useStartOnBoot && mBootProfileUUID!=null)
+ return get(c, mBootProfileUUID);
+ else
+ return null;
+ }
+
+
+
+
+ public Collection<VpnProfile> getProfiles() {
+ return profiles.values();
+ }
+
+ public VpnProfile getProfileByName(String name) {
+ for (VpnProfile vpnp : profiles.values()) {
+ if(vpnp.getName().equals(name)) {
+ return vpnp;
+ }
+ }
+ return null;
+ }
+
+ public void saveProfileList(Context context) {
+ SharedPreferences sharedprefs = context.getSharedPreferences(PREFS_NAME,Activity.MODE_PRIVATE);
+ Editor editor = sharedprefs.edit();
+ editor.putStringSet("vpnlist", profiles.keySet());
+
+ // For reasing I do not understand at all
+ // Android saves my prefs file only one time
+ // if I remove the debug code below :(
+ int counter = sharedprefs.getInt("counter", 0);
+ editor.putInt("counter", counter+1);
+ editor.apply();
+
+ }
+
+ public void addProfile(VpnProfile profile) {
+ profiles.put(profile.getUUID().toString(),profile);
+
+ }
+
+ public static void setTemporaryProfile(VpnProfile tmp) {
+ ProfileManager.tmpprofile = tmp;
+ }
+
+
+ public void saveProfile(Context context,VpnProfile profile) {
+ // First let basic settings save its state
+
+ ObjectOutputStream vpnfile;
+ try {
+ vpnfile = new ObjectOutputStream(context.openFileOutput((profile.getUUID().toString() + ".vp"),Activity.MODE_PRIVATE));
+
+ vpnfile.writeObject(profile);
+ vpnfile.flush();
+ vpnfile.close();
+ } catch (FileNotFoundException e) {
+
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+
+ e.printStackTrace();
+ throw new RuntimeException(e);
+
+ }
+ }
+
+
+ private void loadVPNList(Context context) {
+ profiles = new HashMap<String, VpnProfile>();
+ SharedPreferences listpref = context.getSharedPreferences(PREFS_NAME,Activity.MODE_PRIVATE);
+ Set<String> vlist = listpref.getStringSet("vpnlist", null);
+ Exception exp =null;
+ if(vlist==null){
+ vlist = new HashSet<String>();
+ }
+
+ for (String vpnentry : vlist) {
+ try {
+ ObjectInputStream vpnfile = new ObjectInputStream(context.openFileInput(vpnentry + ".vp"));
+ VpnProfile vp = ((VpnProfile) vpnfile.readObject());
+
+ // Sanity check
+ if(vp==null || vp.mName==null || vp.getUUID()==null)
+ continue;
+
+ profiles.put(vp.getUUID().toString(), vp);
+
+ } catch (StreamCorruptedException e) {
+ exp=e;
+ } catch (FileNotFoundException e) {
+ exp=e;
+ } catch (IOException e) {
+ exp=e;
+ } catch (ClassNotFoundException e) {
+ exp=e;
+ }
+ if(exp!=null) {
+ exp.printStackTrace();
+ }
+ }
+ }
+
+ public int getNumberOfProfiles() {
+ return profiles.size();
+ }
+
+
+
+ public void removeProfile(Context context,VpnProfile profile) {
+ String vpnentry = profile.getUUID().toString();
+ profiles.remove(vpnentry);
+ saveProfileList(context);
+ context.deleteFile(vpnentry + ".vp");
+ if(mLastConnectedVpn==profile)
+ mLastConnectedVpn=null;
+
+ }
+
+
+
+ public static VpnProfile get(Context context, String profileUUID) {
+ checkInstance(context);
+ return get(profileUUID);
+ }
+
+
+
+ public static VpnProfile getLastConnectedVpn() {
+ return mLastConnectedVpn;
+ }
+
+}
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/ProxyDetection.java b/bitmask_android/src/main/java/se/leap/openvpn/ProxyDetection.java
new file mode 100644
index 00000000..c7b3d196
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/ProxyDetection.java
@@ -0,0 +1,54 @@
+package se.leap.openvpn;
+
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.List;
+
+import se.leap.bitmaskclient.R;
+
+public class ProxyDetection {
+ static SocketAddress detectProxy(VpnProfile vp) {
+ // Construct a new url with https as protocol
+ try {
+ URL url = new URL(String.format("https://%s:%s",vp.mServerName,vp.mServerPort));
+ Proxy proxy = getFirstProxy(url);
+
+ if(proxy==null)
+ return null;
+ SocketAddress addr = proxy.address();
+ if (addr instanceof InetSocketAddress) {
+ return addr;
+ }
+
+ } catch (MalformedURLException e) {
+ OpenVPN.logError(R.string.getproxy_error,e.getLocalizedMessage());
+ } catch (URISyntaxException e) {
+ OpenVPN.logError(R.string.getproxy_error,e.getLocalizedMessage());
+ }
+ return null;
+ }
+
+ static Proxy getFirstProxy(URL url) throws URISyntaxException {
+ System.setProperty("java.net.useSystemProxies", "true");
+
+ List<Proxy> proxylist = ProxySelector.getDefault().select(url.toURI());
+
+
+ if (proxylist != null) {
+ for (Proxy proxy: proxylist) {
+ SocketAddress addr = proxy.address();
+
+ if (addr != null) {
+ return proxy;
+ }
+ }
+
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/VPNLaunchHelper.java b/bitmask_android/src/main/java/se/leap/openvpn/VPNLaunchHelper.java
new file mode 100644
index 00000000..418cf7e9
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/VPNLaunchHelper.java
@@ -0,0 +1,76 @@
+package se.leap.openvpn;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import se.leap.bitmaskclient.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+
+public class VPNLaunchHelper {
+ static private boolean writeMiniVPN(Context context) {
+ File mvpnout = new File(context.getCacheDir(),VpnProfile.MINIVPN);
+ if (mvpnout.exists() && mvpnout.canExecute())
+ return true;
+
+ IOException e2 = null;
+
+ try {
+ InputStream mvpn;
+
+ try {
+ mvpn = context.getAssets().open("minivpn." + Build.CPU_ABI);
+ }
+ catch (IOException errabi) {
+ OpenVPN.logInfo("Failed getting assets for archicture " + Build.CPU_ABI);
+ e2=errabi;
+ mvpn = context.getAssets().open("minivpn." + Build.CPU_ABI2);
+
+ }
+
+
+ FileOutputStream fout = new FileOutputStream(mvpnout);
+
+ byte buf[]= new byte[4096];
+
+ int lenread = mvpn.read(buf);
+ while(lenread> 0) {
+ fout.write(buf, 0, lenread);
+ lenread = mvpn.read(buf);
+ }
+ fout.close();
+
+ if(!mvpnout.setExecutable(true)) {
+ OpenVPN.logMessage(0, "","Failed to set minivpn executable");
+ return false;
+ }
+
+
+ return true;
+ } catch (IOException e) {
+ if(e2!=null)
+ OpenVPN.logMessage(0, "",e2.getLocalizedMessage());
+ OpenVPN.logMessage(0, "",e.getLocalizedMessage());
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+
+ public static void startOpenVpn(VpnProfile startprofile, Context context) {
+ if(!writeMiniVPN(context)) {
+ OpenVPN.logMessage(0, "", "Error writing minivpn binary");
+ return;
+ }
+ OpenVPN.logMessage(0, "", context.getString(R.string.building_configration));
+
+ Intent startVPN = startprofile.prepareIntent(context);
+ if(startVPN!=null)
+ context.startService(startVPN);
+
+ }
+}
diff --git a/bitmask_android/src/main/java/se/leap/openvpn/VpnProfile.java b/bitmask_android/src/main/java/se/leap/openvpn/VpnProfile.java
new file mode 100644
index 00000000..481819ad
--- /dev/null
+++ b/bitmask_android/src/main/java/se/leap/openvpn/VpnProfile.java
@@ -0,0 +1,758 @@
+package se.leap.openvpn;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.UUID;
+import java.util.Vector;
+
+import org.spongycastle.util.io.pem.PemObject;
+import org.spongycastle.util.io.pem.PemWriter;
+
+import se.leap.bitmaskclient.ConfigHelper;
+import se.leap.bitmaskclient.Dashboard;
+import se.leap.bitmaskclient.EIP;
+import se.leap.bitmaskclient.Provider;
+import se.leap.bitmaskclient.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.preference.PreferenceManager;
+import android.security.KeyChain;
+import android.security.KeyChainException;
+
+public class VpnProfile implements Serializable{
+ // Parcable
+ /**
+ *
+ */
+ private static final long serialVersionUID = 7085688938959334563L;
+ static final int TYPE_CERTIFICATES=0;
+ static final int TYPE_PKCS12=1;
+ static final int TYPE_KEYSTORE=2;
+ public static final int TYPE_USERPASS = 3;
+ public static final int TYPE_STATICKEYS = 4;
+ public static final int TYPE_USERPASS_CERTIFICATES = 5;
+ public static final int TYPE_USERPASS_PKCS12 = 6;
+ public static final int TYPE_USERPASS_KEYSTORE = 7;
+
+ // Don't change this, not all parts of the program use this constant
+ public static final String EXTRA_PROFILEUUID = "se.leap.bitmaskclient.profileUUID"; // TODO this feels wrong. See Issue #1494
+ public static final String INLINE_TAG = "[[INLINE]]";
+ private static final String OVPNCONFIGFILE = "android.conf";
+
+ protected transient String mTransientPW=null;
+ protected transient String mTransientPCKS12PW=null;
+ private transient PrivateKey mPrivateKey;
+ protected boolean profileDleted=false;
+
+
+ public static String DEFAULT_DNS1="131.234.137.23";
+ public static String DEFAULT_DNS2="131.234.137.24";
+
+ // Public attributes, since I got mad with getter/setter
+ // set members to default values
+ private UUID mUuid;
+ public int mAuthenticationType = TYPE_CERTIFICATES ;
+ public String mName;
+ public String mLocation;
+ public String mAlias;
+ public String mClientCertFilename;
+ public String mTLSAuthDirection="";
+ public String mTLSAuthFilename;
+ public String mClientKeyFilename;
+ public String mCaFilename;
+ public boolean mUseLzo=true;
+ public String mServerPort= "1194" ;
+ public boolean mUseUdp = true;
+ public String mPKCS12Filename;
+ public String mPKCS12Password;
+ public boolean mUseTLSAuth = false;
+ public String mServerName = "openvpn.blinkt.de" ;
+ public String mDNS1=DEFAULT_DNS1;
+ public String mDNS2=DEFAULT_DNS2;
+ public String mIPv4Address;
+ public String mIPv6Address;
+ public boolean mOverrideDNS=false;
+ public String mSearchDomain="blinkt.de";
+ public boolean mUseDefaultRoute=true;
+ public boolean mUsePull=true;
+ public String mCustomRoutes;
+ public boolean mCheckRemoteCN=false;
+ public boolean mExpectTLSCert=true;
+ public String mRemoteCN="";
+ public String mPassword="";
+ public String mUsername="";
+ public boolean mRoutenopull=false;
+ public boolean mUseRandomHostname=false;
+ public boolean mUseFloat=false;
+ public boolean mUseCustomConfig=false;
+ public String mCustomConfigOptions="";
+ public String mVerb="1";
+ public String mCipher="";
+ public boolean mNobind=false;
+ public boolean mUseDefaultRoutev6=true;
+ public String mCustomRoutesv6="";
+ public String mKeyPassword="";
+ public boolean mPersistTun = false;
+ public String mConnectRetryMax="5";
+ public String mConnectRetry="10";
+ public boolean mUserEditable=true;
+
+ static final String MINIVPN = "miniopenvpn";
+
+
+
+
+
+
+ public void clearDefaults() {
+ mServerName="unkown";
+ mUsePull=false;
+ mUseLzo=false;
+ mUseDefaultRoute=false;
+ mUseDefaultRoutev6=false;
+ mExpectTLSCert=false;
+ mPersistTun = false;
+ }
+
+
+ public static String openVpnEscape(String unescaped) {
+ if(unescaped==null)
+ return null;
+ String escapedString = unescaped.replace("\\", "\\\\");
+ escapedString = escapedString.replace("\"","\\\"");
+ escapedString = escapedString.replace("\n","\\n");
+
+ if (escapedString.equals(unescaped) && !escapedString.contains(" ") && !escapedString.contains("#"))
+ return unescaped;
+ else
+ return '"' + escapedString + '"';
+ }
+
+
+ static final String OVPNCONFIGCA = "android-ca.pem";
+ static final String OVPNCONFIGUSERCERT = "android-user.pem";
+
+
+ public VpnProfile(String name) {
+ mUuid = UUID.randomUUID();
+ mName = name;
+ }
+
+ public UUID getUUID() {
+ return mUuid;
+
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+
+ public String getConfigFile(Context context)
+ {
+
+ File cacheDir= context.getCacheDir();
+ String cfg="";
+
+ // Enable managment interface
+ cfg += "# Enables connection to GUI\n";
+ cfg += "management ";
+
+ cfg +=cacheDir.getAbsolutePath() + "/" + "mgmtsocket";
+ cfg += " unix\n";
+ cfg += "management-client\n";
+ // Not needed, see updated man page in 2.3
+ //cfg += "management-signal\n";
+ cfg += "management-query-passwords\n";
+ cfg += "management-hold\n\n";
+
+ /* tmp-dir patched out :)
+ cfg+="# /tmp does not exist on Android\n";
+ cfg+="tmp-dir ";
+ cfg+=cacheDir.getAbsolutePath();
+ cfg+="\n\n"; */
+
+ cfg+="# Log window is better readable this way\n";
+ cfg+="suppress-timestamps\n";
+
+
+
+ boolean useTLSClient = (mAuthenticationType != TYPE_STATICKEYS);
+
+ if(useTLSClient && mUsePull)
+ cfg+="client\n";
+ else if (mUsePull)
+ cfg+="pull\n";
+ else if(useTLSClient)
+ cfg+="tls-client\n";
+
+
+ cfg+="verb " + mVerb + "\n";
+
+ if(mConnectRetryMax ==null) {
+ mConnectRetryMax="5";
+ }
+
+ if(!mConnectRetryMax.equals("-1"))
+ cfg+="connect-retry-max " + mConnectRetryMax+ "\n";
+
+ if(mConnectRetry==null)
+ mConnectRetry="10";
+
+
+ cfg+="connect-retry " + mConnectRetry + "\n";
+
+ cfg+="resolv-retry 60\n";
+
+
+
+ // We cannot use anything else than tun
+ cfg+="dev tun\n";
+
+ // Server Address
+ cfg+="remote ";
+ cfg+=mServerName;
+ cfg+=" ";
+ cfg+=mServerPort;
+ if(mUseUdp)
+ cfg+=" udp\n";
+ else
+ cfg+=" tcp-client\n";
+
+
+
+
+ switch(mAuthenticationType) {
+ case VpnProfile.TYPE_USERPASS_CERTIFICATES:
+ cfg+="auth-user-pass\n";
+ case VpnProfile.TYPE_CERTIFICATES:
+ /*// Ca
+ cfg+=insertFileData("ca",mCaFilename);
+
+ // Client Cert + Key
+ cfg+=insertFileData("key",mClientKeyFilename);
+ cfg+=insertFileData("cert",mClientCertFilename);
+*/
+ // FIXME This is all we need...The whole switch statement can go...
+ SharedPreferences preferences = context.getSharedPreferences(Dashboard.SHARED_PREFERENCES, context.MODE_PRIVATE);
+ cfg+="<ca>\n"+preferences.getString(Provider.CA_CERT, "")+"\n</ca>\n";
+ cfg+="<key>\n"+preferences.getString(EIP.PRIVATE_KEY, "")+"\n</key>\n";
+ cfg+="<cert>\n"+preferences.getString(EIP.CERTIFICATE, "")+"\n</cert>\n";
+
+ break;
+ case VpnProfile.TYPE_USERPASS_PKCS12:
+ cfg+="auth-user-pass\n";
+ case VpnProfile.TYPE_PKCS12:
+ cfg+=insertFileData("pkcs12",mPKCS12Filename);
+ break;
+
+ case VpnProfile.TYPE_USERPASS_KEYSTORE:
+ cfg+="auth-user-pass\n";
+ case VpnProfile.TYPE_KEYSTORE:
+ cfg+="ca " + cacheDir.getAbsolutePath() + "/" + OVPNCONFIGCA + "\n";
+ cfg+="cert " + cacheDir.getAbsolutePath() + "/" + OVPNCONFIGUSERCERT + "\n";
+ cfg+="management-external-key\n";
+
+ break;
+ case VpnProfile.TYPE_USERPASS:
+ cfg+="auth-user-pass\n";
+ cfg+=insertFileData("ca",mCaFilename);
+ }
+
+ if(mUseLzo) {
+ cfg+="comp-lzo\n";
+ }
+
+ if(mUseTLSAuth) {
+ if(mAuthenticationType==TYPE_STATICKEYS)
+ cfg+=insertFileData("secret",mTLSAuthFilename);
+ else
+ cfg+=insertFileData("tls-auth",mTLSAuthFilename);
+
+ if(nonNull(mTLSAuthDirection)) {
+ cfg+= "key-direction ";
+ cfg+= mTLSAuthDirection;
+ cfg+="\n";
+ }
+
+ }
+
+ if(!mUsePull ) {
+ if(nonNull(mIPv4Address))
+ cfg +="ifconfig " + cidrToIPAndNetmask(mIPv4Address) + "\n";
+
+ if(nonNull(mIPv6Address))
+ cfg +="ifconfig-ipv6 " + mIPv6Address + "\n";
+ }
+
+ if(mUsePull && mRoutenopull)
+ cfg += "route-nopull\n";
+
+ String routes = "";
+ int numroutes=0;
+ if(mUseDefaultRoute)
+ routes += "route 0.0.0.0 0.0.0.0\n";
+ else
+ for(String route:getCustomRoutes()) {
+ routes += "route " + route + "\n";
+ numroutes++;
+ }
+
+
+ if(mUseDefaultRoutev6)
+ cfg += "route-ipv6 ::/0\n";
+ else
+ for(String route:getCustomRoutesv6()) {
+ routes += "route-ipv6 " + route + "\n";
+ numroutes++;
+ }
+
+ // Round number to next 100
+ if(numroutes> 90) {
+ numroutes = ((numroutes / 100)+1) * 100;
+ cfg+="# Alot of routes are set, increase max-routes\n";
+ cfg+="max-routes " + numroutes + "\n";
+ }
+ cfg+=routes;
+
+ if(mOverrideDNS || !mUsePull) {
+ if(nonNull(mDNS1))
+ cfg+="dhcp-option DNS " + mDNS1 + "\n";
+ if(nonNull(mDNS2))
+ cfg+="dhcp-option DNS " + mDNS2 + "\n";
+ if(nonNull(mSearchDomain))
+ cfg+="dhcp-option DOMAIN " + mSearchDomain + "\n";
+
+ }
+
+ if(mNobind)
+ cfg+="nobind\n";
+
+
+
+ // Authentication
+ if(mCheckRemoteCN) {
+ if(mRemoteCN == null || mRemoteCN.equals("") )
+ cfg+="tls-remote " + mServerName + "\n";
+ else
+ cfg += "tls-remote " + openVpnEscape(mRemoteCN) + "\n";
+ }
+ if(mExpectTLSCert)
+ cfg += "remote-cert-tls server\n";
+
+
+ if(nonNull(mCipher)){
+ cfg += "cipher " + mCipher + "\n";
+ }
+
+
+ // Obscure Settings dialog
+ if(mUseRandomHostname)
+ cfg += "#my favorite options :)\nremote-random-hostname\n";
+
+ if(mUseFloat)
+ cfg+= "float\n";
+
+ if(mPersistTun) {
+ cfg+= "persist-tun\n";
+ cfg+= "# persist-tun also sets persist-remote-ip to avoid DNS resolve problem\n";
+ cfg+= "persist-remote-ip\n";
+ }
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean usesystemproxy = prefs.getBoolean("usesystemproxy", true);
+ if(usesystemproxy) {
+ cfg+= "# Use system proxy setting\n";
+ cfg+= "management-query-proxy\n";
+ }
+
+
+ if(mUseCustomConfig) {
+ cfg += "# Custom configuration options\n";
+ cfg += "# You are on your on own here :)\n";
+ cfg += mCustomConfigOptions;
+ cfg += "\n";
+
+ }
+
+
+
+ return cfg;
+ }
+
+ //! Put inline data inline and other data as normal escaped filename
+ private String insertFileData(String cfgentry, String filedata) {
+ if(filedata==null) {
+ return String.format("%s %s\n",cfgentry,"missing");
+ }else if(filedata.startsWith(VpnProfile.INLINE_TAG)){
+ String datawoheader = filedata.substring(VpnProfile.INLINE_TAG.length());
+ return String.format("<%s>\n%s\n</%s>\n",cfgentry,datawoheader,cfgentry);
+ } else {
+ return String.format("%s %s\n",cfgentry,openVpnEscape(filedata));
+ }
+ }
+
+ private boolean nonNull(String val) {
+ if(val == null || val.equals(""))
+ return false;
+ else
+ return true;
+ }
+
+ private Collection<String> getCustomRoutes() {
+ Vector<String> cidrRoutes=new Vector<String>();
+ if(mCustomRoutes==null) {
+ // No routes set, return empty vector
+ return cidrRoutes;
+ }
+ for(String route:mCustomRoutes.split("[\n \t]")) {
+ if(!route.equals("")) {
+ String cidrroute = cidrToIPAndNetmask(route);
+ if(cidrRoutes == null)
+ return null;
+
+ cidrRoutes.add(cidrroute);
+ }
+ }
+
+ return cidrRoutes;
+ }
+
+ private Collection<String> getCustomRoutesv6() {
+ Vector<String> cidrRoutes=new Vector<String>();
+ if(mCustomRoutesv6==null) {
+ // No routes set, return empty vector
+ return cidrRoutes;
+ }
+ for(String route:mCustomRoutesv6.split("[\n \t]")) {
+ if(!route.equals("")) {
+ cidrRoutes.add(route);
+ }
+ }
+
+ return cidrRoutes;
+ }
+
+
+
+ private String cidrToIPAndNetmask(String route) {
+ String[] parts = route.split("/");
+
+ // No /xx, assume /32 as netmask
+ if (parts.length ==1)
+ parts = (route + "/32").split("/");
+
+ if (parts.length!=2)
+ return null;
+ int len;
+ try {
+ len = Integer.parseInt(parts[1]);
+ } catch(NumberFormatException ne) {
+ return null;
+ }
+ if (len <0 || len >32)
+ return null;
+
+
+ long nm = 0xffffffffl;
+ nm = (nm << (32-len)) & 0xffffffffl;
+
+ String netmask =String.format("%d.%d.%d.%d", (nm & 0xff000000) >> 24,(nm & 0xff0000) >> 16, (nm & 0xff00) >> 8 ,nm & 0xff );
+ return parts[0] + " " + netmask;
+ }
+
+
+
+ private String[] buildOpenvpnArgv(File cacheDir)
+ {
+ Vector<String> args = new Vector<String>();
+
+ // Add fixed paramenters
+ //args.add("/data/data/se.leap.openvpn/lib/openvpn");
+ args.add(cacheDir.getAbsolutePath() +"/" + VpnProfile.MINIVPN);
+
+ args.add("--config");
+ args.add(cacheDir.getAbsolutePath() + "/" + OVPNCONFIGFILE);
+ // Silences script security warning
+
+ args.add("script-security");
+ args.add("0");
+
+
+ return (String[]) args.toArray(new String[args.size()]);
+ }
+
+ public Intent prepareIntent(Context context) {
+ String prefix = context.getPackageName();
+
+ Intent intent = new Intent(context,OpenVpnService.class);
+
+ if(mAuthenticationType == VpnProfile.TYPE_KEYSTORE || mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) {
+ /*if(!saveCertificates(context))
+ return null;*/
+ }
+
+ intent.putExtra(prefix + ".ARGV" , buildOpenvpnArgv(context.getCacheDir()));
+ intent.putExtra(prefix + ".profileUUID", mUuid.toString());
+
+ ApplicationInfo info = context.getApplicationInfo();
+ intent.putExtra(prefix +".nativelib",info.nativeLibraryDir);
+
+ try {
+ FileWriter cfg = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + OVPNCONFIGFILE);
+ cfg.write(getConfigFile(context));
+ cfg.flush();
+ cfg.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return intent;
+ }
+
+ private boolean saveCertificates(Context context) {
+ PrivateKey privateKey = null;
+ X509Certificate[] cachain=null;
+ try {
+ privateKey = KeyChain.getPrivateKey(context,mAlias);
+ mPrivateKey = privateKey;
+
+ cachain = KeyChain.getCertificateChain(context, mAlias);
+ if(cachain.length <= 1 && !nonNull(mCaFilename))
+ OpenVPN.logMessage(0, "", context.getString(R.string.keychain_nocacert));
+
+ for(X509Certificate cert:cachain) {
+ OpenVPN.logInfo(R.string.cert_from_keystore,cert.getSubjectDN());
+ }
+
+
+
+
+ if(nonNull(mCaFilename)) {
+ try {
+ Certificate cacert = getCacertFromFile();
+ X509Certificate[] newcachain = new X509Certificate[cachain.length+1];
+ for(int i=0;i<cachain.length;i++)
+ newcachain[i]=cachain[i];
+
+ newcachain[cachain.length-1]=(X509Certificate) cacert;
+
+ } catch (Exception e) {
+ OpenVPN.logError("Could not read CA certificate" + e.getLocalizedMessage());
+ }
+ }
+
+
+ FileWriter fout = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + VpnProfile.OVPNCONFIGCA);
+ PemWriter pw = new PemWriter(fout);
+ for(X509Certificate cert:cachain) {
+ pw.writeObject(new PemObject("CERTIFICATE", cert.getEncoded()));
+ }
+
+ pw.close();
+
+
+ if(cachain.length>= 1){
+ X509Certificate usercert = cachain[0];
+
+ FileWriter userout = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + VpnProfile.OVPNCONFIGUSERCERT);
+
+ PemWriter upw = new PemWriter(userout);
+ upw.writeObject(new PemObject("CERTIFICATE", usercert.getEncoded()));
+ upw.close();
+
+ }
+
+ return true;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (KeyChainException e) {
+ OpenVPN.logMessage(0,"",context.getString(R.string.keychain_access));
+ }
+ return false;
+ }
+ private Certificate getCacertFromFile() throws FileNotFoundException, CertificateException {
+ CertificateFactory certFact = CertificateFactory.getInstance("X.509");
+
+ InputStream inStream;
+
+ if(mCaFilename.startsWith(INLINE_TAG))
+ inStream = new ByteArrayInputStream(mCaFilename.replace(INLINE_TAG,"").getBytes());
+ else
+ inStream = new FileInputStream(mCaFilename);
+
+ return certFact.generateCertificate(inStream);
+ }
+
+
+ //! Return an error if somethign is wrong
+ public int checkProfile(Context context) {
+/* if(mAuthenticationType==TYPE_KEYSTORE || mAuthenticationType==TYPE_USERPASS_KEYSTORE) {
+ if(mAlias==null)
+ return R.string.no_keystore_cert_selected;
+ }*/
+
+ if(!mUsePull) {
+ if(mIPv4Address == null || cidrToIPAndNetmask(mIPv4Address) == null)
+ return R.string.ipv4_format_error;
+ }
+ if(isUserPWAuth() && !nonNull(mUsername)) {
+ return R.string.error_empty_username;
+ }
+ if(!mUseDefaultRoute && getCustomRoutes()==null)
+ return R.string.custom_route_format_error;
+
+ // Everything okay
+ return R.string.no_error_found;
+
+ }
+
+ //! Openvpn asks for a "Private Key", this should be pkcs12 key
+ //
+ public String getPasswordPrivateKey() {
+ if(mTransientPCKS12PW!=null) {
+ String pwcopy = mTransientPCKS12PW;
+ mTransientPCKS12PW=null;
+ return pwcopy;
+ }
+ switch (mAuthenticationType) {
+ case TYPE_PKCS12:
+ case TYPE_USERPASS_PKCS12:
+ return mPKCS12Password;
+
+ case TYPE_CERTIFICATES:
+ case TYPE_USERPASS_CERTIFICATES:
+ return mKeyPassword;
+
+ case TYPE_USERPASS:
+ case TYPE_STATICKEYS:
+ default:
+ return null;
+ }
+ }
+ private boolean isUserPWAuth() {
+ switch(mAuthenticationType) {
+ case TYPE_USERPASS:
+ case TYPE_USERPASS_CERTIFICATES:
+ case TYPE_USERPASS_KEYSTORE:
+ case TYPE_USERPASS_PKCS12:
+ return true;
+ default:
+ return false;
+
+ }
+ }
+
+
+ public boolean requireTLSKeyPassword() {
+ if(!nonNull(mClientKeyFilename))
+ return false;
+
+ String data = "";
+ if(mClientKeyFilename.startsWith(INLINE_TAG))
+ data = mClientKeyFilename;
+ else {
+ char[] buf = new char[2048];
+ FileReader fr;
+ try {
+ fr = new FileReader(mClientKeyFilename);
+ int len = fr.read(buf);
+ while(len > 0 ) {
+ data += new String(buf,0,len);
+ len = fr.read(buf);
+ }
+ fr.close();
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+
+ }
+
+ if(data.contains("Proc-Type: 4,ENCRYPTED"))
+ return true;
+ else if(data.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----"))
+ return true;
+ else
+ return false;
+ }
+
+ public int needUserPWInput() {
+ if((mAuthenticationType == TYPE_PKCS12 || mAuthenticationType == TYPE_USERPASS_PKCS12)&&
+ (mPKCS12Password == null || mPKCS12Password.equals(""))) {
+ if(mTransientPCKS12PW==null)
+ return R.string.pkcs12_file_encryption_key;
+ }
+
+ if(mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) {
+ if(requireTLSKeyPassword() && !nonNull(mKeyPassword))
+ if(mTransientPCKS12PW==null) {
+ return R.string.private_key_password;
+ }
+ }
+
+ if(isUserPWAuth() && (mPassword.equals("") || mPassword == null)) {
+ if(mTransientPW==null)
+ return R.string.password;
+
+ }
+ return 0;
+ }
+
+ public String getPasswordAuth() {
+ if(mTransientPW!=null) {
+ String pwcopy = mTransientPW;
+ mTransientPW=null;
+ return pwcopy;
+ } else {
+ return mPassword;
+ }
+ }
+
+
+ // Used by the Array Adapter
+ @Override
+ public String toString() {
+ return mName;
+ }
+
+
+ public String getUUIDString() {
+ return mUuid.toString();
+ }
+
+
+ public PrivateKey getKeystoreKey() {
+ return mPrivateKey;
+ }
+
+
+
+}
+
+
+
+