From 6543f487c993002d5cc9cb4cec8e45f638d15be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Parm=C3=A9nides=20GV?= Date: Wed, 9 Apr 2014 12:13:09 +0200 Subject: Danger code removed from release apk. I've copied all files containing "danger" to both release and debug java folders (solution learnt from http://stackoverflow.com/questions/18782368/android-gradle-buildtypes-duplicate-class), and removed every trace of any "danger" related code in the release apk. This way, we make sure nobody can exploit that unused variable in production mode, so that if Android fails to protect our app (people have been able to introduce code into signed apks, so it's not impossible), we make it difficult to bypass the SSL certificate verification. You can call me paranoid if you want ;) --- .../se/leap/bitmaskclient/ConfigurationWizard.java | 574 ++++++++++++++ .../se/leap/bitmaskclient/NewProviderDialog.java | 117 +++ .../java/se/leap/bitmaskclient/ProviderAPI.java | 860 +++++++++++++++++++++ .../leap/bitmaskclient/ProviderDetailFragment.java | 115 +++ .../se/leap/bitmaskclient/ProviderListContent.java | 110 +++ 5 files changed, 1776 insertions(+) create mode 100644 bitmask_android/src/release/java/se/leap/bitmaskclient/ConfigurationWizard.java create mode 100644 bitmask_android/src/release/java/se/leap/bitmaskclient/NewProviderDialog.java create mode 100644 bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderAPI.java create mode 100644 bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderDetailFragment.java create mode 100644 bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderListContent.java (limited to 'bitmask_android/src/release') diff --git a/bitmask_android/src/release/java/se/leap/bitmaskclient/ConfigurationWizard.java b/bitmask_android/src/release/java/se/leap/bitmaskclient/ConfigurationWizard.java new file mode 100644 index 00000000..15a135d2 --- /dev/null +++ b/bitmask_android/src/release/java/se/leap/bitmaskclient/ConfigurationWizard.java @@ -0,0 +1,574 @@ +/** + * 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 . + */ + package se.leap.bitmaskclient; + + + + + + +import android.app.Activity; +import android.app.DialogFragment; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.AssetManager; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.Display; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View.MeasureSpec; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Iterator; +import org.json.JSONException; +import org.json.JSONObject; +import se.leap.bitmaskclient.DownloadFailedDialog.DownloadFailedDialogInterface; +import se.leap.bitmaskclient.NewProviderDialog.NewProviderDialogInterface; +import se.leap.bitmaskclient.ProviderAPIResultReceiver.Receiver; +import se.leap.bitmaskclient.ProviderDetailFragment.ProviderDetailFragmentInterface; +import se.leap.bitmaskclient.ProviderListContent.ProviderItem; +import se.leap.bitmaskclient.R; + +/** + * Activity that builds and shows the list of known available providers. + * + * It also allows the user to enter custom providers with a button. + * + * @author parmegv + * + */ +public class ConfigurationWizard extends Activity +implements ProviderListFragment.Callbacks, NewProviderDialogInterface, ProviderDetailFragmentInterface, DownloadFailedDialogInterface, Receiver { + + private ProgressBar mProgressBar; + private TextView progressbar_description; + private ProviderListFragment provider_list_fragment; + private Intent mConfigState = new Intent(); + + final public static String TAG = "se.leap.bitmaskclient.ConfigurationWizard"; + final public static String TYPE_OF_CERTIFICATE = "type_of_certificate"; + final public static String ANON_CERTIFICATE = "anon_certificate"; + final public static String AUTHED_CERTIFICATE = "authed_certificate"; + + final protected static String PROVIDER_SET = "PROVIDER SET"; + final protected static String SERVICES_RETRIEVED = "SERVICES RETRIEVED"; + + public ProviderAPIResultReceiver providerAPI_result_receiver; + private ProviderAPIBroadcastReceiver_Update providerAPI_broadcast_receiver_update; + + private static SharedPreferences preferences; + private static boolean setting_up_provider = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + preferences = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE); + + setContentView(R.layout.configuration_wizard_activity); + mProgressBar = (ProgressBar) findViewById(R.id.progressbar_configuration_wizard); + mProgressBar.setVisibility(ProgressBar.INVISIBLE); + progressbar_description = (TextView) findViewById(R.id.progressbar_description); + progressbar_description.setVisibility(TextView.INVISIBLE); + providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler()); + providerAPI_result_receiver.setReceiver(this); + providerAPI_broadcast_receiver_update = new ProviderAPIBroadcastReceiver_Update(); + IntentFilter update_intent_filter = new IntentFilter(ProviderAPI.UPDATE_PROGRESSBAR); + update_intent_filter.addCategory(Intent.CATEGORY_DEFAULT); + registerReceiver(providerAPI_broadcast_receiver_update, update_intent_filter); + + loadPreseededProviders(); + + // Only create our fragments if we're not restoring a saved instance + if ( savedInstanceState == null ){ + // TODO Some welcome screen? + // We will need better flow control when we have more Fragments (e.g. user auth) + provider_list_fragment = ProviderListFragment.newInstance(); + Bundle arguments = new Bundle(); + int configuration_wizard_request_code = getIntent().getIntExtra(Dashboard.REQUEST_CODE, -1); + if(configuration_wizard_request_code == Dashboard.SWITCH_PROVIDER) { + arguments.putBoolean(ProviderListFragment.SHOW_ALL_PROVIDERS, true); + } + provider_list_fragment.setArguments(arguments); + + FragmentManager fragmentManager = getFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.configuration_wizard_layout, provider_list_fragment, ProviderListFragment.TAG) + .commit(); + } + + // TODO: If exposing deep links into your app, handle intents here. + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unregisterReceiver(providerAPI_broadcast_receiver_update); + } + + public void refreshProviderList(int top_padding) { + ProviderListFragment new_provider_list_fragment = new ProviderListFragment(); + Bundle top_padding_bundle = new Bundle(); + top_padding_bundle.putInt(ProviderListFragment.TOP_PADDING, top_padding); + new_provider_list_fragment.setArguments(top_padding_bundle); + + FragmentManager fragmentManager = getFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.configuration_wizard_layout, new_provider_list_fragment, ProviderListFragment.TAG) + .commit(); + } + + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + if(resultCode == ProviderAPI.PROVIDER_OK) { + mConfigState.setAction(PROVIDER_SET); + + if (preferences.getBoolean(EIP.ALLOWED_ANON, false)){ + mConfigState.putExtra(SERVICES_RETRIEVED, true); + downloadAnonCert(); + } else { + mProgressBar.incrementProgressBy(1); + mProgressBar.setVisibility(ProgressBar.GONE); + progressbar_description.setVisibility(TextView.GONE); + setResult(RESULT_OK); + showProviderDetails(getCurrentFocus()); + } + } else if(resultCode == ProviderAPI.PROVIDER_NOK) { + //refreshProviderList(0); + String reason_to_fail = resultData.getString(ProviderAPI.ERRORS); + showDownloadFailedDialog(getCurrentFocus(), reason_to_fail); + mProgressBar.setVisibility(ProgressBar.GONE); + progressbar_description.setVisibility(TextView.GONE); + preferences.edit().remove(Provider.KEY).commit(); + setting_up_provider = false; + setResult(RESULT_CANCELED, mConfigState); + } + else if(resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE) { + mProgressBar.incrementProgressBy(1); + mProgressBar.setVisibility(ProgressBar.GONE); + progressbar_description.setVisibility(TextView.GONE); + //refreshProviderList(0); + setResult(RESULT_OK); + showProviderDetails(getCurrentFocus()); + } else if(resultCode == ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE) { + //refreshProviderList(0); + mProgressBar.setVisibility(ProgressBar.GONE); + progressbar_description.setVisibility(TextView.GONE); + //Toast.makeText(getApplicationContext(), R.string.incorrectly_downloaded_certificate_message, Toast.LENGTH_LONG).show(); + setResult(RESULT_CANCELED, mConfigState); + } else if(resultCode == AboutActivity.VIEWED) { + // Do nothing, right now + // I need this for CW to wait for the About activity to end before going back to Dashboard. + } + } + + /** + * Callback method from {@link ProviderListFragment.Callbacks} + * indicating that the item with the given ID was selected. + */ + @Override + public void onItemSelected(String id) { + //TODO Code 2 pane view + // resetOldConnection(); + ProviderItem selected_provider = getProvider(id); + int provider_index = getProviderIndex(id); + + + startProgressBar(provider_index+1); + provider_list_fragment.hideAllBut(provider_index); + + setUpProvider(selected_provider.providerMainUrl()); + } + + @Override + public void onBackPressed() { + if(setting_up_provider) { + stopSettingUpProvider(); + } else { + usualBackButton(); + } + } + + private void stopSettingUpProvider() { + ProviderAPI.stop(); + mProgressBar.setVisibility(ProgressBar.GONE); + mProgressBar.setProgress(0); + progressbar_description.setVisibility(TextView.GONE); + getSharedPreferences(Dashboard.SHARED_PREFERENCES, Activity.MODE_PRIVATE).edit().remove(Provider.KEY).commit(); + setting_up_provider = false; + showAllProviders(); + } + + private void usualBackButton() { + try { + boolean is_provider_set_up = new JSONObject(preferences.getString(Provider.KEY, "no provider")) != null ? true : false; + boolean is_provider_set_up_truly = new JSONObject(preferences.getString(Provider.KEY, "no provider")).length() != 0 ? true : false; + if(!is_provider_set_up || !is_provider_set_up_truly) { + askDashboardToQuitApp(); + } else { + setResult(RESULT_OK); + } + } catch (JSONException e) { + askDashboardToQuitApp(); + super.onBackPressed(); + e.printStackTrace(); + } + super.onBackPressed(); + } + private void askDashboardToQuitApp() { + Intent ask_quit = new Intent(); + ask_quit.putExtra(Dashboard.ACTION_QUIT, Dashboard.ACTION_QUIT); + setResult(RESULT_CANCELED, ask_quit); + } + + private ProviderItem getProvider(String name) { + Iterator providers_iterator = ProviderListContent.ITEMS.iterator(); + while(providers_iterator.hasNext()) { + ProviderItem provider = providers_iterator.next(); + if(provider.name().equalsIgnoreCase(name)) { + return provider; + } + } + return null; + } + + private String getId(String provider_main_url_string) { + try { + URL provider_url = new URL(provider_main_url_string); + URL aux_provider_url; + Iterator providers_iterator = ProviderListContent.ITEMS.iterator(); + while(providers_iterator.hasNext()) { + ProviderItem provider = providers_iterator.next(); + aux_provider_url = new URL(provider.providerMainUrl()); + if(isSameURL(provider_url, aux_provider_url)) { + return provider.name(); + } + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return ""; + } + + /** + * Checks, whether 2 urls are pointing to the same location. + * + * @param url a url + * @param baseUrl an other url, that should be compared. + * @return true, if the urls point to the same host and port and use the + * same protocol, false otherwise. + */ + private boolean isSameURL(final URL url, final URL baseUrl) { + if (!url.getProtocol().equals(baseUrl.getProtocol())) { + return false; + } + if (!url.getHost().equals(baseUrl.getHost())) { + return false; + } + if (url.getPort() != baseUrl.getPort()) { + return false; + } + return true; + } + + private void startProgressBar() { + mProgressBar.setVisibility(ProgressBar.VISIBLE); + mProgressBar.setProgress(0); + mProgressBar.setMax(3); + } + + private void startProgressBar(int list_item_index) { + mProgressBar.setVisibility(ProgressBar.VISIBLE); + progressbar_description.setVisibility(TextView.VISIBLE); + mProgressBar.setProgress(0); + mProgressBar.setMax(3); + int measured_height = listItemHeight(list_item_index); + mProgressBar.setTranslationY(measured_height); + progressbar_description.setTranslationY(measured_height + mProgressBar.getHeight()); + } + + private int getProviderIndex(String id) { + int index = 0; + Iterator providers_iterator = ProviderListContent.ITEMS.iterator(); + while(providers_iterator.hasNext()) { + ProviderItem provider = providers_iterator.next(); + if(provider.name().equalsIgnoreCase(id)) { + break; + } else index++; + } + return index; + } + + private int listItemHeight(int list_item_index) { + ListView provider_list_view = (ListView)findViewById(android.R.id.list); + ListAdapter provider_list_adapter = provider_list_view.getAdapter(); + View listItem = provider_list_adapter.getView(0, null, provider_list_view); + listItem.setLayoutParams(new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT)); + WindowManager wm = (WindowManager) getApplicationContext() + .getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + int screenWidth = display.getWidth(); // deprecated + + int listViewWidth = screenWidth - 10 - 10; + int widthSpec = MeasureSpec.makeMeasureSpec(listViewWidth, + MeasureSpec.AT_MOST); + listItem.measure(widthSpec, 0); + + return listItem.getMeasuredHeight(); +} + + /** + * Loads providers data from url file contained in the project + * @return true if the file was read correctly + */ + private boolean loadPreseededProviders() { + boolean loaded_preseeded_providers = false; + AssetManager asset_manager = getAssets(); + String[] urls_filepaths = null; + try { + String url_files_folder = "urls"; + //TODO Put that folder in a better place (also inside the "for") + urls_filepaths = asset_manager.list(url_files_folder); + String provider_name = ""; + for(String url_filepath : urls_filepaths) + { + boolean custom = false; + provider_name = url_filepath.subSequence(0, url_filepath.indexOf(".")).toString(); + if(ProviderListContent.ITEMS.isEmpty()) //TODO I have to implement a way of checking if a provider new or is already present in that ITEMS list + ProviderListContent.addItem(new ProviderItem(provider_name, asset_manager.open(url_files_folder + "/" + url_filepath))); + loaded_preseeded_providers = true; + } + } catch (IOException e) { + loaded_preseeded_providers = false; + } + + return loaded_preseeded_providers; + } + + /** + * Asks ProviderAPI to download an anonymous (anon) VPN certificate. + */ + private void downloadAnonCert() { + Intent provider_API_command = new Intent(this, ProviderAPI.class); + + Bundle parameters = new Bundle(); + + parameters.putString(TYPE_OF_CERTIFICATE, ANON_CERTIFICATE); + + provider_API_command.setAction(ProviderAPI.DOWNLOAD_CERTIFICATE); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver); + + startService(provider_API_command); + } + + /** + * Open the new provider dialog + */ + public void addAndSelectNewProvider() { + FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction(); + Fragment previous_new_provider_dialog = getFragmentManager().findFragmentByTag(NewProviderDialog.TAG); + if (previous_new_provider_dialog != null) { + fragment_transaction.remove(previous_new_provider_dialog); + } + fragment_transaction.addToBackStack(null); + + DialogFragment newFragment = NewProviderDialog.newInstance(); + newFragment.show(fragment_transaction, NewProviderDialog.TAG); + } + + /** + * Open the new provider dialog with data + */ + public void addAndSelectNewProvider(String main_url) { + FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction(); + Fragment previous_new_provider_dialog = getFragmentManager().findFragmentByTag(NewProviderDialog.TAG); + if (previous_new_provider_dialog != null) { + fragment_transaction.remove(previous_new_provider_dialog); + } + + DialogFragment newFragment = NewProviderDialog.newInstance(); + Bundle data = new Bundle(); + data.putString(Provider.MAIN_URL, main_url); + newFragment.setArguments(data); + newFragment.show(fragment_transaction, NewProviderDialog.TAG); + } + + /** + * Once selected a provider, this fragment offers the user to log in, + * use it anonymously (if possible) + * or cancel his/her election pressing the back button. + * @param view + * @param reason_to_fail + */ + public void showDownloadFailedDialog(View view, String reason_to_fail) { + FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction(); + Fragment previous_provider_details_dialog = getFragmentManager().findFragmentByTag(DownloadFailedDialog.TAG); + if (previous_provider_details_dialog != null) { + fragment_transaction.remove(previous_provider_details_dialog); + } + fragment_transaction.addToBackStack(null); + + DialogFragment newFragment = DownloadFailedDialog.newInstance(reason_to_fail); + newFragment.show(fragment_transaction, DownloadFailedDialog.TAG); + } + + /** + * Once selected a provider, this fragment offers the user to log in, + * use it anonymously (if possible) + * or cancel his/her election pressing the back button. + * @param view + */ + public void showProviderDetails(View view) { + if(setting_up_provider) { + FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction(); + Fragment previous_provider_details_dialog = getFragmentManager().findFragmentByTag(ProviderDetailFragment.TAG); + if (previous_provider_details_dialog != null) { + fragment_transaction.remove(previous_provider_details_dialog); + } + fragment_transaction.addToBackStack(null); + + DialogFragment newFragment = ProviderDetailFragment.newInstance(); + newFragment.show(fragment_transaction, ProviderDetailFragment.TAG); + } + } + + public void showAndSelectProvider(String provider_main_url) { + if(getId(provider_main_url).isEmpty()) + showProvider(provider_main_url); + autoSelectProvider(provider_main_url); + } + + private void showProvider(final String provider_main_url) { + String provider_name = provider_main_url.replaceFirst("http[s]?://", "").replaceFirst("\\/", "_"); + ProviderItem added_provider = new ProviderItem(provider_name, provider_main_url); + provider_list_fragment.addItem(added_provider); + } + + private void autoSelectProvider(String provider_main_url) { + onItemSelected(getId(provider_main_url)); + } + + /** + * Asks ProviderAPI to download a new provider.json file + * @param provider_name + * @param provider_main_url + */ + public void setUpProvider(String provider_main_url) { + Intent provider_API_command = new Intent(this, ProviderAPI.class); + Bundle parameters = new Bundle(); + parameters.putString(Provider.MAIN_URL, provider_main_url); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver); + + startService(provider_API_command); + setting_up_provider = true; + } + + public void retrySetUpProvider() { + cancelSettingUpProvider(); + if(!ProviderAPI.caCertDownloaded()) { + addAndSelectNewProvider(ProviderAPI.lastProviderMainUrl()); + } else { + Intent provider_API_command = new Intent(this, ProviderAPI.class); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver); + + startService(provider_API_command); + } + } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.configuration_wizard_activity, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item){ + switch (item.getItemId()){ + case R.id.about_leap: + startActivityForResult(new Intent(this, AboutActivity.class), 0); + return true; + case R.id.new_provider: + addAndSelectNewProvider(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + public void showAllProviders() { + provider_list_fragment = (ProviderListFragment) getFragmentManager().findFragmentByTag(ProviderListFragment.TAG); + if(provider_list_fragment != null) + provider_list_fragment.unhideAll(); + } + + public void cancelSettingUpProvider() { + provider_list_fragment = (ProviderListFragment) getFragmentManager().findFragmentByTag(ProviderListFragment.TAG); + if(provider_list_fragment != null) { + provider_list_fragment.removeLastItem(); + } + preferences.edit().remove(Provider.KEY).remove(EIP.ALLOWED_ANON).remove(EIP.KEY).commit(); + } + + @Override + public void login() { + Intent ask_login = new Intent(); + ask_login.putExtra(LogInDialog.VERB, LogInDialog.VERB); + setResult(RESULT_OK, ask_login); + setting_up_provider = false; + finish(); + } + + @Override + public void use_anonymously() { + setResult(RESULT_OK); + setting_up_provider = false; + finish(); + } + + public class ProviderAPIBroadcastReceiver_Update extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + int update = intent.getIntExtra(ProviderAPI.CURRENT_PROGRESS, 0); + mProgressBar.setProgress(update); + } + } +} diff --git a/bitmask_android/src/release/java/se/leap/bitmaskclient/NewProviderDialog.java b/bitmask_android/src/release/java/se/leap/bitmaskclient/NewProviderDialog.java new file mode 100644 index 00000000..7ed1940e --- /dev/null +++ b/bitmask_android/src/release/java/se/leap/bitmaskclient/NewProviderDialog.java @@ -0,0 +1,117 @@ +/** + * 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 . + */ + package se.leap.bitmaskclient; + +import se.leap.bitmaskclient.ProviderListContent.ProviderItem; +import se.leap.bitmaskclient.R; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.Toast; + +/** + * Implements the new custom provider dialog. + * + * @author parmegv + * + */ +public class NewProviderDialog extends DialogFragment { + + final public static String TAG = "newProviderDialog"; + + public interface NewProviderDialogInterface { + public void showAndSelectProvider(String url_provider); + } + + NewProviderDialogInterface interface_with_ConfigurationWizard; + + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance() { + NewProviderDialog dialog_fragment = new NewProviderDialog(); + return dialog_fragment; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + interface_with_ConfigurationWizard = (NewProviderDialogInterface) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement NoticeDialogListener"); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); + View new_provider_dialog_view = inflater.inflate(R.layout.new_provider_dialog, null); + final EditText url_input_field = (EditText)new_provider_dialog_view.findViewById(R.id.new_provider_url); + if(getArguments() != null && getArguments().containsKey(Provider.MAIN_URL)) { + url_input_field.setText(getArguments().getString(Provider.MAIN_URL)); + } + + builder.setView(new_provider_dialog_view) + .setMessage(R.string.introduce_new_provider) + .setPositiveButton(R.string.save, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + String entered_url = url_input_field.getText().toString().trim(); + if(!entered_url.startsWith("https://")) { + if (entered_url.startsWith("http://")){ + entered_url = entered_url.substring("http://".length()); + } + entered_url = "https://".concat(entered_url); + } + if(validURL(entered_url)) { + interface_with_ConfigurationWizard.showAndSelectProvider(entered_url); + 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(); + return android.util.Patterns.WEB_URL.matcher(entered_url).matches(); + } +} diff --git a/bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderAPI.java b/bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderAPI.java new file mode 100644 index 00000000..39ce1146 --- /dev/null +++ b/bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderAPI.java @@ -0,0 +1,860 @@ +/** + * 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 . + */ + package se.leap.bitmaskclient; + +import java.io.DataOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Scanner; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.client.ClientProtocolException; +import org.jboss.security.srp.SRPParameters; +import org.json.JSONException; +import org.json.JSONObject; + +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.ProviderListContent.ProviderItem; +import android.app.IntentService; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.util.Base64; +import android.util.Log; + + +/** + * Implements HTTP api methods used to manage communications with the provider server. + * + * It's an IntentService because it downloads data from the Internet, so it operates in the background. + * + * @author parmegv + * @author MeanderingCode + * + */ +public class ProviderAPI extends IntentService { + + private Handler mHandler; + + final public static String + SET_UP_PROVIDER = "setUpProvider", + DOWNLOAD_NEW_PROVIDER_DOTJSON = "downloadNewProviderDotJSON", + SRP_REGISTER = "srpRegister", + SRP_AUTH = "srpAuth", + LOG_OUT = "logOut", + DOWNLOAD_CERTIFICATE = "downloadUserAuthedCertificate", + PARAMETERS = "parameters", + RESULT_KEY = "result", + RECEIVER_KEY = "receiver", + SESSION_ID_COOKIE_KEY = "session_id_cookie_key", + SESSION_ID_KEY = "session_id", + ERRORS = "errors", + UPDATE_PROGRESSBAR = "update_progressbar", + CURRENT_PROGRESS = "current_progress", + TAG = "provider_api_tag" + ; + + final public static int + CUSTOM_PROVIDER_ADDED = 0, + SRP_AUTHENTICATION_SUCCESSFUL = 3, + SRP_AUTHENTICATION_FAILED = 4, + SRP_REGISTRATION_SUCCESSFUL = 5, + SRP_REGISTRATION_FAILED = 6, + LOGOUT_SUCCESSFUL = 7, + LOGOUT_FAILED = 8, + CORRECTLY_DOWNLOADED_CERTIFICATE = 9, + INCORRECTLY_DOWNLOADED_CERTIFICATE = 10, + PROVIDER_OK = 11, + PROVIDER_NOK = 12, + CORRECTLY_DOWNLOADED_ANON_CERTIFICATE = 13, + INCORRECTLY_DOWNLOADED_ANON_CERTIFICATE = 14 + ; + + private static boolean + CA_CERT_DOWNLOADED = false, + PROVIDER_JSON_DOWNLOADED = false, + EIP_SERVICE_JSON_DOWNLOADED = false + ; + + private static String last_provider_main_url; + private static boolean setting_up_provider = true; + + public static void stop() { + setting_up_provider = false; + } + + public ProviderAPI() { + super("ProviderAPI"); + Log.v("ClassName", "Provider API"); + } + + @Override + public void onCreate() { + super.onCreate(); + mHandler = new Handler(); + CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) ); + } + + public static String lastProviderMainUrl() { + return last_provider_main_url; + } + + private String formatErrorMessage(final int toast_string_id) { + return "{ \"" + ERRORS + "\" : \""+getResources().getString(toast_string_id)+"\" }"; + } + + @Override + protected void onHandleIntent(Intent command) { + final ResultReceiver receiver = command.getParcelableExtra(RECEIVER_KEY); + String action = command.getAction(); + Bundle parameters = command.getBundleExtra(PARAMETERS); + setting_up_provider = true; + + if(action.equalsIgnoreCase(SET_UP_PROVIDER)) { + Bundle result = setUpProvider(parameters); + if(setting_up_provider) { + if(result.getBoolean(RESULT_KEY)) { + receiver.send(PROVIDER_OK, result); + } else { + receiver.send(PROVIDER_NOK, result); + } + } + } else if (action.equalsIgnoreCase(SRP_AUTH)) { + Bundle session_id_bundle = authenticateBySRP(parameters); + if(session_id_bundle.getBoolean(RESULT_KEY)) { + receiver.send(SRP_AUTHENTICATION_SUCCESSFUL, session_id_bundle); + } else { + receiver.send(SRP_AUTHENTICATION_FAILED, session_id_bundle); + } + } else if (action.equalsIgnoreCase(LOG_OUT)) { + if(logOut(parameters)) { + receiver.send(LOGOUT_SUCCESSFUL, Bundle.EMPTY); + } else { + receiver.send(LOGOUT_FAILED, Bundle.EMPTY); + } + } else if (action.equalsIgnoreCase(DOWNLOAD_CERTIFICATE)) { + if(getNewCert(parameters)) { + receiver.send(CORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); + } else { + receiver.send(INCORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); + } + } + } + + /** + * Starts the authentication process using SRP protocol. + * + * @param task containing: username, password and api url. + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if authentication was successful. + */ + private Bundle authenticateBySRP(Bundle task) { + Bundle session_id_bundle = new Bundle(); + int progress = 0; + + String username = (String) task.get(LogInDialog.USERNAME); + String password = (String) task.get(LogInDialog.PASSWORD); + if(validUserLoginData(username, password)) { + + String authentication_server = (String) task.get(Provider.API_URL); + + SRPParameters params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), ConfigHelper.G.toByteArray(), BigInteger.ZERO.toByteArray(), "SHA-256"); + LeapSRPSession client = new LeapSRPSession(username, password, params); + byte[] A = client.exponential(); + broadcast_progress(progress++); + try { + JSONObject saltAndB = sendAToSRPServer(authentication_server, username, new BigInteger(1, A).toString(16)); + if(saltAndB.length() > 0) { + String salt = saltAndB.getString(LeapSRPSession.SALT); + broadcast_progress(progress++); + byte[] Bbytes = new BigInteger(saltAndB.getString("B"), 16).toByteArray(); + byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes); + if(M1 != null) { + broadcast_progress(progress++); + JSONObject session_idAndM2 = sendM1ToSRPServer(authentication_server, username, M1); + if(session_idAndM2.has(LeapSRPSession.M2) && client.verify((byte[])session_idAndM2.get(LeapSRPSession.M2))) { + session_id_bundle.putBoolean(RESULT_KEY, true); + broadcast_progress(progress++); + } else { + session_id_bundle.putBoolean(RESULT_KEY, false); + session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_bad_user_password_user_message)); + session_id_bundle.putString(LogInDialog.USERNAME, username); + } + } else { + session_id_bundle.putBoolean(RESULT_KEY, false); + session_id_bundle.putString(LogInDialog.USERNAME, username); + session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_srp_math_error_user_message)); + } + broadcast_progress(progress++); + } else { + session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_bad_user_password_user_message)); + session_id_bundle.putString(LogInDialog.USERNAME, username); + session_id_bundle.putBoolean(RESULT_KEY, false); + } + } catch (ClientProtocolException e) { + session_id_bundle.putBoolean(RESULT_KEY, false); + session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_client_http_user_message)); + session_id_bundle.putString(LogInDialog.USERNAME, username); + } catch (IOException e) { + session_id_bundle.putBoolean(RESULT_KEY, false); + session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_io_exception_user_message)); + session_id_bundle.putString(LogInDialog.USERNAME, username); + } catch (JSONException e) { + session_id_bundle.putBoolean(RESULT_KEY, false); + session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_json_exception_user_message)); + session_id_bundle.putString(LogInDialog.USERNAME, username); + } catch (NoSuchAlgorithmException e) { + session_id_bundle.putBoolean(RESULT_KEY, false); + session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_no_such_algorithm_exception_user_message)); + session_id_bundle.putString(LogInDialog.USERNAME, username); + } catch (KeyManagementException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (KeyStoreException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } else { + if(!wellFormedPassword(password)) { + session_id_bundle.putBoolean(RESULT_KEY, false); + session_id_bundle.putString(LogInDialog.USERNAME, username); + session_id_bundle.putBoolean(LogInDialog.PASSWORD_INVALID_LENGTH, true); + } + if(username.isEmpty()) { + session_id_bundle.putBoolean(RESULT_KEY, false); + session_id_bundle.putBoolean(LogInDialog.USERNAME_MISSING, true); + } + } + + return session_id_bundle; + } + + /** + * Sets up an intent with the progress value passed as a parameter + * and sends it as a broadcast. + * @param progress + */ + private void broadcast_progress(int progress) { + Intent intentUpdate = new Intent(); + intentUpdate.setAction(UPDATE_PROGRESSBAR); + intentUpdate.addCategory(Intent.CATEGORY_DEFAULT); + intentUpdate.putExtra(CURRENT_PROGRESS, progress); + sendBroadcast(intentUpdate); + } + + /** + * Validates parameters entered by the user to log in + * @param entered_username + * @param entered_password + * @return true if both parameters are present and the entered password length is greater or equal to eight (8). + */ + private boolean validUserLoginData(String entered_username, String entered_password) { + return !(entered_username.isEmpty()) && wellFormedPassword(entered_password); + } + + /** + * Validates a password + * @param entered_password + * @return true if the entered password length is greater or equal to eight (8). + */ + private boolean wellFormedPassword(String entered_password) { + return entered_password.length() >= 8; + } + + /** + * Sends an HTTP POST request to the authentication server with the SRP Parameter A. + * @param server_url + * @param username + * @param clientA First SRP parameter sent + * @return response from authentication server + * @throws ClientProtocolException + * @throws IOException + * @throws JSONException + * @throws CertificateException + * @throws NoSuchAlgorithmException + * @throws KeyStoreException + * @throws KeyManagementException + */ + private JSONObject sendAToSRPServer(String server_url, String username, String clientA) throws ClientProtocolException, IOException, JSONException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException, CertificateException { + Map parameters = new HashMap(); + 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 parameters = new HashMap(); + parameters.put("client_auth", new BigInteger(1, ConfigHelper.trim(m1)).toString(16)); + + //HttpPut put = new HttpPut(server_url + "/sessions/" + username +".json" + "?" + "client_auth" + "=" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)); + JSONObject json_response = sendToServer(server_url + "/sessions/" + username +".json", "PUT", parameters); + + JSONObject session_idAndM2 = new JSONObject(); + if(json_response.length() > 0) { + byte[] M2_not_trimmed = new BigInteger(json_response.getString(LeapSRPSession.M2), 16).toByteArray(); + /*Cookie session_id_cookie = LeapHttpClient.getInstance(getApplicationContext()).getCookieStore().getCookies().get(0); + session_idAndM2.put(ConfigHelper.SESSION_ID_COOKIE_KEY, session_id_cookie.getName()); + session_idAndM2.put(ConfigHelper.SESSION_ID_KEY, session_id_cookie.getValue());*/ + session_idAndM2.put(LeapSRPSession.M2, ConfigHelper.trim(M2_not_trimmed)); + CookieHandler.setDefault(null); // we don't need cookies anymore + String token = json_response.getString(LeapSRPSession.TOKEN); + LeapSRPSession.setToken(token); + } + return session_idAndM2; + } + + /** + * Executes an HTTP request expecting a JSON response. + * @param url + * @param request_method + * @param parameters + * @return response from authentication server + * @throws IOException + * @throws JSONException + * @throws MalformedURLException + * @throws CertificateException + * @throws NoSuchAlgorithmException + * @throws KeyStoreException + * @throws KeyManagementException + */ + private JSONObject sendToServer(String url, String request_method, Map 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 parameters) throws UnsupportedEncodingException { + StringBuilder result = new StringBuilder(); + boolean first = true; + + Iterator parameter_iterator = parameters.keySet().iterator(); + while(parameter_iterator.hasNext()) { + if(first) + first = false; + else + result.append("&&"); + + String key = parameter_iterator.next(); + String value = parameters.get(key); + + result.append(URLEncoder.encode(key, "UTF-8")); + result.append("="); + result.append(URLEncoder.encode(value, "UTF-8")); + } + + return result.toString(); + } + + + + + /** + * Downloads a provider.json from a given URL, adding a new provider using the given name. + * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url. + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful. + */ + private Bundle setUpProvider(Bundle task) { + int progress = 0; + Bundle current_download = new Bundle(); + + if(task != null && task.containsKey(Provider.MAIN_URL)) { + last_provider_main_url = task.getString(Provider.MAIN_URL); + CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = EIP_SERVICE_JSON_DOWNLOADED = false; + } + + if(!CA_CERT_DOWNLOADED) + current_download = downloadCACert(last_provider_main_url); + if(CA_CERT_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) { + broadcast_progress(progress++); + CA_CERT_DOWNLOADED = true; + if(!PROVIDER_JSON_DOWNLOADED) + current_download = getAndSetProviderJson(last_provider_main_url); + if(PROVIDER_JSON_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) { + broadcast_progress(progress++); + PROVIDER_JSON_DOWNLOADED = true; + current_download = getAndSetEipServiceJson(); + if(current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY)) { + broadcast_progress(progress++); + EIP_SERVICE_JSON_DOWNLOADED = true; + } + } + } + + return current_download; + } + + private Bundle downloadCACert(String provider_main_url) { + Bundle result = new Bundle(); + String cert_string = downloadWithCommercialCA(provider_main_url + "/ca.crt"); + + if(validCertificate(cert_string) && setting_up_provider) { + getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(Provider.CA_CERT, cert_string).commit(); + result.putBoolean(RESULT_KEY, true); + } else { + String reason_to_fail = pickErrorMessage(cert_string); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); + } + + return result; + } + + + public static boolean caCertDownloaded() { + return CA_CERT_DOWNLOADED; + } + + private boolean validCertificate(String cert_string) { + boolean result = false; + if(!ConfigHelper.checkErroneousDownload(cert_string)) { + X509Certificate certCert = ConfigHelper.parseX509CertificateFromString(cert_string); + try { + Base64.encodeToString( certCert.getEncoded(), Base64.DEFAULT); + result = true; + } catch (CertificateEncodingException e) { + Log.d(TAG, e.getLocalizedMessage()); + } + } + + return result; + } + + private Bundle getAndSetProviderJson(String provider_main_url) { + Bundle result = new Bundle(); + + if(setting_up_provider) { + String provider_dot_json_string = downloadWithProviderCA(provider_main_url + "/provider.json"); + + try { + JSONObject provider_json = new JSONObject(provider_dot_json_string); + String name = provider_json.getString(Provider.NAME); + //TODO setProviderName(name); + + getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(Provider.KEY, provider_json.toString()).commit(); + getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(EIP.ALLOWED_ANON, provider_json.getJSONObject(Provider.SERVICE).getBoolean(EIP.ALLOWED_ANON)).commit(); + + result.putBoolean(RESULT_KEY, true); + } catch (JSONException e) { + //TODO Error message should be contained in that provider_dot_json_string + String reason_to_fail = pickErrorMessage(provider_dot_json_string); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); + } + } + return result; + } + + + + public static boolean providerJsonDownloaded() { + return PROVIDER_JSON_DOWNLOADED; + } + + private Bundle getAndSetEipServiceJson() { + Bundle result = new Bundle(); + String eip_service_json_string = ""; + if(setting_up_provider) { + try { + JSONObject provider_json = new JSONObject(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.KEY, "")); + String eip_service_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH; + eip_service_json_string = downloadWithProviderCA(eip_service_url); + JSONObject eip_service_json = new JSONObject(eip_service_json_string); + eip_service_json.getInt(Provider.API_RETURN_SERIAL); + + getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(EIP.KEY, eip_service_json.toString()).commit(); + + result.putBoolean(RESULT_KEY, true); + } catch (JSONException e) { + String reason_to_fail = pickErrorMessage(eip_service_json_string); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); + } + } + return result; + } + + public static boolean eipServiceDownloaded() { + return EIP_SERVICE_JSON_DOWNLOADED; + } + + /** + * Interprets the error message as a JSON object and extract the "errors" keyword pair. + * If the error message is not a JSON object, then it is returned untouched. + * @param string_json_error_message + * @return final error message + */ + private String pickErrorMessage(String string_json_error_message) { + String error_message = ""; + try { + JSONObject json_error_message = new JSONObject(string_json_error_message); + error_message = json_error_message.getString(ERRORS); + } catch (JSONException e) { + // TODO Auto-generated catch block + error_message = string_json_error_message; + } + + return error_message; + } + + /** + * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider. + * + * @param string_url + * @return + */ + private String downloadWithCommercialCA(String string_url) { + + String json_file_content = ""; + + URL provider_url = null; + int seconds_of_timeout = 1; + try { + provider_url = new URL(string_url); + URLConnection url_connection = provider_url.openConnection(); + url_connection.setConnectTimeout(seconds_of_timeout*1000); + if(!LeapSRPSession.getToken().isEmpty()) + url_connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token = " + LeapSRPSession.getToken()); + json_file_content = new Scanner(url_connection.getInputStream()).useDelimiter("\\A").next(); + } catch (MalformedURLException e) { + json_file_content = formatErrorMessage(R.string.malformed_url); + } catch(SocketTimeoutException e) { + json_file_content = formatErrorMessage(R.string.server_unreachable_message); + } catch (IOException e) { + if(provider_url != null) { + json_file_content = downloadWithProviderCA(string_url); + } else { + json_file_content = formatErrorMessage(R.string.certificate_error); + } + } catch (Exception e) { + if(provider_url != null) { + json_file_content = downloadWithProviderCA(string_url); + } + } + + return json_file_content; + } + + /** + * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. + * @param url as a string + * @return an empty string if it fails, the url content if not. + */ + private String downloadWithProviderCA(String url_string) { + String json_file_content = ""; + + try { + URL url = new URL(url_string); + // Tell the URLConnection to use a SocketFactory from our SSLContext + HttpsURLConnection urlConnection = + (HttpsURLConnection)url.openConnection(); + urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory()); + if(!LeapSRPSession.getToken().isEmpty()) + urlConnection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); + json_file_content = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next(); + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (UnknownHostException e) { + json_file_content = formatErrorMessage(R.string.server_unreachable_message); + } catch (IOException e) { + // The downloaded certificate doesn't validate our https connection. + json_file_content = formatErrorMessage(R.string.certificate_error); + } catch (KeyStoreException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (KeyManagementException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return json_file_content; + } + + private javax.net.ssl.SSLSocketFactory getProviderSSLSocketFactory() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException { + String provider_cert_string = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.CA_CERT,""); + + java.security.cert.Certificate provider_certificate = ConfigHelper.parseX509CertificateFromString(provider_cert_string); + + // Create a KeyStore containing our trusted CAs + String keyStoreType = KeyStore.getDefaultType(); + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(null, null); + keyStore.setCertificateEntry("provider_ca_certificate", provider_certificate); + + // Create a TrustManager that trusts the CAs in our KeyStore + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init(keyStore); + + // Create an SSLContext that uses our TrustManager + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, tmf.getTrustManagers(), null); + + return context.getSocketFactory(); + } + + /** + * Downloads the string that's in the url with any certificate. + */ + private String downloadWithoutCA(String url_string) { + String string = ""; + try { + + HostnameVerifier hostnameVerifier = new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; + + class DefaultTrustManager implements X509TrustManager { + + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(new KeyManager[0], new TrustManager[] {new DefaultTrustManager()}, new SecureRandom()); + + URL url = new URL(url_string); + HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection(); + urlConnection.setSSLSocketFactory(context.getSocketFactory()); + urlConnection.setHostnameVerifier(hostnameVerifier); + string = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next(); + System.out.println("String ignoring certificate = " + string); + } catch (FileNotFoundException e) { + e.printStackTrace(); + string = formatErrorMessage(R.string.server_unreachable_message); + } catch (IOException e) { + // The downloaded certificate doesn't validate our https connection. + e.printStackTrace(); + string = formatErrorMessage(R.string.certificate_error); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (KeyManagementException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return string; + } + + /** + * Logs out from the api url retrieved from the task. + * @param task containing api url from which the user will log out + * @return true if there were no exceptions + */ + private boolean logOut(Bundle task) { + try { + String delete_url = task.getString(Provider.API_URL) + "/logout"; + int progress = 0; + + HttpsURLConnection urlConnection = (HttpsURLConnection)new URL(delete_url).openConnection(); + urlConnection.setRequestMethod("DELETE"); + urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory()); + + int responseCode = urlConnection.getResponseCode(); + broadcast_progress(progress++); + LeapSRPSession.setToken(""); + Log.d(TAG, Integer.toString(responseCode)); + } catch (ClientProtocolException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } catch (IndexOutOfBoundsException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } catch (KeyManagementException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (KeyStoreException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return true; + } + + /** + * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. + * + * @param task containing the type of the certificate to be downloaded + * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. + */ + private boolean getNewCert(Bundle task) { + + try { + String type_of_certificate = task.getString(ConfigurationWizard.TYPE_OF_CERTIFICATE); + JSONObject provider_json = new JSONObject(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.KEY, "")); + + String provider_main_url = provider_json.getString(Provider.API_URL); + URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.CERTIFICATE); + + String cert_string = downloadWithProviderCA(new_cert_string_url.toString()); + + if(!cert_string.isEmpty()) { + if(ConfigHelper.checkErroneousDownload(cert_string)) { + String reason_to_fail = provider_json.getString(ERRORS); + //result.putString(ConfigHelper.ERRORS_KEY, reason_to_fail); + //result.putBoolean(ConfigHelper.RESULT_KEY, false); + return false; + } else { + + // API returns concatenated cert & key. Split them for OpenVPN options + String certificateString = null, keyString = null; + String[] certAndKey = cert_string.split("(?<=-\n)"); + for (int i=0; i < certAndKey.length-1; i++){ + if ( certAndKey[i].contains("KEY") ) { + keyString = certAndKey[i++] + certAndKey[i]; + } + else if ( certAndKey[i].contains("CERTIFICATE") ) { + certificateString = certAndKey[i++] + certAndKey[i]; + } + } + try { + RSAPrivateKey keyCert = ConfigHelper.parseRsaKeyFromString(keyString); + keyString = Base64.encodeToString( keyCert.getEncoded(), Base64.DEFAULT ); + getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(EIP.PRIVATE_KEY, "-----BEGIN RSA PRIVATE KEY-----\n"+keyString+"-----END RSA PRIVATE KEY-----").commit(); + + X509Certificate certCert = ConfigHelper.parseX509CertificateFromString(certificateString); + certificateString = Base64.encodeToString( certCert.getEncoded(), Base64.DEFAULT); + getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(EIP.CERTIFICATE, "-----BEGIN CERTIFICATE-----\n"+certificateString+"-----END CERTIFICATE-----").commit(); + + return true; + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } + } + } else { + return false; + } + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } /*catch (URISyntaxException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + }*/ + } +} diff --git a/bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderDetailFragment.java b/bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderDetailFragment.java new file mode 100644 index 00000000..42cdd516 --- /dev/null +++ b/bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderDetailFragment.java @@ -0,0 +1,115 @@ +package se.leap.bitmaskclient; + +import org.json.JSONException; +import org.json.JSONObject; + +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.ProviderListContent.ProviderItem; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +public class ProviderDetailFragment extends DialogFragment { + + final public static String TAG = "providerDetailFragment"; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + try { + + LayoutInflater inflater = getActivity().getLayoutInflater(); + View provider_detail_view = inflater.inflate(R.layout.provider_detail_fragment, null); + + JSONObject provider_json = new JSONObject(getActivity().getSharedPreferences(Dashboard.SHARED_PREFERENCES, getActivity().MODE_PRIVATE).getString(Provider.KEY, "")); + + final TextView domain = (TextView)provider_detail_view.findViewById(R.id.provider_detail_domain); + domain.setText(provider_json.getString(Provider.DOMAIN)); + final TextView name = (TextView)provider_detail_view.findViewById(R.id.provider_detail_name); + name.setText(provider_json.getJSONObject(Provider.NAME).getString("en")); + final TextView description = (TextView)provider_detail_view.findViewById(R.id.provider_detail_description); + description.setText(provider_json.getJSONObject(Provider.DESCRIPTION).getString("en")); + + builder.setView(provider_detail_view); + builder.setTitle(R.string.provider_details_fragment_title); + + if(anon_allowed(provider_json)) { + builder.setPositiveButton(R.string.use_anonymously_button, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + interface_with_configuration_wizard.use_anonymously(); + } + }); + } + + if(registration_allowed(provider_json)) { + builder.setNegativeButton(R.string.login_button, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + interface_with_configuration_wizard.login(); + } + }); + } + + return builder.create(); + } catch (JSONException e) { + return null; + } + } + + private boolean anon_allowed(JSONObject provider_json) { + try { + JSONObject service_description = provider_json.getJSONObject(Provider.SERVICE); + return service_description.has(EIP.ALLOWED_ANON) && service_description.getBoolean(EIP.ALLOWED_ANON); + } catch (JSONException e) { + return false; + } + } + + private boolean registration_allowed(JSONObject provider_json) { + try { + JSONObject service_description = provider_json.getJSONObject(Provider.SERVICE); + return service_description.has(Provider.ALLOW_REGISTRATION) && service_description.getBoolean(Provider.ALLOW_REGISTRATION); + } catch (JSONException e) { + return false; + } + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + SharedPreferences.Editor editor = getActivity().getSharedPreferences(Dashboard.SHARED_PREFERENCES, Activity.MODE_PRIVATE).edit(); + editor.remove(Provider.KEY).remove(EIP.ALLOWED_ANON).remove(EIP.KEY).commit(); + interface_with_configuration_wizard.showAllProviders(); + } + + public static DialogFragment newInstance() { + ProviderDetailFragment provider_detail_fragment = new ProviderDetailFragment(); + return provider_detail_fragment; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + interface_with_configuration_wizard = (ProviderDetailFragmentInterface) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement LogInDialogListener"); + } + } + + public interface ProviderDetailFragmentInterface { + public void login(); + public void use_anonymously(); + public void showAllProviders(); + } + + ProviderDetailFragmentInterface interface_with_configuration_wizard; +} diff --git a/bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderListContent.java b/bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderListContent.java new file mode 100644 index 00000000..230c297f --- /dev/null +++ b/bitmask_android/src/release/java/se/leap/bitmaskclient/ProviderListContent.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 . + */ + 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 ITEMS = new ArrayList(); + + public static Map ITEM_MAP = new HashMap(); + + /** + * Adds a new provider item to the end of the items map, and to the items list. + * @param item + */ + public static void addItem(ProviderItem item) { + ITEMS.add(item); + ITEM_MAP.put(String.valueOf(ITEMS.size()), item); + } + public static void removeItem(ProviderItem item) { + ITEMS.remove(item); + ITEM_MAP.remove(item); + } + + /** + * A provider item. + */ + public static class ProviderItem { + final public static String CUSTOM = "custom"; + private String provider_main_url; + private String name; + + /** + * @param name of the provider + * @param urls_file_input_stream file input stream linking with the assets url file + * @param custom if it's a new provider entered by the user or not + */ + public ProviderItem(String name, InputStream urls_file_input_stream) { + + try { + byte[] urls_file_bytes = new byte[urls_file_input_stream.available()]; + urls_file_input_stream.read(urls_file_bytes); + String urls_file_content = new String(urls_file_bytes); + JSONObject file_contents = new JSONObject(urls_file_content); + provider_main_url = file_contents.getString(Provider.MAIN_URL); + this.name = name; + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * @param name of the provider + * @param provider_main_url used to download provider.json file of the provider + * @param provider_json already downloaded + * @param custom if it's a new provider entered by the user or not + */ + public ProviderItem(String name, String provider_main_url) { + this.name = name; + this.provider_main_url = provider_main_url; + } + + public String name() { return name; } + + public String providerMainUrl() { return provider_main_url; } + + public String domain() { + try { + return new URL(provider_main_url).getHost(); + } catch (MalformedURLException e) { + return provider_main_url.replaceFirst("http[s]?://", "").replaceFirst("/.*", ""); + } + } + } +} -- cgit v1.2.3