diff options
author | Sean Leonard <meanderingcode@aetherislands.net> | 2013-10-20 14:15:22 -0700 |
---|---|---|
committer | Sean Leonard <meanderingcode@aetherislands.net> | 2013-10-20 14:15:22 -0700 |
commit | d3413b007a8f89e8a222455b5dd571b0e05e5a78 (patch) | |
tree | 40981e15c9eff744c71278e42cd0aba203c23f08 /src/se/leap/bitmaskclient | |
parent | 04fedee9f6be1e3e46a8c91c9a39343513cc6fe1 (diff) | |
parent | f657ab24f95016173d037d1e4714bb07bcd33fc2 (diff) |
Merge branch 'feature/rebrand-as-bitmask' into develop
Diffstat (limited to 'src/se/leap/bitmaskclient')
-rw-r--r-- | src/se/leap/bitmaskclient/AboutFragment.java | 60 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/ConfigHelper.java | 317 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/ConfigurationWizard.java | 418 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/Dashboard.java | 354 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/EIP.java | 520 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/EipServiceFragment.java | 265 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/LeapHttpClient.java | 84 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/LeapSRPSession.java | 326 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/LogInDialog.java | 139 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/NewProviderDialog.java | 110 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/Provider.java | 208 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/ProviderAPI.java | 780 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/ProviderAPIResultReceiver.java | 56 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/ProviderDetailFragment.java | 114 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/ProviderListContent.java | 146 | ||||
-rw-r--r-- | src/se/leap/bitmaskclient/ProviderListFragment.java | 192 |
16 files changed, 4089 insertions, 0 deletions
diff --git a/src/se/leap/bitmaskclient/AboutFragment.java b/src/se/leap/bitmaskclient/AboutFragment.java new file mode 100644 index 00000000..d751dc2f --- /dev/null +++ b/src/se/leap/bitmaskclient/AboutFragment.java @@ -0,0 +1,60 @@ +package se.leap.bitmaskclient; + +import android.app.Fragment; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.text.Html; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import se.leap.bitmaskclient.R; + +public class AboutFragment extends Fragment { + + final public static String TAG = "aboutFragment"; + + public static Fragment newInstance() { + AboutFragment provider_detail_fragment = new AboutFragment(); + return provider_detail_fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View v= inflater.inflate(R.layout.about, container, false); + TextView ver = (TextView) v.findViewById(R.id.version); + + String version; + String name="Openvpn"; + try { + PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().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)); + + TextView translation = (TextView) v.findViewById(R.id.translation); + + // Don't print a text for myself + if ( getString(R.string.translationby).contains("Arne Schwabe")) + translation.setText(""); + else + translation.setText(R.string.translationby); + return v; + } + +} diff --git a/src/se/leap/bitmaskclient/ConfigHelper.java b/src/se/leap/bitmaskclient/ConfigHelper.java new file mode 100644 index 00000000..b916a9ac --- /dev/null +++ b/src/se/leap/bitmaskclient/ConfigHelper.java @@ -0,0 +1,317 @@ +/** + * 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 { + + public static SharedPreferences shared_preferences; + private static KeyStore keystore_trusted; + + final public static String NG_1024 = + "eeaf0ab9adb38dd69c33f80afa8fc5e86072618775ff3c0b9ea2314c9c256576d674df7496ea81d3383b4813d692c6e0e0d5d8e250b98be48e495c1d6089dad15dc7d7b46154d6b6ce8ef4ad69b15d4982559b297bcf1885c529f566660e57ec68edbc3c05726cc02fd4cbf4976eaa9afd5138fe8376435b9fc61d2fc0eb06e3"; + final public static BigInteger G = new BigInteger("2"); + + + private static boolean checkSharedPrefs() { + try { + shared_preferences = Dashboard.getAppContext().getSharedPreferences(Dashboard.SHARED_PREFERENCES,Context.MODE_PRIVATE); + } catch (Exception e) { + return false; + } + + return true; + } + + /** + * Saves a JSON object into class scope Shared Preferences + * @param shared_preferences_key + * @param content + */ + public static void saveSharedPref(String shared_preferences_key, JSONObject content) { + + SharedPreferences.Editor shared_preferences_editor = shared_preferences + .edit(); + shared_preferences_editor.putString(shared_preferences_key, + content.toString()); + shared_preferences_editor.commit(); + } + + /** + * Saves a String object into class scope Shared Preferences + * @param shared_preferences_key + * @param content + */ + public static void saveSharedPref(String shared_preferences_key, String content) { + + SharedPreferences.Editor shared_preferences_editor = shared_preferences + .edit(); + shared_preferences_editor.putString(shared_preferences_key, + content); + shared_preferences_editor.commit(); + } + + /** + * Saves a boolean object into class scope Shared Preferences + * @param shared_preferences_key + * @param content + */ + public static void saveSharedPref(String shared_preferences_key, boolean content) { + + SharedPreferences.Editor shared_preferences_editor = shared_preferences + .edit(); + shared_preferences_editor.putBoolean(shared_preferences_key, content); + shared_preferences_editor.commit(); + } + + /** + * Saves an int into class scope Shared Preferences + * + * @param shared_preferences_key + * @param value + */ + protected static void saveSharedPref(String shared_preferences_key, int value) { + SharedPreferences.Editor shared_preferences_editor = shared_preferences.edit(); + shared_preferences_editor.putInt(shared_preferences_key, value).commit(); + } + + /** + * Gets String object from class scope Shared Preferences + * @param shared_preferences_key + * @return the string correspondent to the key parameter + */ + public static String getStringFromSharedPref(String shared_preferences_key) { + String content = null; + content = shared_preferences.getString(shared_preferences_key, ""); + return content; + } + + /** + * Gets JSON object from class scope Shared Preferences + * @param shared_preferences_key + * @return the JSON object correspondent to the key parameter + */ + public static JSONObject getJsonFromSharedPref(String shared_preferences_key) throws JSONException { + JSONObject content = null; + if ( checkSharedPrefs() ) { + String json_string = shared_preferences.getString(shared_preferences_key, ""); + content = new JSONObject(json_string); + } + + return content; + } + + /* + * This method defaults to false. + * If you use this method, be sure to fail-closed on false! + * TODO This is obviously less than ideal...solve it! + */ + public static boolean getBoolFromSharedPref(String shared_preferences_key) { + boolean value = false; + if ( checkSharedPrefs() ) { + value = shared_preferences.getBoolean(shared_preferences_key, false); + } + return value; + } + + /** + * Get an int from SharedPreferences + * + * @param shared_preferences_key Key to retrieve + * @return The value for the key or 0 + */ + protected static int getIntFromSharedPref(String shared_preferences_key) { + return shared_preferences.getInt(shared_preferences_key, 0); + } + + /* + * This method defaults to false. + * If you use this method, be sure to fail-closed on false! + * TODO This is obviously less than ideal...solve it! + */ + public static boolean removeFromSharedPref(String shared_preferences_key) { + SharedPreferences.Editor shared_preferences_editor = shared_preferences + .edit(); + shared_preferences_editor.remove(shared_preferences_key); + return shared_preferences_editor.commit(); + } + + /** + * 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; + } + + /** + * Sets class scope Shared Preferences + * @param shared_preferences + */ + public static void setSharedPreferences( + SharedPreferences shared_preferences) { + ConfigHelper.shared_preferences = shared_preferences; + } + + 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/src/se/leap/bitmaskclient/ConfigurationWizard.java b/src/se/leap/bitmaskclient/ConfigurationWizard.java new file mode 100644 index 00000000..bcb4fa9f --- /dev/null +++ b/src/se/leap/bitmaskclient/ConfigurationWizard.java @@ -0,0 +1,418 @@ +/**
+ * 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.util.Iterator;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.ProviderAPIResultReceiver.Receiver;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import android.app.Activity;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.content.res.AssetManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+/**
+ * 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, NewProviderDialog.NewProviderDialogInterface, ProviderDetailFragment.ProviderDetailFragmentInterface, Receiver {
+
+ private ProviderItem mSelectedProvider;
+ private ProgressDialog mProgressDialog;
+ private Intent mConfigState = new Intent();
+
+ 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;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.configuration_wizard_activity);
+
+ providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler());
+ providerAPI_result_receiver.setReceiver(this);
+
+ ConfigHelper.setSharedPreferences(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE));
+
+ 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)
+ ProviderListFragment providerList = new ProviderListFragment();
+
+ FragmentManager fragmentManager = getFragmentManager();
+ fragmentManager.beginTransaction()
+ .add(R.id.configuration_wizard_layout, providerList, "providerlist")
+ .commit();
+ }
+
+ // TODO: If exposing deep links into your app, handle intents here.
+ }
+
+ @Override
+ public void onReceiveResult(int resultCode, Bundle resultData) {
+ if(resultCode == ProviderAPI.CORRECTLY_UPDATED_PROVIDER_DOT_JSON) {
+ JSONObject provider_json;
+ try {
+ provider_json = new JSONObject(resultData.getString(Provider.KEY));
+ boolean danger_on = resultData.getBoolean(ProviderItem.DANGER_ON);
+ ConfigHelper.saveSharedPref(Provider.KEY, provider_json);
+ ConfigHelper.saveSharedPref(ProviderItem.DANGER_ON, danger_on);
+ ConfigHelper.saveSharedPref(EIP.ALLOWED_ANON, provider_json.getJSONObject(Provider.SERVICE).getBoolean(EIP.ALLOWED_ANON));
+ mConfigState.setAction(PROVIDER_SET);
+
+ if(mProgressDialog != null) mProgressDialog.dismiss();
+ mProgressDialog = ProgressDialog.show(this, getResources().getString(R.string.config_wait_title), getResources().getString(R.string.config_connecting_provider), true);
+ mProgressDialog.setMessage(getResources().getString(R.string.config_downloading_services));
+ if(resultData.containsKey(Provider.NAME))
+ mSelectedProvider = getProvider(resultData.getString(Provider.NAME));
+
+ ProviderListFragment providerList = new ProviderListFragment();
+
+ FragmentManager fragmentManager = getFragmentManager();
+ fragmentManager.beginTransaction()
+ .replace(R.id.configuration_wizard_layout, providerList, "providerlist")
+ .commit();
+ downloadJSONFiles(mSelectedProvider);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+
+ mProgressDialog.dismiss();
+ //Toast.makeText(this, getResources().getString(R.string.config_error_parsing), Toast.LENGTH_LONG);
+ setResult(RESULT_CANCELED, mConfigState);
+ }
+ }
+ else if(resultCode == ProviderAPI.INCORRECTLY_UPDATED_PROVIDER_DOT_JSON) {
+ mProgressDialog.dismiss();
+ setResult(RESULT_CANCELED, mConfigState);
+ }
+ else if(resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_JSON_FILES) {
+ if (ConfigHelper.getBoolFromSharedPref(EIP.ALLOWED_ANON)){
+ mProgressDialog.setMessage(getResources().getString(R.string.config_downloading_certificates));
+ mConfigState.putExtra(SERVICES_RETRIEVED, true);
+ downloadAnonCert();
+ } else {
+ mProgressDialog.dismiss();
+ //Toast.makeText(getApplicationContext(), R.string.success, Toast.LENGTH_LONG).show();
+ setResult(RESULT_OK);
+ finish();
+ }
+ }
+ else if(resultCode == ProviderAPI.INCORRECTLY_DOWNLOADED_JSON_FILES) {
+ //Toast.makeText(getApplicationContext(), R.string.incorrectly_downloaded_json_files_message, Toast.LENGTH_LONG).show();
+ mProgressDialog.dismiss();
+ setResult(RESULT_CANCELED, mConfigState);
+ }
+ else if(resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE) {
+ mProgressDialog.dismiss();
+ setResult(RESULT_OK);
+ showProviderDetails(getCurrentFocus());
+ } else if(resultCode == ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE) {
+ mProgressDialog.dismiss();
+ //Toast.makeText(getApplicationContext(), R.string.incorrectly_downloaded_certificate_message, Toast.LENGTH_LONG).show();
+ setResult(RESULT_CANCELED, mConfigState);
+ }
+ }
+
+ /**
+ * 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
+ ProviderItem selected_provider = getProvider(id);
+ mProgressDialog = ProgressDialog.show(this, getResources().getString(R.string.config_wait_title), getResources().getString(R.string.config_connecting_provider), true);
+ mSelectedProvider = selected_provider;
+ saveProviderJson(mSelectedProvider);
+ }
+
+ @Override
+ public void onBackPressed() {
+ try {
+ if(ConfigHelper.getJsonFromSharedPref(Provider.KEY) == null || ConfigHelper.getJsonFromSharedPref(Provider.KEY).length() == 0) {
+ askDashboardToQuitApp();
+ } else {
+ setResult(RESULT_OK);
+ }
+ } catch (JSONException e) {
+ askDashboardToQuitApp();
+ }
+ 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 id) {
+ Iterator<ProviderItem> providers_iterator = ProviderListContent.ITEMS.iterator();
+ while(providers_iterator.hasNext()) {
+ ProviderItem provider = providers_iterator.next();
+ if(provider.id.equalsIgnoreCase(id)) {
+ return provider;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 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), custom, false));
+ loaded_preseeded_providers = true;
+ }
+ } catch (IOException e) {
+ loaded_preseeded_providers = false;
+ }
+
+ return loaded_preseeded_providers;
+ }
+
+ /**
+ * Saves provider.json file associated with provider.
+ *
+ * If the provider is custom, the file has already been downloaded so we load it from memory.
+ * If not, the file is updated using the provider's URL.
+ * @param provider
+ */
+ private void saveProviderJson(ProviderItem provider) {
+ JSONObject provider_json = new JSONObject();
+ try {
+ if(!provider.custom) {
+ updateProviderDotJson(provider.name, provider.provider_json_url, provider.danger_on);
+ } else {
+ // FIXME!! We should we be updating our seeded providers list at ConfigurationWizard onStart() ?
+ // I think yes, but if so, where does this list live? leap.se, as it's the non-profit project for the software?
+ // If not, we should just be getting names/urls, and fetching the provider.json like in custom entries
+ provider_json = provider.provider_json;
+ ConfigHelper.saveSharedPref(Provider.KEY, provider_json);
+ ConfigHelper.saveSharedPref(EIP.ALLOWED_ANON, provider_json.getJSONObject(Provider.SERVICE).getBoolean(EIP.ALLOWED_ANON));
+ ConfigHelper.saveSharedPref(ProviderItem.DANGER_ON, provider.danger_on);
+
+ mProgressDialog.setMessage(getResources().getString(R.string.config_downloading_services));
+ downloadJSONFiles(mSelectedProvider);
+ }
+ } catch (JSONException e) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+
+ /**
+ * Asks ProviderAPI to download provider site's certificate and eip-service.json
+ *
+ * URLs are fetched from the provider parameter
+ * @param provider from which certificate and eip-service.json files are going to be downloaded
+ */
+ private void downloadJSONFiles(ProviderItem provider) {
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ Bundle parameters = new Bundle();
+
+ parameters.putString(Provider.KEY, provider.name);
+ parameters.putString(Provider.CA_CERT, provider.cert_json_url);
+ parameters.putString(EIP.KEY, provider.eip_service_json_url);
+ parameters.putBoolean(ProviderItem.DANGER_ON, provider.danger_on);
+
+ provider_API_command.setAction(ProviderAPI.DOWNLOAD_JSON_FILES_BUNDLE_EXTRA);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ startService(provider_API_command);
+ }
+
+ /**
+ * 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
+ * @param view from which the dialog is showed
+ */
+ public void addAndSelectNewProvider(View view) {
+ 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);
+ }
+
+ /**
+ * 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) {
+ 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);
+ }
+
+ @Override
+ public void saveAndSelectProvider(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.DOWNLOAD_NEW_PROVIDER_DOTJSON);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ startService(provider_API_command);
+ }
+
+ /**
+ * Asks ProviderAPI to download a new provider.json file
+ * @param provider_name
+ * @param provider_json_url
+ * @param danger_on tells if HTTPS client should bypass certificate errors
+ */
+ public void updateProviderDotJson(String provider_name, String provider_json_url, boolean danger_on) {
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ Bundle parameters = new Bundle();
+ parameters.putString(Provider.NAME, provider_name);
+ parameters.putString(Provider.DOT_JSON_URL, provider_json_url);
+ parameters.putBoolean(ProviderItem.DANGER_ON, danger_on);
+
+ provider_API_command.setAction(ProviderAPI.UPDATE_PROVIDER_DOTJSON);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ 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:
+ showAboutFragment(getCurrentFocus());
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * 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 showAboutFragment(View view) {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_about_fragment = getFragmentManager().findFragmentByTag(AboutFragment.TAG);
+ if (previous_about_fragment == null) {
+ fragment_transaction.addToBackStack(null);
+
+ Fragment newFragment = AboutFragment.newInstance();
+ fragment_transaction.replace(R.id.configuration_wizard_layout, newFragment, AboutFragment.TAG).commit();
+ }
+ }
+
+ @Override
+ public void login() {
+ Intent ask_login = new Intent();
+ ask_login.putExtra(LogInDialog.VERB, LogInDialog.VERB);
+ setResult(RESULT_OK, ask_login);
+ finish();
+ }
+
+ @Override
+ public void use_anonymously() {
+ setResult(RESULT_OK);
+ finish();
+ }
+}
diff --git a/src/se/leap/bitmaskclient/Dashboard.java b/src/se/leap/bitmaskclient/Dashboard.java new file mode 100644 index 00000000..97367e25 --- /dev/null +++ b/src/se/leap/bitmaskclient/Dashboard.java @@ -0,0 +1,354 @@ +/** + * 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.apache.http.cookie.Cookie; +import org.apache.http.impl.cookie.BasicClientCookie; +import org.json.JSONException; +import org.json.JSONObject; + +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.ProviderAPIResultReceiver.Receiver; +import se.leap.openvpn.MainActivity; +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.app.ProgressDialog; +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.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +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> + */ +public class Dashboard extends Activity implements LogInDialog.LogInDialogInterface,Receiver { + + protected static final int CONFIGURE_LEAP = 0; + + private static final String TAG_EIP_FRAGMENT = "EIP_DASHBOARD_FRAGMENT"; + final public static String SHARED_PREFERENCES = "LEAPPreferences"; + final public static String ACTION_QUIT = "quit"; + + private ProgressDialog mProgressDialog; + + private static Context app; + private static SharedPreferences preferences; + private static Provider provider; + + private TextView providerNameTV; + + private boolean authed = false; + + public ProviderAPIResultReceiver providerAPI_result_receiver; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + app = this; + + ConfigHelper.setSharedPreferences(getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE)); + preferences = ConfigHelper.shared_preferences; + + if (ConfigHelper.getStringFromSharedPref(Provider.KEY).isEmpty()) + startActivityForResult(new Intent(this,ConfigurationWizard.class),CONFIGURE_LEAP); + else + buildDashboard(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data){ + if ( requestCode == CONFIGURE_LEAP ) { + if ( resultCode == RESULT_OK ){ + startService( new Intent(EIP.ACTION_UPDATE_EIP_SERVICE) ); + buildDashboard(); + 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); + + FragmentManager fragMan = getFragmentManager(); + if ( provider.hasEIP()){ + EipServiceFragment eipFragment = new EipServiceFragment(); + fragMan.beginTransaction().replace(R.id.servicesCollection, eipFragment, TAG_EIP_FRAGMENT).commit(); + } + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + JSONObject provider_json; + try { + provider_json = ConfigHelper.getJsonFromSharedPref(Provider.KEY); + JSONObject service_description = provider_json.getJSONObject(Provider.SERVICE); + if(service_description.getBoolean(Provider.ALLOW_REGISTRATION)) { + if(authed) { + 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: + Fragment aboutFragment = new AboutFragment(); + FragmentTransaction trans = getFragmentManager().beginTransaction(); + trans.replace(R.id.dashboardLayout, aboutFragment); + trans.addToBackStack(null); + trans.commit(); + return true; + case R.id.legacy_interface: + intent = new Intent(this,MainActivity.class); + startActivity(intent); + return true; + case R.id.switch_provider: + startActivityForResult(new Intent(this,ConfigurationWizard.class),CONFIGURE_LEAP); + 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) { + 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); + + if(mProgressDialog != null) mProgressDialog.dismiss(); + mProgressDialog = ProgressDialog.show(this, getResources().getString(R.string.authenticating_title), getResources().getString(R.string.authenticating_message), true); + startService(provider_API_command); + } + + /** + * 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(mProgressDialog != null) mProgressDialog.dismiss(); + mProgressDialog = ProgressDialog.show(this, getResources().getString(R.string.logout_title), getResources().getString(R.string.logout_message), true); + startService(provider_API_command); + } + + /** + * 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 = true; + invalidateOptionsMenu(); + + //Cookie session_id = new BasicClientCookie(session_id_cookie_key, session_id_string); + downloadAuthedUserCertificate(/*session_id*/); + } else if(resultCode == ProviderAPI.SRP_AUTHENTICATION_FAILED) { + mProgressDialog.dismiss(); + logInDialog(getCurrentFocus(), resultData); + } else if(resultCode == ProviderAPI.LOGOUT_SUCCESSFUL) { + authed = false; + invalidateOptionsMenu(); + setResult(RESULT_OK); + mProgressDialog.dismiss(); + } else if(resultCode == ProviderAPI.LOGOUT_FAILED) { + setResult(RESULT_CANCELED); + mProgressDialog.dismiss(); + Toast.makeText(getApplicationContext(), R.string.log_out_failed_message, Toast.LENGTH_LONG).show(); + } else if(resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE) { + setResult(RESULT_OK); + mProgressDialog.dismiss(); + Toast.makeText(getApplicationContext(), R.string.successful_authed_cert_downloaded_message, Toast.LENGTH_LONG).show(); + } else if(resultCode == ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE) { + setResult(RESULT_CANCELED); + mProgressDialog.dismiss(); + Toast.makeText(getApplicationContext(), R.string.authed_cert_download_failed_message, Toast.LENGTH_LONG).show(); + } + } + + /** + * 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; + } + +} diff --git a/src/se/leap/bitmaskclient/EIP.java b/src/se/leap/bitmaskclient/EIP.java new file mode 100644 index 00000000..c643d501 --- /dev/null +++ b/src/se/leap/bitmaskclient/EIP.java @@ -0,0 +1,520 @@ +/** + * 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.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.NoSuchElementException; +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.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 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"; + + 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(); + + try { + eipDefinition = ConfigHelper.getJsonFromSharedPref(KEY); + parsedEipSerial = ConfigHelper.getIntFromSharedPref(PARSED_SERIAL); + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + 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 void retreiveVpnService() { + Intent bindIntent = new Intent(this,OpenVpnService.class); + bindIntent.setAction(OpenVpnService.RETRIEVE_SERVICE); + bindService(bindIntent, mVpnServiceConn, 0); + } + + 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; + 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, EIP_NOTIFICATION); + 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; + } else { + mPending = ACTION_IS_EIP_RUNNING; + this.retreiveVpnService(); + } + + if (mReceiver != null){ + mReceiver.send(resultCode, resultData); + } + } + + /** + * Initiates an EIP connection by selecting a gateway and preparing and sending an + * Intent to {@link se.leap.openvpn.LaunchVPN} + */ + private void startEIP() { + if (activeGateway==null) + 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 = ConfigHelper.getJsonFromSharedPref(EIP.KEY); + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + 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 Implement gateway selection logic based on TZ or preferences + // TODO Implement search through gateways loaded from SharedPreferences + // TODO Remove String arg constructor in favor of findGatewayByName(String) + return new OVPNGateway("first"); + } + + /** + * 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(); + } + } + + ConfigHelper.saveSharedPref(PARSED_SERIAL, eipDefinition.optInt(Provider.API_RETURN_SERIAL)); + } + + /** + * 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 == "first" ) { + mName = vpl.getProfiles().iterator().next().mName; + } + + 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.contains( 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"; + + 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(); + + 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/src/se/leap/bitmaskclient/EipServiceFragment.java b/src/se/leap/bitmaskclient/EipServiceFragment.java new file mode 100644 index 00000000..daf446a5 --- /dev/null +++ b/src/se/leap/bitmaskclient/EipServiceFragment.java @@ -0,0 +1,265 @@ +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.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, OnClickListener, OnCheckedChangeListener { + + private static final String IS_EIP_PENDING = "is_eip_pending"; + + private View eipFragment; + private Switch eipSwitch; + private View eipDetail; + private TextView eipStatus; + + private boolean eipAutoSwitched = true; + + private boolean mEipStartPending = false; + + private EIPReceiver mEIPReceiver; + + @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); + eipStatus.setOnClickListener(this); + + 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); + } + + @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 onClick(View buttonView) { + if (buttonView.equals(eipStatus)) + showEIPLog(); + } + + /** + * Launches the se.leap.openvpn.LogWindow Activity showing detailed OpenVPN log + */ + public void showEIPLog(){ + Intent intent = new Intent(getActivity().getBaseContext(),LogWindow.class); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + startActivity(intent); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (buttonView.equals(eipSwitch) && !eipAutoSwitched){ + 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); + } + }) + .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); + } + } + } + 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 = "Connection Secure"; + getActivity().findViewById(R.id.eipProgress).setVisibility(View.GONE); + mEipStartPending = false; + } else if (state.equals("BYTECOUNT")) { + statusMessage = logmessage; + } else if ( (state.equals("NOPROCESS") && !mEipStartPending ) || state.equals("EXITING")) { + statusMessage = "Not running! Connection not secure!"; + getActivity().findViewById(R.id.eipProgress).setVisibility(View.GONE); + mEipStartPending = false; + switchState = false; + } 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; + } + } +} diff --git a/src/se/leap/bitmaskclient/LeapHttpClient.java b/src/se/leap/bitmaskclient/LeapHttpClient.java new file mode 100644 index 00000000..837da236 --- /dev/null +++ b/src/se/leap/bitmaskclient/LeapHttpClient.java @@ -0,0 +1,84 @@ +/** + * 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 { + final Context context; + + 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(Context context) { + if(client == null) { + client = new LeapHttpClient(context); + String cert_string = ConfigHelper.getStringFromSharedPref(Provider.CA_CERT); + 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); + } + } + + public LeapHttpClient(Context context) { + this.context = context; + } +} diff --git a/src/se/leap/bitmaskclient/LeapSRPSession.java b/src/se/leap/bitmaskclient/LeapSRPSession.java new file mode 100644 index 00000000..9260c81f --- /dev/null +++ b/src/se/leap/bitmaskclient/LeapSRPSession.java @@ -0,0 +1,326 @@ +/** + * 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 { + + final public static String SALT = "salt"; + final public static String M1 = "M1"; + final public static String M2 = "M2"; + + 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[] 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); + + byte[] 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; + } + + /** + * @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/src/se/leap/bitmaskclient/LogInDialog.java b/src/se/leap/bitmaskclient/LogInDialog.java new file mode 100644 index 00000000..3651e9ba --- /dev/null +++ b/src/se/leap/bitmaskclient/LogInDialog.java @@ -0,0 +1,139 @@ +/** + * 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"; + + + 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)); + } + + + 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().trim(); + String password = password_field.getText().toString().trim(); + 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); + } + + 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"); + } + } +} diff --git a/src/se/leap/bitmaskclient/NewProviderDialog.java b/src/se/leap/bitmaskclient/NewProviderDialog.java new file mode 100644 index 00000000..f77cb6d4 --- /dev/null +++ b/src/se/leap/bitmaskclient/NewProviderDialog.java @@ -0,0 +1,110 @@ +/** + * 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.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +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 saveAndSelectProvider(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); + final CheckBox danger_checkbox = (CheckBox)new_provider_dialog_view.findViewById(R.id.danger_checkbox); + + 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://")) { + entered_url = "https://".concat(entered_url); + } + boolean danger_on = danger_checkbox.isChecked(); + if(validURL(entered_url)) { + interface_with_ConfigurationWizard.saveAndSelectProvider(entered_url, danger_on); + Toast.makeText(getActivity().getApplicationContext(), R.string.valid_url_entered, Toast.LENGTH_LONG).show(); + } else { + url_input_field.setText(""); + 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(); + } +} diff --git a/src/se/leap/bitmaskclient/Provider.java b/src/se/leap/bitmaskclient/Provider.java new file mode 100644 index 00000000..e462829f --- /dev/null +++ b/src/se/leap/bitmaskclient/Provider.java @@ -0,0 +1,208 @@ +/** + * 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", "") ); + } 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 (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + 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/src/se/leap/bitmaskclient/ProviderAPI.java b/src/se/leap/bitmaskclient/ProviderAPI.java new file mode 100644 index 00000000..39b44e24 --- /dev/null +++ b/src/se/leap/bitmaskclient/ProviderAPI.java @@ -0,0 +1,780 @@ +/** + * 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.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.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; +import android.widget.Toast; + +/** + * 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 + DOWNLOAD_JSON_FILES_BUNDLE_EXTRA = "downloadJSONFiles", + UPDATE_PROVIDER_DOTJSON = "updateProviderDotJSON", + 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" + ; + + final public static int + CUSTOM_PROVIDER_ADDED = 0, + CORRECTLY_DOWNLOADED_JSON_FILES = 1, + INCORRECTLY_DOWNLOADED_JSON_FILES = 2, + 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, + CORRECTLY_UPDATED_PROVIDER_DOT_JSON = 11, + INCORRECTLY_UPDATED_PROVIDER_DOT_JSON = 12, + CORRECTLY_DOWNLOADED_ANON_CERTIFICATE = 13, + INCORRECTLY_DOWNLOADED_ANON_CERTIFICATE = 14 + ; + + 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) ); + } + + private void displayToast(final int toast_string_id) { + mHandler.post(new Runnable() { + + @Override + public void run() { + Toast.makeText(ProviderAPI.this, toast_string_id, Toast.LENGTH_LONG).show(); + } + }); + } + + @Override + protected void onHandleIntent(Intent command) { + final ResultReceiver receiver = command.getParcelableExtra("receiver"); + String action = command.getAction(); + Bundle parameters = command.getBundleExtra(PARAMETERS); + + if(action.equalsIgnoreCase(DOWNLOAD_JSON_FILES_BUNDLE_EXTRA)) { + if(!downloadJsonFiles(parameters)) { + receiver.send(INCORRECTLY_DOWNLOADED_JSON_FILES, Bundle.EMPTY); + } else { + receiver.send(CORRECTLY_DOWNLOADED_JSON_FILES, Bundle.EMPTY); + } + } else if(action.equalsIgnoreCase(UPDATE_PROVIDER_DOTJSON)) { + Bundle result = updateProviderDotJSON(parameters); + if(result.getBoolean(RESULT_KEY)) { + receiver.send(CORRECTLY_UPDATED_PROVIDER_DOT_JSON, result); + } else { + receiver.send(INCORRECTLY_UPDATED_PROVIDER_DOT_JSON, Bundle.EMPTY); + } + } else if (action.equalsIgnoreCase(DOWNLOAD_NEW_PROVIDER_DOTJSON)) { + Bundle result = downloadNewProviderDotJSON(parameters); + if(result.getBoolean(RESULT_KEY)) { + receiver.send(CORRECTLY_UPDATED_PROVIDER_DOT_JSON, result); + } else { + receiver.send(INCORRECTLY_DOWNLOADED_JSON_FILES, Bundle.EMPTY); + } + } 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); + } + } + } + + /** + * Downloads the main cert and the eip-service.json files given through the task parameter + * @param task + * @return true if eip-service.json was parsed as a JSON object correctly. + */ + private boolean downloadJsonFiles(Bundle task) { + String cert_url = task.getString(Provider.CA_CERT); + String eip_service_json_url = task.getString(EIP.KEY); + boolean danger_on = task.getBoolean(ProviderItem.DANGER_ON); + try { + String cert_string = downloadWithCommercialCA(cert_url, danger_on); + if(cert_string.isEmpty()) return false; + X509Certificate certCert = ConfigHelper.parseX509CertificateFromString(cert_string); + cert_string = Base64.encodeToString( certCert.getEncoded(), Base64.DEFAULT); + ConfigHelper.saveSharedPref(Provider.CA_CERT, "-----BEGIN CERTIFICATE-----\n"+cert_string+"-----END CERTIFICATE-----"); + + String eip_service_string = downloadWithCommercialCA(eip_service_json_url, danger_on); + ConfigHelper.saveSharedPref(EIP.KEY, new JSONObject(eip_service_string)); + + return true; + } catch (JSONException e) { + return false; + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } + } + + /** + * 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(); + + 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(); + try { + JSONObject saltAndB = sendAToSRPServer(authentication_server, username, new BigInteger(1, A).toString(16)); + if(saltAndB.length() > 0) { + String salt = saltAndB.getString(LeapSRPSession.SALT); + byte[] Bbytes = new BigInteger(saltAndB.getString("B"), 16).toByteArray(); + byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes); + 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); + } 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.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; + } + + /** + * 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)); + } + 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 updateProviderDotJSON(Bundle task) { + Bundle result = new Bundle(); + boolean custom = task.getBoolean(ProviderItem.CUSTOM); + boolean danger_on = task.getBoolean(ProviderItem.DANGER_ON); + String provider_json_url = task.getString(Provider.DOT_JSON_URL); + String provider_name = task.getString(Provider.NAME); + + try { + String provider_dot_json_string = downloadWithCommercialCA(provider_json_url, danger_on); + if(provider_dot_json_string.isEmpty()) { + result.putBoolean(RESULT_KEY, false); + } else { + JSONObject provider_json = new JSONObject(provider_dot_json_string); + ConfigHelper.saveSharedPref(EIP.ALLOWED_ANON, provider_json.getJSONObject(Provider.SERVICE).getBoolean(EIP.ALLOWED_ANON)); + + //ProviderListContent.addItem(new ProviderItem(provider_name, provider_json_url, provider_json, custom, danger_on)); + result.putBoolean(RESULT_KEY, true); + result.putString(Provider.KEY, provider_json.toString()); + result.putBoolean(ProviderItem.DANGER_ON, danger_on); + } + } catch (JSONException e) { + result.putBoolean(RESULT_KEY, false); + } + + return result; + } + + /** + * Downloads a custom provider provider.json file + * @param task containing a boolean meaning if the user completely trusts this provider, and the provider main url entered in the new custom provider dialog. + * @return true if provider.json file was successfully parsed as a JSON object. + */ + private Bundle downloadNewProviderDotJSON(Bundle task) { + Bundle result = new Bundle(); + boolean custom = true; + boolean danger_on = task.getBoolean(ProviderItem.DANGER_ON); + + String provider_main_url = (String) task.get(Provider.MAIN_URL); + String provider_name = provider_main_url.replaceFirst("http[s]?://", "").replaceFirst("\\/", "_"); + String provider_json_url = guessProviderDotJsonURL(provider_main_url); + + String provider_json_string = downloadWithCommercialCA(provider_json_url, danger_on); + try { + if(provider_json_string.isEmpty()) { + result.putBoolean(RESULT_KEY, false); + } else { + JSONObject provider_json = new JSONObject(provider_json_string); + + ConfigHelper.saveSharedPref(Provider.KEY, provider_json); + ConfigHelper.saveSharedPref(ProviderItem.DANGER_ON, danger_on); + ConfigHelper.saveSharedPref(EIP.ALLOWED_ANON, provider_json.getJSONObject(Provider.SERVICE).getBoolean(EIP.ALLOWED_ANON)); + ProviderItem added_provider = new ProviderItem(provider_name, provider_json_url, provider_json, custom, danger_on); + ProviderListContent.addItem(added_provider); + + result.putString(Provider.NAME, added_provider.getName()); + result.putBoolean(RESULT_KEY, true); + result.putString(Provider.KEY, provider_json.toString()); + result.putBoolean(ProviderItem.DANGER_ON, danger_on); + } + } catch (JSONException e) { + result.putBoolean(RESULT_KEY, false); + } + + return result; + } + + /** + * 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); + json_file_content = new Scanner(url_connection.getInputStream()).useDelimiter("\\A").next(); + } catch (MalformedURLException e) { + displayToast(R.string.malformed_url); + } catch(SocketTimeoutException e) { + displayToast(R.string.server_is_down_message); + } catch (FileNotFoundException e) { + e.printStackTrace(); + displayToast(R.string.server_is_down_message); + } catch (IOException e) { + if(provider_url != null) { + json_file_content = downloadWithProviderCA(provider_url, danger_on); + } else { + displayToast(R.string.certificate_error); + } + } catch (Exception e) { + if(provider_url != null && danger_on) { + json_file_content = downloadWithProviderCA(provider_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 + * @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(URL url, boolean danger_on) { + String json_file_content = ""; + + try { + // Tell the URLConnection to use a SocketFactory from our SSLContext + HttpsURLConnection urlConnection = + (HttpsURLConnection)url.openConnection(); + urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory()); + json_file_content = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next(); + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (UnknownHostException e) { + displayToast(R.string.server_is_down_message); + } catch (FileNotFoundException e) { + e.printStackTrace(); + displayToast(R.string.server_is_down_message); + } catch (IOException e) { + // The downloaded certificate doesn't validate our https connection. + if(danger_on) { + json_file_content = downloadWithoutCA(url); + } else { + displayToast(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 = ConfigHelper.getStringFromSharedPref(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(URL url) { + 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()); + + 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(); + displayToast(R.string.server_is_down_message); + } catch (IOException e) { + // The downloaded certificate doesn't validate our https connection. + e.printStackTrace(); + displayToast(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; + } + + /** + * Tries to guess the provider.json url given the main provider url. + * @param provider_main_url + * @return the guessed provider.json url + */ + private String guessProviderDotJsonURL(String provider_main_url) { + return provider_main_url + "/provider.json"; + } + + /** + * 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"; + + HttpsURLConnection urlConnection = (HttpsURLConnection)new URL(delete_url).openConnection(); + urlConnection.setRequestMethod("DELETE"); + urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory()); + + int responseCode = urlConnection.getResponseCode(); + Log.d("logout", 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 = ConfigHelper.getJsonFromSharedPref(Provider.KEY); + URL provider_main_url = new URL(provider_json.getString(Provider.API_URL)); + String new_cert_string_url = provider_main_url.toString() + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.CERTIFICATE; + + boolean danger_on = ConfigHelper.getBoolFromSharedPref(ProviderItem.DANGER_ON); + String cert_string = downloadWithCommercialCA(new_cert_string_url, danger_on); + if(!cert_string.isEmpty()) { + // 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 ); + ConfigHelper.saveSharedPref(EIP.PRIVATE_KEY, "-----BEGIN RSA PRIVATE KEY-----\n"+keyString+"-----END RSA PRIVATE KEY-----"); + + X509Certificate certCert = ConfigHelper.parseX509CertificateFromString(certificateString); + certificateString = Base64.encodeToString( certCert.getEncoded(), Base64.DEFAULT); + ConfigHelper.saveSharedPref(EIP.CERTIFICATE, "-----BEGIN CERTIFICATE-----\n"+certificateString+"-----END CERTIFICATE-----"); + + 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/src/se/leap/bitmaskclient/ProviderAPIResultReceiver.java b/src/se/leap/bitmaskclient/ProviderAPIResultReceiver.java new file mode 100644 index 00000000..7b256124 --- /dev/null +++ b/src/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/src/se/leap/bitmaskclient/ProviderDetailFragment.java b/src/se/leap/bitmaskclient/ProviderDetailFragment.java new file mode 100644 index 00000000..4006a76c --- /dev/null +++ b/src/se/leap/bitmaskclient/ProviderDetailFragment.java @@ -0,0 +1,114 @@ +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.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 = ConfigHelper.getJsonFromSharedPref(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);
+ ConfigHelper.removeFromSharedPref(Provider.KEY);
+ ConfigHelper.removeFromSharedPref(ProviderItem.DANGER_ON);
+ ConfigHelper.removeFromSharedPref(EIP.ALLOWED_ANON);
+ ConfigHelper.removeFromSharedPref(EIP.KEY);
+ }
+
+ 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();
+ }
+
+ ProviderDetailFragmentInterface interface_with_configuration_wizard;
+}
diff --git a/src/se/leap/bitmaskclient/ProviderListContent.java b/src/se/leap/bitmaskclient/ProviderListContent.java new file mode 100644 index 00000000..940a594d --- /dev/null +++ b/src/se/leap/bitmaskclient/ProviderListContent.java @@ -0,0 +1,146 @@ +/**
+ * 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);
+ }
+
+ /**
+ * A provider item.
+ */
+ public static class ProviderItem {
+
+
+ final public static String CUSTOM = "custom";
+ final public static String DANGER_ON = "danger_on";
+
+ public boolean custom = false;
+ public String id;
+ public String name;
+ public String domain;
+ public String provider_json_url;
+ public JSONObject provider_json;
+ public String provider_json_filename;
+ public String eip_service_json_url;
+ public String cert_json_url;
+ public boolean danger_on = false;
+
+ /**
+ * @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, boolean custom, boolean danger_on) {
+
+ 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);
+ id = name;
+ this.name = name;
+ provider_json_url = file_contents.getString(Provider.DOT_JSON_URL);
+ domain = new URL(provider_json_url).getHost();
+ //provider_json_filename = file_contents.getString("assets_json_provider");
+ eip_service_json_url = file_contents.getString("json_eip_service");
+ cert_json_url = file_contents.getString(EIP.CERTIFICATE);
+ this.custom = custom;
+ this.danger_on = danger_on;
+ } catch (MalformedURLException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } 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_json_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
+ * @param danger_on if the user trusts completely the new provider
+ */
+ public ProviderItem(String name, String provider_json_url, JSONObject provider_json, boolean custom, boolean danger_on) {
+
+ try {
+ id = name;
+ //this.name = name;
+ this.provider_json_url = provider_json_url;
+ this.provider_json = provider_json;
+ this.name = provider_json.getJSONObject("name").getString("en");
+ domain = new URL(provider_json_url).getHost();
+ eip_service_json_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH;
+ cert_json_url = provider_json.getString("ca_cert_uri");
+ this.custom = custom;
+ this.danger_on = danger_on;
+ if(custom)
+ provider_json_filename = name + "_provider.json".replaceFirst("__", "_");
+ } catch (MalformedURLException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public String getName() {
+ return id;
+ }
+ }
+}
diff --git a/src/se/leap/bitmaskclient/ProviderListFragment.java b/src/se/leap/bitmaskclient/ProviderListFragment.java new file mode 100644 index 00000000..82c4bdc8 --- /dev/null +++ b/src/se/leap/bitmaskclient/ProviderListFragment.java @@ -0,0 +1,192 @@ +/**
+ * 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.ArrayAdapter;
+import android.widget.ListView;
+import android.content.Context;
+import android.widget.TwoLineListItem;
+
+/**
+ * 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 {
+
+ private ArrayAdapter<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);
+ content_adapter = new ArrayAdapter<ProviderListContent.ProviderItem>(
+ getActivity(),
+ android.R.layout.simple_list_item_activated_2,
+ ProviderListContent.ITEMS) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent){
+ TwoLineListItem row;
+ if (convertView == null) {
+ LayoutInflater inflater = (LayoutInflater)getActivity().getApplicationContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ row = (TwoLineListItem)inflater.inflate(android.R.layout.simple_list_item_2, null);
+ } else {
+ row = (TwoLineListItem)convertView;
+ }
+ ProviderListContent.ProviderItem data = ProviderListContent.ITEMS.get(position);
+ row.getText1().setText(data.domain);
+ row.getText2().setText(data.name);
+
+ return row;
+ }
+ };
+ 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));
+ }
+ }
+
+ @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).id);
+ }
+
+ @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);
+ }
+ }
+
+ /**
+ * 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;
+ }
+}
|